Skip to main content

usvg/text/
colr.rs

1// Copyright 2024 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use crate::parser::OptionLog;
5use skrifa::instance::LocationRef;
6use skrifa::prelude::Size;
7use skrifa::raw::types::Point;
8use skrifa::{
9    MetadataProvider,
10    color::{Brush, ColorStop, Extend, Transform},
11    outline::DrawSettings,
12};
13use std::fmt::Write as _;
14use svgtypes::Color;
15
16use super::transform::{skrifa_to_tsp_transform, tsp_to_skrifa_transform};
17
18struct Builder<'a> {
19    path: &'a mut String,
20    min_x: f32,
21    min_y: f32,
22    max_x: f32,
23    max_y: f32,
24}
25
26impl<'a> Builder<'a> {
27    fn new(path: &'a mut String) -> Self {
28        Self {
29            path,
30            min_x: f32::MAX,
31            min_y: f32::MAX,
32            max_x: f32::MIN,
33            max_y: f32::MIN,
34        }
35    }
36
37    fn add_point(&mut self, x: f32, y: f32) {
38        self.min_x = self.min_x.min(x);
39        self.min_y = self.min_y.min(y);
40        self.max_x = self.max_x.max(x);
41        self.max_y = self.max_y.max(y);
42    }
43
44    /// Returns a conservative bounding box of the written path.
45    /// It includes curve control points, so it can be larger than the exact
46    /// bounding box, but never smaller.
47    fn bounds(&self) -> Option<tiny_skia_path::Rect> {
48        if self.min_x <= self.max_x && self.min_y <= self.max_y {
49            tiny_skia_path::Rect::from_ltrb(self.min_x, self.min_y, self.max_x, self.max_y)
50        } else {
51            None
52        }
53    }
54
55    fn finish(&mut self) {
56        if !self.path.is_empty() {
57            self.path.pop(); // remove trailing space
58        }
59    }
60}
61
62impl skrifa::outline::OutlinePen for Builder<'_> {
63    fn move_to(&mut self, x: f32, y: f32) {
64        self.add_point(x, y);
65        write!(self.path, "M {} {} ", x, y).unwrap();
66    }
67
68    fn line_to(&mut self, x: f32, y: f32) {
69        self.add_point(x, y);
70        write!(self.path, "L {} {} ", x, y).unwrap();
71    }
72
73    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
74        self.add_point(cx0, cy0);
75        self.add_point(x, y);
76        write!(self.path, "Q {} {} {} {} ", cx0, cy0, x, y).unwrap();
77    }
78
79    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
80        self.add_point(cx0, cy0);
81        self.add_point(cx1, cy1);
82        self.add_point(x, y);
83        write!(self.path, "C {} {} {} {} {} {} ", cx0, cy0, cx1, cy1, x, y).unwrap();
84    }
85
86    fn close(&mut self) {
87        self.path.push_str("Z ");
88    }
89}
90
91trait XmlWriterExt {
92    fn write_color_attribute(&mut self, name: &str, ts: Color);
93    fn write_transform_attribute(&mut self, name: &str, ts: Transform);
94    fn write_spread_method_attribute(&mut self, method: Extend);
95}
96
97impl XmlWriterExt for xmlwriter::XmlWriter {
98    fn write_color_attribute(&mut self, name: &str, color: Color) {
99        self.write_attribute_fmt(
100            name,
101            format_args!("rgb({}, {}, {})", color.red, color.green, color.blue),
102        );
103    }
104
105    fn write_transform_attribute(&mut self, name: &str, ts: Transform) {
106        if ts == Transform::default() {
107            return;
108        }
109
110        self.write_attribute_fmt(
111            name,
112            format_args!(
113                "matrix({} {} {} {} {} {})",
114                ts.xx, ts.yx, ts.xy, ts.yy, ts.dx, ts.dy
115            ),
116        );
117    }
118
119    fn write_spread_method_attribute(&mut self, extend: Extend) {
120        self.write_attribute(
121            "spreadMethod",
122            match extend {
123                Extend::Pad => "pad",
124                Extend::Repeat => "repeat",
125                Extend::Reflect => "reflect",
126                Extend::Unknown => return,
127            },
128        );
129    }
130}
131
132// NOTE: This is only a best-effort translation of COLR into SVG.
133pub(crate) struct GlyphPainter<'a> {
134    pub(crate) font: &'a skrifa::FontRef<'a>,
135    /// The variation location to draw outlines at.
136    pub(crate) location: LocationRef<'a>,
137    pub(crate) svg: &'a mut xmlwriter::XmlWriter,
138    pub(crate) path_buf: &'a mut String,
139    pub(crate) gradient_index: usize,
140    pub(crate) clip_path_index: usize,
141    pub(crate) foreground_color: Color,
142    pub(crate) transform: Transform,
143    pub(crate) outline_transform: Transform,
144    pub(crate) transforms_stack: Vec<Transform>,
145    /// The bounding box of every active clip, in the root coordinate space.
146    /// `None` means the clip is empty (or its bounds are unknown).
147    pub(crate) clip_stack: Vec<Option<tiny_skia_path::Rect>>,
148}
149
150impl<'a> GlyphPainter<'a> {
151    fn write_gradient_stops(&mut self, stops: &[ColorStop]) {
152        for stop in stops {
153            let color = self.palette_index_to_color(stop.palette_index, stop.alpha);
154            self.svg.start_element("stop");
155            self.svg.write_attribute("offset", &stop.offset);
156            self.svg.write_color_attribute("stop-color", color);
157            let opacity = f32::from(color.alpha) / 255.0;
158            self.svg.write_attribute("stop-opacity", &opacity);
159            self.svg.end_element();
160        }
161    }
162
163    fn paint_solid(&mut self, color: Color) {
164        self.svg.start_element("path");
165        self.svg.write_color_attribute("fill", color);
166        let opacity = f32::from(color.alpha) / 255.0;
167        self.svg.write_attribute("fill-opacity", &opacity);
168        self.svg
169            .write_transform_attribute("transform", self.outline_transform);
170        self.svg.write_attribute("d", self.path_buf);
171        self.svg.end_element();
172    }
173
174    fn paint_linear_gradient(
175        &mut self,
176        p0: Point<f32>,
177        p1: Point<f32>,
178        color_stops: &[ColorStop],
179        extend: Extend,
180    ) {
181        let gradient_id = format!("lg{}", self.gradient_index);
182        self.gradient_index += 1;
183
184        let gradient_transform = paint_transform(self.outline_transform, self.transform);
185
186        self.svg.start_element("linearGradient");
187        self.svg.write_attribute("id", &gradient_id);
188        self.svg.write_attribute("x1", &p0.x);
189        self.svg.write_attribute("y1", &p0.y);
190        self.svg.write_attribute("x2", &p1.x);
191        self.svg.write_attribute("y2", &p1.y);
192        self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
193        self.svg.write_spread_method_attribute(extend);
194        self.svg
195            .write_transform_attribute("gradientTransform", gradient_transform);
196        self.write_gradient_stops(color_stops);
197        self.svg.end_element();
198
199        self.svg.start_element("path");
200        self.svg
201            .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id));
202        self.svg
203            .write_transform_attribute("transform", self.outline_transform);
204        self.svg.write_attribute("d", self.path_buf);
205        self.svg.end_element();
206    }
207
208    fn paint_radial_gradient(
209        &mut self,
210        c0: Point<f32>,
211        r0: f32,
212        c1: Point<f32>,
213        r1: f32,
214        color_stops: &[ColorStop],
215        extend: Extend,
216    ) {
217        let gradient_id = format!("rg{}", self.gradient_index);
218        self.gradient_index += 1;
219
220        let gradient_transform = paint_transform(self.outline_transform, self.transform);
221
222        // TODO: Normalizing the stops into the 0..1 range moves the circles onto the
223        // first and last stop, which can make `r0` (and in theory `r1`) negative.
224        // SVG cannot express that, so the color line should be cut where the radius
225        // reaches zero, with an interpolated stop inserted at the cut and the
226        // remaining stops reparameterized into the 0..1 range.
227        self.svg.start_element("radialGradient");
228        self.svg.write_attribute("id", &gradient_id);
229        self.svg.write_attribute("cx", &c1.x);
230        self.svg.write_attribute("cy", &c1.y);
231        self.svg.write_attribute("r", &r1);
232        self.svg.write_attribute("fr", &r0);
233        self.svg.write_attribute("fx", &c0.x);
234        self.svg.write_attribute("fy", &c0.y);
235        self.svg.write_attribute("gradientUnits", &"userSpaceOnUse");
236        self.svg.write_spread_method_attribute(extend);
237        self.svg
238            .write_transform_attribute("gradientTransform", gradient_transform);
239        self.write_gradient_stops(color_stops);
240        self.svg.end_element();
241
242        self.svg.start_element("path");
243        self.svg
244            .write_attribute_fmt("fill", format_args!("url(#{})", gradient_id));
245        self.svg
246            .write_transform_attribute("transform", self.outline_transform);
247        self.svg.write_attribute("d", self.path_buf);
248        self.svg.end_element();
249    }
250
251    fn paint_sweep_gradient(
252        &mut self,
253        _c0: Point<f32>,
254        _start_angle: f32,
255        _end_angle: f32,
256        _color_stops: &[ColorStop],
257        _extend: Extend,
258    ) {
259        // Sweep gradients are not supported.
260        // TODO: surface warning without printing to stdout
261    }
262}
263
264fn paint_transform(outline_transform: Transform, transform: Transform) -> Transform {
265    let outline_transform = skrifa_to_tsp_transform(outline_transform);
266    let gradient_transform = skrifa_to_tsp_transform(transform);
267
268    let gradient_transform = outline_transform
269        .invert()
270        .log_none(|| log::warn!("Failed to calculate transform for gradient in glyph."))
271        .unwrap_or_default()
272        .pre_concat(gradient_transform);
273
274    tsp_to_skrifa_transform(gradient_transform)
275}
276
277/// Returns the bounding box of `rect` transformed by `ts`.
278fn map_rect(
279    rect: tiny_skia_path::Rect,
280    ts: tiny_skia_path::Transform,
281) -> Option<tiny_skia_path::Rect> {
282    let mut points = [
283        tiny_skia_path::Point::from_xy(rect.left(), rect.top()),
284        tiny_skia_path::Point::from_xy(rect.right(), rect.top()),
285        tiny_skia_path::Point::from_xy(rect.left(), rect.bottom()),
286        tiny_skia_path::Point::from_xy(rect.right(), rect.bottom()),
287    ];
288    ts.map_points(&mut points);
289    let min_x = points.iter().map(|p| p.x).fold(f32::MAX, f32::min);
290    let min_y = points.iter().map(|p| p.y).fold(f32::MAX, f32::min);
291    let max_x = points.iter().map(|p| p.x).fold(f32::MIN, f32::max);
292    let max_y = points.iter().map(|p| p.y).fold(f32::MIN, f32::max);
293    tiny_skia_path::Rect::from_ltrb(min_x, min_y, max_x, max_y)
294}
295
296/// Returns the intersection of two rects, or `None` when they do not overlap.
297fn intersect_rects(
298    a: tiny_skia_path::Rect,
299    b: tiny_skia_path::Rect,
300) -> Option<tiny_skia_path::Rect> {
301    tiny_skia_path::Rect::from_ltrb(
302        a.left().max(b.left()),
303        a.top().max(b.top()),
304        a.right().min(b.right()),
305        a.bottom().min(b.bottom()),
306    )
307}
308
309impl GlyphPainter<'_> {
310    fn clip_with_path(&mut self, path: &str) {
311        let clip_id = format!("cp{}", self.clip_path_index);
312        self.clip_path_index += 1;
313
314        self.svg.start_element("clipPath");
315        self.svg.write_attribute("id", &clip_id);
316        self.svg.start_element("path");
317        self.svg
318            .write_transform_attribute("transform", self.outline_transform);
319        self.svg.write_attribute("d", &path);
320        self.svg.end_element();
321        self.svg.end_element();
322
323        self.svg.start_element("g");
324        self.svg
325            .write_attribute_fmt("clip-path", format_args!("url(#{})", clip_id));
326    }
327
328    /// Outlines a glyph into `path_buf` at the current variation location
329    /// (an empty path on failure), records the current transform as the
330    /// outline transform and returns the outline's conservative bounding box
331    /// in the glyph's local coordinate space.
332    fn outline_glyph(&mut self, glyph_id: skrifa::GlyphId) -> Option<tiny_skia_path::Rect> {
333        self.path_buf.clear();
334
335        let mut bounds = None;
336        let outlined = if let Some(outliner) = self.font.outline_glyphs().get(glyph_id) {
337            let mut builder = Builder::new(self.path_buf);
338            let size = Size::unscaled();
339            let ok = outliner
340                .draw(DrawSettings::unhinted(size, self.location), &mut builder)
341                .is_ok();
342            if ok {
343                builder.finish();
344                bounds = builder.bounds();
345            }
346            ok
347        } else {
348            false
349        };
350        if !outlined {
351            // A partial outline may have been written before a draw error.
352            self.path_buf.clear();
353        }
354
355        // We have to write outline using the current transform.
356        self.outline_transform = self.transform;
357
358        bounds
359    }
360
361    /// Paints `path_buf` (positioned by the outline transform) with the given brush.
362    fn paint_brush(&mut self, brush: Brush<'_>) {
363        match brush {
364            Brush::Solid {
365                palette_index,
366                alpha,
367            } => {
368                let color = self.palette_index_to_color(palette_index, alpha);
369                self.paint_solid(color);
370            }
371            Brush::LinearGradient {
372                p0,
373                p1,
374                color_stops,
375                extend,
376            } => self.paint_linear_gradient(p0, p1, color_stops, extend),
377            Brush::RadialGradient {
378                c0,
379                r0,
380                c1,
381                r1,
382                color_stops,
383                extend,
384            } => self.paint_radial_gradient(c0, r0, c1, r1, color_stops, extend),
385
386            Brush::SweepGradient {
387                c0,
388                start_angle,
389                end_angle,
390                color_stops,
391                extend,
392            } => self.paint_sweep_gradient(c0, start_angle, end_angle, color_stops, extend),
393        }
394    }
395
396    fn palette_index_to_color(&self, palette_index: u16, alpha: f32) -> Color {
397        let lookup = || -> Option<Color> {
398            // We always use the first palette. `ColorPalettes` handles
399            // per-palette record offsets internally.
400            let palettes = self.font.color_palettes();
401            let palette = palettes.get(0)?;
402            let color = palette.colors().get(palette_index as usize)?;
403            Some(Color {
404                red: color.red,
405                blue: color.blue,
406                green: color.green,
407                alpha: color.alpha,
408            })
409        };
410
411        let mut color = if palette_index == u16::MAX {
412            self.foreground_color
413        } else {
414            lookup().unwrap_or(self.foreground_color)
415        };
416
417        // Multiply alpha
418        color.alpha = ((color.alpha as f32) * alpha) as u8;
419
420        color
421    }
422}
423
424impl<'a> skrifa::color::ColorPainter for GlyphPainter<'a> {
425    fn push_transform(&mut self, transform: Transform) {
426        self.transforms_stack.push(self.transform);
427        self.transform = self.transform * transform;
428    }
429
430    fn pop_transform(&mut self) {
431        if let Some(ts) = self.transforms_stack.pop() {
432            self.transform = ts;
433        }
434    }
435
436    fn fill_glyph(
437        &mut self,
438        glyph_id: skrifa::GlyphId,
439        brush_transform: Option<Transform>,
440        brush: Brush<'_>,
441    ) {
442        // Fill the glyph outline directly instead of the default
443        // clip-then-fill decomposition. This avoids a redundant clip path
444        // per fill and matches the output of the old ttf-parser based painter.
445        self.outline_glyph(glyph_id);
446
447        if let Some(brush_transform) = brush_transform {
448            self.push_transform(brush_transform);
449            self.paint_brush(brush);
450            self.pop_transform();
451        } else {
452            self.paint_brush(brush);
453        }
454    }
455
456    fn push_clip_glyph(&mut self, glyph_id: skrifa::GlyphId) {
457        let bounds = self.outline_glyph(glyph_id);
458
459        // Clip with the outline. This must always open a clip group - even
460        // when outlining failed (an empty path clips everything away) - since
461        // the corresponding `pop_clip` will unconditionally close it.
462        let path = self.path_buf.clone();
463        self.clip_with_path(&path);
464
465        let root_bounds =
466            bounds.and_then(|b| map_rect(b, skrifa_to_tsp_transform(self.outline_transform)));
467        self.clip_stack.push(root_bounds);
468    }
469
470    fn push_clip_box(&mut self, clip_box: skrifa::raw::types::BoundingBox<f32>) {
471        let x_min = clip_box.x_min;
472        let x_max = clip_box.x_max;
473        let y_min = clip_box.y_min;
474        let y_max = clip_box.y_max;
475
476        let clip_path = format!(
477            "M {} {} L {} {} L {} {} L {} {} Z",
478            x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max
479        );
480
481        // The clip box is positioned by the current transform.
482        self.outline_transform = self.transform;
483        self.clip_with_path(&clip_path);
484
485        let bounds = tiny_skia_path::Rect::from_ltrb(x_min, y_min, x_max, y_max)
486            .and_then(|b| map_rect(b, skrifa_to_tsp_transform(self.outline_transform)));
487        self.clip_stack.push(bounds);
488    }
489
490    fn pop_clip(&mut self) {
491        self.svg.end_element();
492        self.clip_stack.pop();
493    }
494
495    fn fill(&mut self, brush: Brush<'_>) {
496        // A fill paints the intersection of all currently active clips.
497        // Paint a rectangle covering that intersection and let the enclosing
498        // clip groups shape it.
499        if self.clip_stack.is_empty() {
500            log::warn!("Unclipped COLR fills are not supported.");
501            return;
502        }
503
504        let mut region: Option<tiny_skia_path::Rect> = None;
505        for bounds in &self.clip_stack {
506            // A clip with no (or unknown) bounds clips everything away.
507            let Some(bounds) = bounds else { return };
508            region = Some(match region {
509                Some(region) => match intersect_rects(region, *bounds) {
510                    Some(r) => r,
511                    // An empty intersection - there is nothing to paint.
512                    None => return,
513                },
514                None => *bounds,
515            });
516        }
517        let Some(region) = region else { return };
518
519        self.path_buf.clear();
520        write!(
521            self.path_buf,
522            "M {} {} L {} {} L {} {} L {} {} Z",
523            region.left(),
524            region.top(),
525            region.right(),
526            region.top(),
527            region.right(),
528            region.bottom(),
529            region.left(),
530            region.bottom()
531        )
532        .unwrap();
533
534        // The covering rectangle is in the root coordinate space.
535        self.outline_transform = Transform::default();
536
537        self.paint_brush(brush);
538    }
539
540    fn push_layer(&mut self, composite_mode: skrifa::color::CompositeMode) {
541        use skrifa::color::CompositeMode;
542        // TODO: Need to figure out how to represent the other blend modes in SVG.
543        let composite_mode = match composite_mode {
544            CompositeMode::SrcOver => "normal",
545            CompositeMode::Screen => "screen",
546            CompositeMode::Overlay => "overlay",
547            CompositeMode::Darken => "darken",
548            CompositeMode::Lighten => "lighten",
549            CompositeMode::ColorDodge => "color-dodge",
550            CompositeMode::ColorBurn => "color-burn",
551            CompositeMode::HardLight => "hard-light",
552            CompositeMode::SoftLight => "soft-light",
553            CompositeMode::Difference => "difference",
554            CompositeMode::Exclusion => "exclusion",
555            CompositeMode::Multiply => "multiply",
556            CompositeMode::HslHue => "hue",
557            CompositeMode::HslSaturation => "saturation",
558            CompositeMode::HslColor => "color",
559            CompositeMode::HslLuminosity => "luminosity",
560            _ => {
561                // Unsupported blend mode
562                // TODO: support other blend modes
563                // TODO: surface warning without printing to stdout
564                "normal"
565            }
566        };
567
568        self.svg.start_element("g");
569        self.svg.write_attribute_fmt(
570            "style",
571            format_args!("mix-blend-mode: {}; isolation: isolate", composite_mode),
572        );
573    }
574
575    fn pop_layer(&mut self) {
576        self.svg.end_element();
577    }
578}