From 0fe8f09ba98e8dac115c32c7182feff750d22525 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:49:51 +0200 Subject: [PATCH] Add support for drawing COLR glyphs in SVG export (#6693) --- crates/typst-library/src/text/font/color.rs | 29 +++++++++---- crates/typst-svg/src/text.rs | 46 ++++++++++++++++++++- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index 0a7b13c9..92f4814d 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -127,13 +127,8 @@ fn draw_raster_glyph( Some(()) } -/// Draws a glyph from the COLR table into the frame. -fn draw_colr_glyph( - frame: &mut Frame, - font: &Font, - upem: Abs, - glyph_id: GlyphId, -) -> Option<()> { +/// Convert a COLR glyph into an SVG file. +pub fn colr_glyph_to_svg(font: &Font, glyph_id: GlyphId) -> Option { let mut svg = XmlWriter::new(xmlwriter::Options::default()); let ttf = font.ttf(); @@ -176,7 +171,25 @@ fn draw_colr_glyph( ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?; svg.end_element(); - let data = Bytes::from_string(svg.end_document()); + Some(svg.end_document()) +} + +/// Draws a glyph from the COLR table into the frame. +fn draw_colr_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + glyph_id: GlyphId, +) -> Option<()> { + let svg_string = colr_glyph_to_svg(font, glyph_id)?; + + let ttf = font.ttf(); + let width = ttf.global_bounding_box().width() as f64; + let height = ttf.global_bounding_box().height() as f64; + let x_min = ttf.global_bounding_box().x_min as f64; + let y_max = ttf.global_bounding_box().y_max as f64; + + let data = Bytes::from_string(svg_string); let image = Image::plain(SvgImage::new(data).ok()?); let y_shift = Abs::pt(upem.to_pt() - y_max); diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index fdc3497e..0c4cd22f 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -5,6 +5,7 @@ use ecow::EcoString; use ttf_parser::GlyphId; use typst_library::foundations::Bytes; use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; +use typst_library::text::color::colr_glyph_to_svg; use typst_library::text::{Font, TextItem}; use typst_library::visualize::{ ExchangeFormat, FillRule, Image, Paint, RasterImage, RelativeTo, @@ -31,7 +32,8 @@ impl SVGRenderer<'_> { let x_offset = x + glyph.x_offset.at(text.size).to_pt(); let y_offset = y + glyph.y_offset.at(text.size).to_pt(); - self.render_svg_glyph(text, id, x_offset, y_offset, scale) + self.render_colr_glyph(text, id, x_offset, y_offset, scale) + .or_else(|| self.render_svg_glyph(text, id, x_offset, y_offset, scale)) .or_else(|| self.render_bitmap_glyph(text, id, x_offset, y_offset)) .or_else(|| { self.render_outline_glyph( @@ -87,6 +89,42 @@ impl SVGRenderer<'_> { Some(()) } + /// Render a glyph defined by COLR glyph descriptions. + fn render_colr_glyph( + &mut self, + text: &TextItem, + id: GlyphId, + x_offset: f64, + y_offset: f64, + scale: f64, + ) -> Option<()> { + let ttf = text.font.ttf(); + let converted = colr_glyph_to_svg(&text.font, id)?; + let width = ttf.global_bounding_box().width() as f64; + let height = ttf.global_bounding_box().height() as f64; + let data_url = svg_to_base64(&converted); + + let x_min = ttf.global_bounding_box().x_min as f64; + let y_max = ttf.global_bounding_box().y_max as f64; + + let glyph_hash = hash128(&(&text.font, id)); + let id = self.glyphs.insert_with(glyph_hash, || RenderedGlyph::Image { + url: data_url, + width, + height, + ts: Transform::scale(Ratio::new(scale), Ratio::new(-scale)) + .pre_concat(Transform::translate(Abs::pt(x_min), -Abs::pt(y_max))), + }); + + self.xml.start_element("use"); + self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); + self.xml.write_attribute("x", &(x_offset)); + self.xml.write_attribute("y", &(y_offset)); + self.xml.end_element(); + + Some(()) + } + /// Render a glyph defined by a bitmap. fn render_bitmap_glyph( &mut self, @@ -320,10 +358,14 @@ fn convert_svg_glyph_to_base64_url(font: &Font, id: GlyphId) -> Option EcoString { let mut url: EcoString = "data:image/svg+xml;base64,".into(); let b64_encoded = base64::engine::general_purpose::STANDARD.encode(svg_str.as_bytes()); url.push_str(&b64_encoded); - Some(url) + url }