Skip to main content

glifo/
colr.rs

1// Copyright 2025 the Vello Authors and the Parley Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Drawing COLR glyphs.
5
6use crate::atlas::commands::AtlasPaint;
7use crate::color::Srgb;
8use crate::color::{AlphaColor, DynamicColor};
9use crate::glyph::{
10    CachedOutline, FontEmbolden, FontInfo, GlyphColr, OutlineCacheSession, OutlinePath,
11    VarLookupKey,
12};
13use crate::interface::DrawSink;
14use crate::kurbo::{Affine, Point, Rect, Shape};
15use crate::peniko::{self, BlendMode, ColorStops, Compose, Extend, Gradient, Mix};
16use crate::util::FloatExt;
17use alloc::sync::Arc;
18use alloc::vec;
19use alloc::vec::Vec;
20use core::fmt::Debug;
21use peniko::{LinearGradientPosition, RadialGradientPosition, SweepGradientPosition};
22use skrifa::color::{Brush, ColorPainter, ColorStop, CompositeMode, Transform};
23use skrifa::instance::LocationRef;
24use skrifa::outline::OutlineGlyphCollection;
25use skrifa::raw::TableProvider;
26use skrifa::raw::types::BoundingBox;
27use skrifa::{FontRef, GlyphId, MetadataProvider};
28use smallvec::SmallVec;
29
30trait ColrDrawSinkExt: DrawSink {
31    fn fill_with_paint(&mut self, rect: &Rect, paint: AtlasPaint) {
32        self.set_paint(paint);
33        self.fill_rect(rect);
34    }
35
36    fn fill_solid(&mut self, rect: &Rect, color: AlphaColor<Srgb>) {
37        self.fill_with_paint(rect, AtlasPaint::Solid(color));
38    }
39
40    fn fill_gradient(&mut self, rect: &Rect, gradient: Gradient) {
41        self.fill_with_paint(rect, AtlasPaint::Gradient(gradient));
42    }
43}
44
45impl<T: DrawSink + ?Sized> ColrDrawSinkExt for T {}
46
47/// An abstraction for painting COLR glyphs.
48pub(crate) struct ColrPainter<'a, 'b> {
49    transforms: Vec<Affine>,
50    colr_glyph: &'a GlyphColr<'a>,
51    outline_glyphs: OutlineGlyphCollection<'a>,
52    outline_cache: &'a mut OutlineCacheSession<'b>,
53    clip_outline: OutlinePath,
54    context_color: AlphaColor<Srgb>,
55    painter: &'a mut dyn DrawSink,
56    stack: Vec<ColrStackEntry>,
57    skip_blend_layers: bool,
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61enum ColrStackEntry {
62    ClipPath,
63    BlendLayer,
64}
65
66impl Debug for ColrPainter<'_, '_> {
67    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
68        f.debug_struct("ColrPainter()").finish()
69    }
70}
71
72impl<'a, 'b> ColrPainter<'a, 'b> {
73    /// Create a new COLR painter.
74    pub(crate) fn new(
75        colr_glyph: &'a GlyphColr<'a>,
76        context_color: AlphaColor<Srgb>,
77        painter: &'a mut dyn DrawSink,
78        outline_cache: &'a mut OutlineCacheSession<'b>,
79    ) -> Self {
80        Self {
81            transforms: vec![colr_glyph.draw_transform],
82            colr_glyph,
83            outline_glyphs: colr_glyph.font_ref.outline_glyphs(),
84            outline_cache,
85            clip_outline: OutlinePath::new(),
86            context_color,
87            painter,
88            stack: Vec::new(),
89            // In case the emoji doesn't use non-default blending, we can ignore layers
90            // completely and use src-over compositing throughout.
91            skip_blend_layers: !colr_glyph.has_non_default_blend,
92        }
93    }
94
95    /// Paint the underlying glyph.
96    pub(crate) fn paint(&mut self) {
97        let skrifa_glyph = self.colr_glyph.skrifa_glyph.clone();
98        let location_ref = self.colr_glyph.location;
99        // Ignore errors for now.
100        let _ = skrifa_glyph.paint(location_ref, self);
101
102        // In certain malformed fonts (i.e. if there is a cycle), skrifa will not
103        // ensure that the push/pop count is the same, so we pop the remaining ones here.
104        while let Some(entry) = self.stack.pop() {
105            match entry {
106                ColrStackEntry::ClipPath => self.painter.pop_clip_path(),
107                ColrStackEntry::BlendLayer => self.painter.pop_layer(),
108            }
109        }
110    }
111
112    fn cur_transform(&self) -> Affine {
113        self.transforms.last().copied().unwrap_or_default()
114    }
115
116    fn get_outline(&mut self, glyph_id: GlyphId) -> Option<CachedOutline<'_>> {
117        let outline_glyph = self.outline_glyphs.get(glyph_id)?;
118
119        Some(self.outline_cache.get_or_insert(
120            glyph_id.to_u32(),
121            self.colr_glyph.font_info,
122            self.colr_glyph.font_info.upem,
123            FontEmbolden::default(),
124            VarLookupKey::new(self.colr_glyph.location.coords()),
125            &outline_glyph,
126            None,
127        ))
128    }
129
130    fn palette_index_to_color(&self, palette_index: u16, alpha: f32) -> Option<AlphaColor<Srgb>> {
131        if palette_index != u16::MAX {
132            let color = self
133                .colr_glyph
134                .font_ref
135                .cpal()
136                .ok()?
137                .color_records_array()?
138                .ok()?[palette_index as usize];
139
140            Some(
141                AlphaColor::from_rgba8(color.red, color.green, color.blue, color.alpha)
142                    .multiply_alpha(alpha),
143            )
144        } else {
145            Some(self.context_color.multiply_alpha(alpha))
146        }
147    }
148
149    fn convert_stops(&self, stops: &[ColorStop]) -> ColorStops {
150        let mut stops = stops
151            .iter()
152            .map(|s| {
153                let color = self
154                    .palette_index_to_color(s.palette_index, s.alpha)
155                    .unwrap_or(AlphaColor::BLACK);
156
157                peniko::ColorStop {
158                    offset: s.offset,
159                    color: DynamicColor::from_alpha_color(color),
160                }
161            })
162            .collect::<SmallVec<[peniko::ColorStop; 4]>>();
163
164        // Pad stops if necessary, since vello requires offsets
165        // to start at 0.0 and end at 1.0.
166        let first_stop = stops[0];
167        let last_stop = *stops.last().unwrap();
168
169        if first_stop.offset != 0.0 {
170            let mut new_stop = first_stop;
171            new_stop.offset = 0.0;
172            stops.insert(0, new_stop);
173        }
174
175        if last_stop.offset != 1.0 {
176            let mut new_stop = last_stop;
177            new_stop.offset = 1.0;
178            stops.push(new_stop);
179        }
180
181        // The COLR spec has the very specific requirement that if there are multiple stops with the
182        // offset 1.0, only the last one should be used. We abstract this away by removing all such
183        // superfluous stops.
184        while let Some(stop) = stops.get(stops.len() - 2).map(|s| s.offset) {
185            if (stop - 1.0).is_nearly_zero() {
186                stops.remove(stops.len() - 2);
187            } else {
188                break;
189            }
190        }
191
192        ColorStops(stops)
193    }
194
195    fn push_clip(&mut self) {
196        self.painter.push_clip_path(&self.clip_outline.path);
197        self.stack.push(ColrStackEntry::ClipPath);
198    }
199
200    fn pop_stack_entry(&mut self, expected: ColrStackEntry) -> bool {
201        // This should only be false for malformed fonts. Assuming that our
202        // implementation is correct, this shouldn't ever be reached for valid fonts.
203        #[cfg(test)]
204        assert_eq!(
205            self.stack.last().copied(),
206            Some(expected),
207            "assertion should always be true for valid fonts"
208        );
209
210        if self.stack.last().copied() == Some(expected) {
211            self.stack.pop();
212
213            true
214        } else {
215            false
216        }
217    }
218}
219
220pub(crate) struct ColrGlyphInfo {
221    /// A conservative bounding box of the glyph.
222    ///
223    /// Is `None` in case the glyph is empty (i.e. doesn't contain any drawable content).
224    pub(crate) bbox: Option<Rect>,
225    /// Whether the glyph uses any non-default blending.
226    pub(crate) has_non_default_blend: bool,
227}
228
229pub(crate) fn get_colr_info<'a, 'b>(
230    font_ref: &'a FontRef<'a>,
231    color_glyph: &skrifa::color::ColorGlyph<'a>,
232    location: LocationRef<'a>,
233    outline_cache: &'a mut OutlineCacheSession<'b>,
234    font_info: FontInfo,
235) -> ColrGlyphInfo {
236    let mut extractor = GlyphInfoExtractor::new(font_ref, location, outline_cache, font_info);
237    let _ = color_glyph.paint(location, &mut extractor);
238    extractor.finish()
239}
240
241struct GlyphInfoExtractor<'a, 'b> {
242    transforms: Vec<Affine>,
243    clip_stack: Vec<Rect>,
244    coarse_bbox: Option<Rect>,
245    has_non_default_blend: bool,
246    outline_glyphs: OutlineGlyphCollection<'a>,
247    outline_cache: &'a mut OutlineCacheSession<'b>,
248    location: LocationRef<'a>,
249    font_info: FontInfo,
250}
251
252impl<'a, 'b> GlyphInfoExtractor<'a, 'b> {
253    fn new(
254        font_ref: &'a FontRef<'a>,
255        location: LocationRef<'a>,
256        outline_cache: &'a mut OutlineCacheSession<'b>,
257        font_info: FontInfo,
258    ) -> Self {
259        Self {
260            transforms: vec![Affine::IDENTITY],
261            clip_stack: Vec::new(),
262            coarse_bbox: None,
263            has_non_default_blend: false,
264            outline_glyphs: font_ref.outline_glyphs(),
265            outline_cache,
266            location,
267            font_info,
268        }
269    }
270
271    fn cur_transform(&self) -> Affine {
272        self.transforms.last().copied().unwrap_or_default()
273    }
274
275    fn push_clip_bbox(&mut self, clip_bbox: Rect) {
276        let active = self
277            .clip_stack
278            .last()
279            .copied()
280            .map_or(clip_bbox, |parent| parent.intersect(clip_bbox));
281        self.coarse_bbox = Some(
282            self.coarse_bbox
283                .map_or(active, |coarse_bbox| coarse_bbox.union(active)),
284        );
285        self.clip_stack.push(active);
286    }
287
288    fn transform_rect(&self, rect: Rect) -> Rect {
289        self.cur_transform().transform_rect_bbox(rect)
290    }
291
292    fn get_outline(&mut self, glyph_id: GlyphId) -> Option<CachedOutline<'_>> {
293        let outline_glyph = self.outline_glyphs.get(glyph_id)?;
294
295        Some(self.outline_cache.get_or_insert(
296            glyph_id.to_u32(),
297            self.font_info,
298            self.font_info.upem,
299            FontEmbolden::default(),
300            VarLookupKey::new(self.location.coords()),
301            &outline_glyph,
302            None,
303        ))
304    }
305
306    fn finish(self) -> ColrGlyphInfo {
307        ColrGlyphInfo {
308            bbox: self.coarse_bbox,
309            has_non_default_blend: self.has_non_default_blend,
310        }
311    }
312}
313
314impl ColorPainter for ColrPainter<'_, '_> {
315    fn push_transform(&mut self, t: Transform) {
316        self.transforms
317            .push(self.cur_transform() * convert_affine(t));
318    }
319
320    fn pop_transform(&mut self) {
321        self.transforms.pop();
322    }
323
324    fn push_clip_glyph(&mut self, glyph_id: GlyphId) {
325        let outline = {
326            let Some(outline) = self.get_outline(glyph_id) else {
327                return;
328            };
329            Arc::clone(outline.path)
330        };
331
332        self.clip_outline.reuse();
333        // TODO: We can make use of `DrawSink::set_transform` to let the client
334        // take care of applying it, so we can avoid copying the path.
335        self.clip_outline.path.extend(outline.iter());
336        self.clip_outline.path.apply_affine(self.cur_transform());
337        self.push_clip();
338    }
339
340    fn push_clip_box(&mut self, clip_box: BoundingBox<f32>) {
341        let rect = Rect::new(
342            f64::from(clip_box.x_min),
343            f64::from(clip_box.y_min),
344            f64::from(clip_box.x_max),
345            f64::from(clip_box.y_max),
346        );
347        let transformed = self.cur_transform().transform_rect_bbox(rect);
348        self.clip_outline.reuse();
349        // Note that the bbox will become stale, but we don't need it anyway here.
350        self.clip_outline
351            .path
352            .extend(transformed.path_elements(0.1));
353        self.push_clip();
354    }
355
356    fn pop_clip(&mut self) {
357        if self.pop_stack_entry(ColrStackEntry::ClipPath) {
358            self.painter.pop_clip_path();
359        }
360    }
361
362    fn fill(&mut self, brush: Brush<'_>) {
363        // Ceil so that we don't apply unnecessary anti-aliasing in case the
364        // glyph area is at a sub-pixel position.
365        let fill_rect = &self.colr_glyph.area.ceil();
366
367        match brush {
368            Brush::Solid {
369                palette_index,
370                alpha,
371            } => {
372                let color = self
373                    .palette_index_to_color(palette_index, alpha)
374                    .unwrap_or(AlphaColor::BLACK);
375
376                self.painter.fill_solid(fill_rect, color);
377            }
378            Brush::LinearGradient {
379                p0,
380                p1,
381                color_stops,
382                extend,
383            } => {
384                let p0 = convert_point(p0);
385                let p1 = convert_point(p1);
386                let extend = convert_extend(extend);
387                let stops = self.convert_stops(color_stops);
388
389                if stops.len() == 1 {
390                    self.painter
391                        .fill_solid(fill_rect, stops[0].color.to_alpha_color());
392                } else {
393                    let grad = Gradient {
394                        kind: LinearGradientPosition { start: p0, end: p1 }.into(),
395                        stops,
396                        extend,
397                        ..Default::default()
398                    };
399                    self.painter.set_paint_transform(self.cur_transform());
400                    self.painter.fill_gradient(fill_rect, grad);
401                }
402            }
403            Brush::RadialGradient {
404                c0,
405                r0,
406                c1,
407                r1,
408                color_stops,
409                extend,
410            } => {
411                // TODO: Radial gradients with negative r0.
412
413                let p0 = convert_point(c0);
414                let p1 = convert_point(c1);
415                let extend = convert_extend(extend);
416                let stops = self.convert_stops(color_stops);
417
418                if r1 <= 0.0 || stops.len() == 1 {
419                    self.painter
420                        .fill_solid(fill_rect, stops[0].color.to_alpha_color());
421
422                    return;
423                }
424
425                let grad = Gradient {
426                    kind: RadialGradientPosition {
427                        start_center: p0,
428                        start_radius: r0,
429                        end_center: p1,
430                        end_radius: r1,
431                    }
432                    .into(),
433                    stops,
434                    extend,
435                    ..Default::default()
436                };
437
438                self.painter.set_paint_transform(self.cur_transform());
439                self.painter.fill_gradient(fill_rect, grad);
440            }
441            Brush::SweepGradient {
442                c0,
443                start_angle,
444                mut end_angle,
445                color_stops,
446                extend,
447            } => {
448                let p0 = convert_point(c0);
449                let extend = convert_extend(extend);
450                let stops = self.convert_stops(color_stops);
451
452                if stops.len() == 1 {
453                    self.painter
454                        .fill_solid(fill_rect, stops[0].color.to_alpha_color());
455
456                    return;
457                }
458
459                if start_angle == end_angle {
460                    match extend {
461                        Extend::Pad => {
462                            // Vello doesn't accept sweep gradient with same start and end
463                            // angle, so add an artificial, small offset.
464                            end_angle += 0.01;
465                        }
466                        _ => {
467                            // Cannot be reached,
468                            // see https://github.com/googlefonts/fontations/issues/1017.
469                            unreachable!()
470                        }
471                    }
472                }
473
474                // We need to invert the direction of the gradient to bridge the gap between
475                // peniko and COLR.
476                let grad = Gradient {
477                    kind: SweepGradientPosition {
478                        center: Point::new(p0.x, -p0.y),
479                        start_angle: start_angle.to_radians(),
480                        end_angle: end_angle.to_radians(),
481                    }
482                    .into(),
483                    stops,
484                    extend,
485                    ..Default::default()
486                };
487
488                let paint_transform = self.cur_transform() * Affine::scale_non_uniform(1.0, -1.0);
489
490                self.painter.set_paint_transform(paint_transform);
491                self.painter.fill_gradient(fill_rect, grad);
492            }
493        };
494    }
495
496    fn push_layer(&mut self, composite_mode: CompositeMode) {
497        let blend_mode = convert_composite_mode(composite_mode);
498
499        if !self.skip_blend_layers {
500            self.painter.push_blend_layer(blend_mode);
501            self.stack.push(ColrStackEntry::BlendLayer);
502        }
503    }
504
505    fn pop_layer(&mut self) {
506        if !self.skip_blend_layers && self.pop_stack_entry(ColrStackEntry::BlendLayer) {
507            self.painter.pop_layer();
508        }
509    }
510}
511
512impl ColorPainter for GlyphInfoExtractor<'_, '_> {
513    fn push_transform(&mut self, t: Transform) {
514        self.transforms
515            .push(self.cur_transform() * convert_affine(t));
516    }
517
518    fn pop_transform(&mut self) {
519        self.transforms.pop();
520    }
521
522    fn push_clip_glyph(&mut self, glyph_id: GlyphId) {
523        let outline_bbox = {
524            let Some(outline) = self.get_outline(glyph_id) else {
525                return;
526            };
527            outline.bbox
528        };
529
530        self.push_clip_bbox(self.transform_rect(outline_bbox));
531    }
532
533    fn push_clip_box(&mut self, clip_box: BoundingBox<f32>) {
534        self.push_clip_bbox(self.transform_rect(convert_bounding_box(clip_box)));
535    }
536
537    fn pop_clip(&mut self) {
538        self.clip_stack.pop();
539    }
540
541    fn fill(&mut self, _brush: Brush<'_>) {}
542
543    fn push_layer(&mut self, composite_mode: CompositeMode) {
544        self.has_non_default_blend |=
545            convert_composite_mode(composite_mode) != BlendMode::default();
546    }
547
548    fn pop_layer(&mut self) {}
549}
550
551fn convert_composite_mode(composite_mode: CompositeMode) -> BlendMode {
552    match composite_mode {
553        CompositeMode::Clear => BlendMode::new(Mix::Normal, Compose::Clear),
554        CompositeMode::Src => BlendMode::new(Mix::Normal, Compose::Copy),
555        CompositeMode::Dest => BlendMode::new(Mix::Normal, Compose::Dest),
556        CompositeMode::SrcOver => BlendMode::new(Mix::Normal, Compose::SrcOver),
557        CompositeMode::DestOver => BlendMode::new(Mix::Normal, Compose::DestOver),
558        CompositeMode::SrcIn => BlendMode::new(Mix::Normal, Compose::SrcIn),
559        CompositeMode::DestIn => BlendMode::new(Mix::Normal, Compose::DestIn),
560        CompositeMode::SrcOut => BlendMode::new(Mix::Normal, Compose::SrcOut),
561        CompositeMode::DestOut => BlendMode::new(Mix::Normal, Compose::DestOut),
562        CompositeMode::SrcAtop => BlendMode::new(Mix::Normal, Compose::SrcAtop),
563        CompositeMode::DestAtop => BlendMode::new(Mix::Normal, Compose::DestAtop),
564        CompositeMode::Xor => BlendMode::new(Mix::Normal, Compose::Xor),
565        CompositeMode::Plus => BlendMode::new(Mix::Normal, Compose::Plus),
566        CompositeMode::Screen => BlendMode::new(Mix::Screen, Compose::SrcOver),
567        CompositeMode::Overlay => BlendMode::new(Mix::Overlay, Compose::SrcOver),
568        CompositeMode::Darken => BlendMode::new(Mix::Darken, Compose::SrcOver),
569        CompositeMode::Lighten => BlendMode::new(Mix::Lighten, Compose::SrcOver),
570        CompositeMode::ColorDodge => BlendMode::new(Mix::ColorDodge, Compose::SrcOver),
571        CompositeMode::ColorBurn => BlendMode::new(Mix::ColorBurn, Compose::SrcOver),
572        CompositeMode::HardLight => BlendMode::new(Mix::HardLight, Compose::SrcOver),
573        CompositeMode::SoftLight => BlendMode::new(Mix::SoftLight, Compose::SrcOver),
574        CompositeMode::Difference => BlendMode::new(Mix::Difference, Compose::SrcOver),
575        CompositeMode::Exclusion => BlendMode::new(Mix::Exclusion, Compose::SrcOver),
576        CompositeMode::Multiply => BlendMode::new(Mix::Multiply, Compose::SrcOver),
577        CompositeMode::HslHue => BlendMode::new(Mix::Hue, Compose::SrcOver),
578        CompositeMode::HslSaturation => BlendMode::new(Mix::Saturation, Compose::SrcOver),
579        CompositeMode::HslColor => BlendMode::new(Mix::Color, Compose::SrcOver),
580        CompositeMode::HslLuminosity => BlendMode::new(Mix::Luminosity, Compose::SrcOver),
581        CompositeMode::Unknown => BlendMode::default(),
582    }
583}
584
585fn convert_affine(transform: Transform) -> Affine {
586    Affine::new([
587        f64::from(transform.xx),
588        f64::from(transform.yx),
589        f64::from(transform.xy),
590        f64::from(transform.yy),
591        f64::from(transform.dx),
592        f64::from(transform.dy),
593    ])
594}
595
596fn convert_extend(extend: skrifa::color::Extend) -> Extend {
597    match extend {
598        skrifa::color::Extend::Pad => Extend::Pad,
599        skrifa::color::Extend::Repeat => Extend::Repeat,
600        skrifa::color::Extend::Reflect => Extend::Reflect,
601        skrifa::color::Extend::Unknown => Extend::Pad,
602    }
603}
604
605fn convert_point(point: skrifa::raw::types::Point<f32>) -> Point {
606    Point::new(f64::from(point.x), f64::from(point.y))
607}
608
609pub(crate) fn convert_bounding_box(rect: BoundingBox<f32>) -> Rect {
610    Rect::new(
611        f64::from(rect.x_min),
612        f64::from(rect.y_min),
613        f64::from(rect.x_max),
614        f64::from(rect.y_max),
615    )
616}