typst_library/text/font/
color.rs

1//! Utilities for color font handling
2
3use std::io::Read;
4
5use ttf_parser::{GlyphId, RgbaColor};
6use typst_syntax::Span;
7use usvg::tiny_skia_path;
8use xmlwriter::XmlWriter;
9
10use crate::foundations::Bytes;
11use crate::layout::{Abs, Frame, FrameItem, Point, Size};
12use crate::text::{Font, Glyph};
13use crate::visualize::{
14    ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, SvgImage,
15};
16
17/// Whether this glyph should be rendered via simple outlining instead of via
18/// `glyph_frame`.
19pub fn should_outline(font: &Font, glyph: &Glyph) -> bool {
20    let ttf = font.ttf();
21    let glyph_id = GlyphId(glyph.id);
22    (ttf.tables().glyf.is_some() || ttf.tables().cff.is_some())
23        && !ttf
24            .glyph_raster_image(glyph_id, u16::MAX)
25            .is_some_and(|img| img.format == ttf_parser::RasterImageFormat::PNG)
26        && !ttf.is_color_glyph(glyph_id)
27        && ttf.glyph_svg_image(glyph_id).is_none()
28}
29
30/// Returns a frame representing a glyph and whether it is a fallback tofu
31/// frame.
32///
33/// Should only be called on glyphs for which [`should_outline`] returns false.
34///
35/// The glyphs are sized in font units, [`text.item.size`] is not taken into
36/// account.
37#[comemo::memoize]
38pub fn glyph_frame(font: &Font, glyph_id: u16) -> (Frame, bool) {
39    let upem = Abs::pt(font.units_per_em());
40    let glyph_id = GlyphId(glyph_id);
41
42    let mut frame = Frame::soft(Size::splat(upem));
43    let mut tofu = false;
44
45    if draw_glyph(&mut frame, font, upem, glyph_id).is_none()
46        && font.ttf().glyph_index(' ') != Some(glyph_id)
47    {
48        // Generate a fallback tofu if the glyph couldn't be drawn, unless it is
49        // the space glyph. Then, an empty frame does the job. (This happens for
50        // some rare CBDT fonts, which don't define a bitmap for the space, but
51        // also don't have a glyf or CFF table.)
52        draw_fallback_tofu(&mut frame, font, upem, glyph_id);
53        tofu = true;
54    }
55
56    (frame, tofu)
57}
58
59/// Tries to draw a glyph.
60fn draw_glyph(
61    frame: &mut Frame,
62    font: &Font,
63    upem: Abs,
64    glyph_id: GlyphId,
65) -> Option<()> {
66    let ttf = font.ttf();
67    if let Some(raster_image) = ttf
68        .glyph_raster_image(glyph_id, u16::MAX)
69        .filter(|img| img.format == ttf_parser::RasterImageFormat::PNG)
70    {
71        draw_raster_glyph(frame, font, upem, raster_image)
72    } else if ttf.is_color_glyph(glyph_id) {
73        draw_colr_glyph(frame, font, upem, glyph_id)
74    } else if ttf.glyph_svg_image(glyph_id).is_some() {
75        draw_svg_glyph(frame, font, upem, glyph_id)
76    } else {
77        None
78    }
79}
80
81/// Draws a fallback tofu box with the advance width of the glyph.
82fn draw_fallback_tofu(frame: &mut Frame, font: &Font, upem: Abs, glyph_id: GlyphId) {
83    let advance = font
84        .ttf()
85        .glyph_hor_advance(glyph_id)
86        .map(|advance| Abs::pt(advance as f64))
87        .unwrap_or(upem / 3.0);
88    let inset = 0.15 * advance;
89    let height = 0.7 * upem;
90    let pos = Point::new(inset, upem - height);
91    let size = Size::new(advance - inset * 2.0, height);
92    let thickness = upem / 20.0;
93    let stroke = FixedStroke { thickness, ..Default::default() };
94    let shape = Geometry::Rect(size).stroked(stroke);
95    frame.push(pos, FrameItem::Shape(shape, Span::detached()));
96}
97
98/// Draws a raster glyph in a frame.
99///
100/// Supports only PNG images.
101fn draw_raster_glyph(
102    frame: &mut Frame,
103    font: &Font,
104    upem: Abs,
105    raster_image: ttf_parser::RasterGlyphImage,
106) -> Option<()> {
107    let data = Bytes::new(raster_image.data.to_vec());
108    let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?);
109
110    // Apple Color emoji doesn't provide offset information (or at least
111    // not in a way ttf-parser understands), so we artificially shift their
112    // baseline to make it look good.
113    let y_offset = if font.info().family.to_lowercase() == "apple color emoji" {
114        20.0
115    } else {
116        -(raster_image.y as f64)
117    };
118
119    let position = Point::new(
120        upem * raster_image.x as f64 / raster_image.pixels_per_em as f64,
121        upem * y_offset / raster_image.pixels_per_em as f64,
122    );
123    let aspect_ratio = image.width() / image.height();
124    let size = Size::new(upem, upem * aspect_ratio);
125    frame.push(position, FrameItem::Image(image, size, Span::detached()));
126
127    Some(())
128}
129
130/// Draws a glyph from the COLR table into the frame.
131fn draw_colr_glyph(
132    frame: &mut Frame,
133    font: &Font,
134    upem: Abs,
135    glyph_id: GlyphId,
136) -> Option<()> {
137    let mut svg = XmlWriter::new(xmlwriter::Options::default());
138
139    let ttf = font.ttf();
140    let width = ttf.global_bounding_box().width() as f64;
141    let height = ttf.global_bounding_box().height() as f64;
142    let x_min = ttf.global_bounding_box().x_min as f64;
143    let y_max = ttf.global_bounding_box().y_max as f64;
144    let tx = -x_min;
145    let ty = -y_max;
146
147    svg.start_element("svg");
148    svg.write_attribute("xmlns", "http://www.w3.org/2000/svg");
149    svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
150    svg.write_attribute("width", &width);
151    svg.write_attribute("height", &height);
152    svg.write_attribute_fmt("viewBox", format_args!("0 0 {width} {height}"));
153
154    let mut path_buf = String::with_capacity(256);
155    let gradient_index = 1;
156    let clip_path_index = 1;
157
158    svg.start_element("g");
159    svg.write_attribute_fmt(
160        "transform",
161        format_args!("matrix(1 0 0 -1 0 0) matrix(1 0 0 1 {tx} {ty})"),
162    );
163
164    let mut glyph_painter = GlyphPainter {
165        face: ttf,
166        svg: &mut svg,
167        path_buf: &mut path_buf,
168        gradient_index,
169        clip_path_index,
170        palette_index: 0,
171        transform: ttf_parser::Transform::default(),
172        outline_transform: ttf_parser::Transform::default(),
173        transforms_stack: vec![ttf_parser::Transform::default()],
174    };
175
176    ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?;
177    svg.end_element();
178
179    let data = Bytes::from_string(svg.end_document());
180    let image = Image::plain(SvgImage::new(data).ok()?);
181
182    let y_shift = Abs::pt(upem.to_pt() - y_max);
183    let position = Point::new(Abs::pt(x_min), y_shift);
184    let size = Size::new(Abs::pt(width), Abs::pt(height));
185    frame.push(position, FrameItem::Image(image, size, Span::detached()));
186
187    Some(())
188}
189
190/// Draws an SVG glyph in a frame.
191fn draw_svg_glyph(
192    frame: &mut Frame,
193    font: &Font,
194    upem: Abs,
195    glyph_id: GlyphId,
196) -> Option<()> {
197    // TODO: Our current conversion of the SVG table works for Twitter Color Emoji,
198    // but might not work for others. See also: https://github.com/RazrFalcon/resvg/pull/776
199    let mut data = font.ttf().glyph_svg_image(glyph_id)?.data;
200
201    // Decompress SVGZ.
202    let mut decoded = vec![];
203    if data.starts_with(&[0x1f, 0x8b]) {
204        let mut decoder = flate2::read::GzDecoder::new(data);
205        decoder.read_to_end(&mut decoded).ok()?;
206        data = &decoded;
207    }
208
209    // Parse XML.
210    let xml = std::str::from_utf8(data).ok()?;
211    let document = roxmltree::Document::parse(xml).ok()?;
212
213    // Parse SVG.
214    let opts = usvg::Options::default();
215    let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
216
217    let bbox = tree.root().bounding_box();
218    let width = bbox.width() as f64;
219    let height = bbox.height() as f64;
220    let left = bbox.left() as f64;
221    let top = bbox.top() as f64;
222
223    let mut data = tree.to_string(&usvg::WriteOptions::default());
224
225    // The SVG coordinates and the font coordinates are not the same: the Y axis
226    // is mirrored. But the origin of the axes are the same (which means that
227    // the horizontal axis in the SVG document corresponds to the baseline). See
228    // the reference for more details:
229    // https://learn.microsoft.com/en-us/typography/opentype/spec/svg#coordinate-systems-and-glyph-metrics
230    //
231    // If we used the SVG document as it is, svg2pdf would produce a cropped
232    // glyph (only what is under the baseline would be visible). So we need to
233    // embed the original SVG in another one that has the exact dimensions of
234    // the glyph, with a transform to make it fit. We also need to remove the
235    // viewBox, height and width attributes from the inner SVG, otherwise usvg
236    // takes into account these values to clip the embedded SVG.
237    make_svg_unsized(&mut data);
238    let wrapper_svg = format!(
239        r#"
240        <svg
241            width="{width}"
242            height="{height}"
243            viewBox="0 0 {width} {height}"
244            xmlns="http://www.w3.org/2000/svg">
245            <g transform="matrix(1 0 0 1 {tx} {ty})">
246            {inner}
247            </g>
248        </svg>
249    "#,
250        inner = data,
251        tx = -left,
252        ty = -top,
253    );
254
255    let data = Bytes::from_string(wrapper_svg);
256    let image = Image::plain(SvgImage::new(data).ok()?);
257
258    let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
259    let size = Size::new(Abs::pt(width), Abs::pt(height));
260    frame.push(position, FrameItem::Image(image, size, Span::detached()));
261
262    Some(())
263}
264
265/// Remove all size specifications (viewBox, width and height attributes) from a
266/// SVG document.
267fn make_svg_unsized(svg: &mut String) {
268    let mut viewbox_range = None;
269    let mut width_range = None;
270    let mut height_range = None;
271
272    let mut s = unscanny::Scanner::new(svg);
273
274    s.eat_until("<svg");
275    s.eat_if("<svg");
276    while !s.eat_if('>') && !s.done() {
277        s.eat_whitespace();
278        let start = s.cursor();
279        let attr_name = s.eat_until('=').trim();
280        // Eat the equal sign and the quote.
281        s.eat();
282        s.eat();
283        let mut escaped = false;
284        while (escaped || !s.eat_if('"')) && !s.done() {
285            escaped = s.eat() == Some('\\');
286        }
287        match attr_name {
288            "viewBox" => viewbox_range = Some(start..s.cursor()),
289            "width" => width_range = Some(start..s.cursor()),
290            "height" => height_range = Some(start..s.cursor()),
291            _ => {}
292        }
293    }
294
295    // Remove the `viewBox` attribute.
296    if let Some(range) = viewbox_range {
297        svg.replace_range(range.clone(), &" ".repeat(range.len()));
298    }
299
300    // Remove the `width` attribute.
301    if let Some(range) = width_range {
302        svg.replace_range(range.clone(), &" ".repeat(range.len()));
303    }
304
305    // Remove the `height` attribute.
306    if let Some(range) = height_range {
307        svg.replace_range(range, "");
308    }
309}
310
311struct ColrBuilder<'a>(&'a mut String);
312
313impl ColrBuilder<'_> {
314    fn finish(&mut self) {
315        if !self.0.is_empty() {
316            self.0.pop(); // remove trailing space
317        }
318    }
319}
320
321impl ttf_parser::OutlineBuilder for ColrBuilder<'_> {
322    fn move_to(&mut self, x: f32, y: f32) {
323        use std::fmt::Write;
324        write!(self.0, "M {x} {y} ").unwrap()
325    }
326
327    fn line_to(&mut self, x: f32, y: f32) {
328        use std::fmt::Write;
329        write!(self.0, "L {x} {y} ").unwrap()
330    }
331
332    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
333        use std::fmt::Write;
334        write!(self.0, "Q {x1} {y1} {x} {y} ").unwrap()
335    }
336
337    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
338        use std::fmt::Write;
339        write!(self.0, "C {x1} {y1} {x2} {y2} {x} {y} ").unwrap()
340    }
341
342    fn close(&mut self) {
343        self.0.push_str("Z ")
344    }
345}
346
347// NOTE: This is only a best-effort translation of COLR into SVG. It's not feature-complete
348// and it's also not possible to make it feature-complete using just raw SVG features.
349pub(crate) struct GlyphPainter<'a> {
350    pub(crate) face: &'a ttf_parser::Face<'a>,
351    pub(crate) svg: &'a mut xmlwriter::XmlWriter,
352    pub(crate) path_buf: &'a mut String,
353    pub(crate) gradient_index: usize,
354    pub(crate) clip_path_index: usize,
355    pub(crate) palette_index: u16,
356    pub(crate) transform: ttf_parser::Transform,
357    pub(crate) outline_transform: ttf_parser::Transform,
358    pub(crate) transforms_stack: Vec<ttf_parser::Transform>,
359}
360
361impl<'a> GlyphPainter<'a> {
362    fn write_gradient_stops(&mut self, stops: ttf_parser::colr::GradientStopsIter) {
363        for stop in stops {
364            self.svg.start_element("stop");
365            self.svg.write_attribute("offset", &stop.stop_offset);
366            self.write_color_attribute("stop-color", stop.color);
367            let opacity = f32::from(stop.color.alpha) / 255.0;
368            self.svg.write_attribute("stop-opacity", &opacity);
369            self.svg.end_element();
370        }
371    }
372
373    fn write_color_attribute(&mut self, name: &str, color: ttf_parser::RgbaColor) {
374        self.svg.write_attribute_fmt(
375            name,
376            format_args!("rgb({}, {}, {})", color.red, color.green, color.blue),
377        );
378    }
379
380    fn write_transform_attribute(&mut self, name: &str, ts: ttf_parser::Transform) {
381        if ts.is_default() {
382            return;
383        }
384
385        self.svg.write_attribute_fmt(
386            name,
387            format_args!("matrix({} {} {} {} {} {})", ts.a, ts.b, ts.c, ts.d, ts.e, ts.f),
388        );
389    }
390
391    fn write_spread_method_attribute(
392        &mut self,
393        extend: ttf_parser::colr::GradientExtend,
394    ) {
395        self.svg.write_attribute(
396            "spreadMethod",
397            match extend {
398                ttf_parser::colr::GradientExtend::Pad => &"pad",
399                ttf_parser::colr::GradientExtend::Repeat => &"repeat",
400                ttf_parser::colr::GradientExtend::Reflect => &"reflect",
401            },
402        );
403    }
404
405    fn paint_solid(&mut self, color: ttf_parser::RgbaColor) {
406        self.svg.start_element("path");
407        self.write_color_attribute("fill", color);
408        let opacity = f32::from(color.alpha) / 255.0;
409        self.svg.write_attribute("fill-opacity", &opacity);
410        self.write_transform_attribute("transform", self.outline_transform);
411        self.svg.write_attribute("d", self.path_buf);
412        self.svg.end_element();
413    }
414
415    fn paint_linear_gradient(&mut self, gradient: ttf_parser::colr::LinearGradient<'a>) {
416        let gradient_id = format!("lg{}", self.gradient_index);
417        self.gradient_index += 1;
418
419        let gradient_transform = paint_transform(self.outline_transform, self.transform);
420
421        // TODO: We ignore x2, y2. Have to apply them somehow.
422        // TODO: The way spreadMode works in ttf and svg is a bit different. In SVG, the spreadMode
423        // will always be applied based on x1/y1 and x2/y2. However, in TTF the spreadMode will
424        // be applied from the first/last stop. So if we have a gradient with x1=0 x2=1, and
425        // a stop at x=0.4 and x=0.6, then in SVG we will always see a padding, while in ttf
426        // we will see the actual spreadMode. We need to account for that somehow.
427        self.svg.start_element("linearGradient");
428        self.svg.write_attribute("id", &gradient_id);
429        self.svg.write_attribute("x1", &gradient.x0);
430        self.svg.write_attribute("y1", &gradient.y0);
431        self.svg.write_attribute("x2", &gradient.x1);
432        self.svg.write_attribute("y2", &gradient.y1);
433        self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
434        self.write_spread_method_attribute(gradient.extend);
435        self.write_transform_attribute("gradientTransform", gradient_transform);
436        self.write_gradient_stops(
437            gradient.stops(self.palette_index, self.face.variation_coordinates()),
438        );
439        self.svg.end_element();
440
441        self.svg.start_element("path");
442        self.svg
443            .write_attribute_fmt("fill", format_args!("url(#{gradient_id})"));
444        self.write_transform_attribute("transform", self.outline_transform);
445        self.svg.write_attribute("d", self.path_buf);
446        self.svg.end_element();
447    }
448
449    fn paint_radial_gradient(&mut self, gradient: ttf_parser::colr::RadialGradient<'a>) {
450        let gradient_id = format!("rg{}", self.gradient_index);
451        self.gradient_index += 1;
452
453        let gradient_transform = paint_transform(self.outline_transform, self.transform);
454
455        self.svg.start_element("radialGradient");
456        self.svg.write_attribute("id", &gradient_id);
457        self.svg.write_attribute("cx", &gradient.x1);
458        self.svg.write_attribute("cy", &gradient.y1);
459        self.svg.write_attribute("r", &gradient.r1);
460        self.svg.write_attribute("fr", &gradient.r0);
461        self.svg.write_attribute("fx", &gradient.x0);
462        self.svg.write_attribute("fy", &gradient.y0);
463        self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
464        self.write_spread_method_attribute(gradient.extend);
465        self.write_transform_attribute("gradientTransform", gradient_transform);
466        self.write_gradient_stops(
467            gradient.stops(self.palette_index, self.face.variation_coordinates()),
468        );
469        self.svg.end_element();
470
471        self.svg.start_element("path");
472        self.svg
473            .write_attribute_fmt("fill", format_args!("url(#{gradient_id})"));
474        self.write_transform_attribute("transform", self.outline_transform);
475        self.svg.write_attribute("d", self.path_buf);
476        self.svg.end_element();
477    }
478
479    fn paint_sweep_gradient(&mut self, _: ttf_parser::colr::SweepGradient<'a>) {}
480}
481
482fn paint_transform(
483    outline_transform: ttf_parser::Transform,
484    transform: ttf_parser::Transform,
485) -> ttf_parser::Transform {
486    let outline_transform = tiny_skia_path::Transform::from_row(
487        outline_transform.a,
488        outline_transform.b,
489        outline_transform.c,
490        outline_transform.d,
491        outline_transform.e,
492        outline_transform.f,
493    );
494
495    let gradient_transform = tiny_skia_path::Transform::from_row(
496        transform.a,
497        transform.b,
498        transform.c,
499        transform.d,
500        transform.e,
501        transform.f,
502    );
503
504    let gradient_transform = outline_transform
505        .invert()
506        // In theory, we should error out. But the transform shouldn't ever be uninvertible, so let's ignore it.
507        .unwrap_or_default()
508        .pre_concat(gradient_transform);
509
510    ttf_parser::Transform {
511        a: gradient_transform.sx,
512        b: gradient_transform.ky,
513        c: gradient_transform.kx,
514        d: gradient_transform.sy,
515        e: gradient_transform.tx,
516        f: gradient_transform.ty,
517    }
518}
519
520impl GlyphPainter<'_> {
521    fn clip_with_path(&mut self, path: &str) {
522        let clip_id = format!("cp{}", self.clip_path_index);
523        self.clip_path_index += 1;
524
525        self.svg.start_element("clipPath");
526        self.svg.write_attribute("id", &clip_id);
527        self.svg.start_element("path");
528        self.write_transform_attribute("transform", self.outline_transform);
529        self.svg.write_attribute("d", &path);
530        self.svg.end_element();
531        self.svg.end_element();
532
533        self.svg.start_element("g");
534        self.svg
535            .write_attribute_fmt("clip-path", format_args!("url(#{clip_id})"));
536    }
537}
538
539impl<'a> ttf_parser::colr::Painter<'a> for GlyphPainter<'a> {
540    fn outline_glyph(&mut self, glyph_id: ttf_parser::GlyphId) {
541        self.path_buf.clear();
542        let mut builder = ColrBuilder(self.path_buf);
543        match self.face.outline_glyph(glyph_id, &mut builder) {
544            Some(v) => v,
545            None => return,
546        };
547        builder.finish();
548
549        // We have to write outline using the current transform.
550        self.outline_transform = self.transform;
551    }
552
553    fn push_layer(&mut self, mode: ttf_parser::colr::CompositeMode) {
554        self.svg.start_element("g");
555
556        use ttf_parser::colr::CompositeMode;
557        // TODO: Need to figure out how to represent the other blend modes
558        // in SVG.
559        let mode = match mode {
560            CompositeMode::SourceOver => "normal",
561            CompositeMode::Screen => "screen",
562            CompositeMode::Overlay => "overlay",
563            CompositeMode::Darken => "darken",
564            CompositeMode::Lighten => "lighten",
565            CompositeMode::ColorDodge => "color-dodge",
566            CompositeMode::ColorBurn => "color-burn",
567            CompositeMode::HardLight => "hard-light",
568            CompositeMode::SoftLight => "soft-light",
569            CompositeMode::Difference => "difference",
570            CompositeMode::Exclusion => "exclusion",
571            CompositeMode::Multiply => "multiply",
572            CompositeMode::Hue => "hue",
573            CompositeMode::Saturation => "saturation",
574            CompositeMode::Color => "color",
575            CompositeMode::Luminosity => "luminosity",
576            _ => "normal",
577        };
578        self.svg.write_attribute_fmt(
579            "style",
580            format_args!("mix-blend-mode: {mode}; isolation: isolate"),
581        );
582    }
583
584    fn pop_layer(&mut self) {
585        self.svg.end_element(); // g
586    }
587
588    fn push_transform(&mut self, transform: ttf_parser::Transform) {
589        self.transforms_stack.push(self.transform);
590        self.transform = ttf_parser::Transform::combine(self.transform, transform);
591    }
592
593    fn paint(&mut self, paint: ttf_parser::colr::Paint<'a>) {
594        match paint {
595            ttf_parser::colr::Paint::Solid(color) => self.paint_solid(color),
596            ttf_parser::colr::Paint::LinearGradient(lg) => self.paint_linear_gradient(lg),
597            ttf_parser::colr::Paint::RadialGradient(rg) => self.paint_radial_gradient(rg),
598            ttf_parser::colr::Paint::SweepGradient(sg) => self.paint_sweep_gradient(sg),
599        }
600    }
601
602    fn pop_transform(&mut self) {
603        if let Some(ts) = self.transforms_stack.pop() {
604            self.transform = ts
605        }
606    }
607
608    fn push_clip(&mut self) {
609        self.clip_with_path(&self.path_buf.clone());
610    }
611
612    fn pop_clip(&mut self) {
613        self.svg.end_element();
614    }
615
616    fn push_clip_box(&mut self, clipbox: ttf_parser::colr::ClipBox) {
617        let x_min = clipbox.x_min;
618        let x_max = clipbox.x_max;
619        let y_min = clipbox.y_min;
620        let y_max = clipbox.y_max;
621
622        let clip_path = format!(
623            "M {x_min} {y_min} L {x_max} {y_min} L {x_max} {y_max} L {x_min} {y_max} Z"
624        );
625
626        self.clip_with_path(&clip_path);
627    }
628}