skrifa/color/
mod.rs

1//! Drawing color glyphs.
2//!
3//! # Examples
4//! ## Retrieve the clip box of a COLRv1 glyph if it has one:
5//!
6//! ```
7//! # use core::result::Result;
8//! # use skrifa::{instance::{Size, Location}, color::{ColorGlyphFormat, ColorPainter, PaintError}, GlyphId, MetadataProvider};
9//! # fn get_colr_bb(font: read_fonts::FontRef, color_painter_impl : &mut impl ColorPainter, glyph_id : GlyphId, size: Size) -> Result<(), PaintError> {
10//! match font.color_glyphs()
11//!       .get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
12//!       .expect("Glyph not found.")
13//!       .bounding_box(&Location::default(), size)
14//! {
15//!   Some(bounding_box) => {
16//!       println!("Bounding box is {:?}", bounding_box);
17//!   }
18//!   None => {
19//!       println!("Glyph has no clip box.");
20//!   }
21//! }
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! ## Paint a COLRv1 glyph given a font, and a glyph id and a [`ColorPainter`] implementation:
27//! ```
28//! # use core::result::Result;
29//! # use skrifa::{instance::{Size, Location}, color::{ColorGlyphFormat, ColorPainter, PaintError}, GlyphId, MetadataProvider};
30//! # fn paint_colr(font: read_fonts::FontRef, color_painter_impl : &mut impl ColorPainter, glyph_id : GlyphId) -> Result<(), PaintError> {
31//! let color_glyph = font.color_glyphs()
32//!                     .get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
33//!                     .expect("Glyph not found");
34//! color_glyph.paint(&Location::default(), color_painter_impl)
35//! # }
36//! ```
37//!
38mod instance;
39mod transform;
40mod traversal;
41
42#[cfg(test)]
43mod traversal_tests;
44
45use raw::tables::colr;
46#[cfg(test)]
47use serde::{Deserialize, Serialize};
48
49pub use read_fonts::tables::colr::{CompositeMode, Extend};
50
51use read_fonts::{
52    types::{BoundingBox, GlyphId, Point},
53    ReadError, TableProvider,
54};
55
56use std::{fmt::Debug, ops::Range};
57
58use traversal::{get_clipbox_font_units, traverse_v0_range, traverse_with_callbacks, VisitedSet};
59
60pub use transform::Transform;
61
62use crate::prelude::{LocationRef, Size};
63
64use self::instance::{resolve_paint, PaintId};
65
66/// An error during drawing a COLR glyph.
67///
68/// This covers inconsistencies in the COLRv1 paint graph as well as downstream
69/// parse errors from read-fonts.
70#[derive(Debug, Clone)]
71pub enum PaintError {
72    ParseError(ReadError),
73    GlyphNotFound(GlyphId),
74    PaintCycleDetected,
75    DepthLimitExceeded,
76}
77
78impl std::fmt::Display for PaintError {
79    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80        match self {
81            PaintError::ParseError(read_error) => {
82                write!(f, "Error parsing font data: {read_error}")
83            }
84            PaintError::GlyphNotFound(glyph_id) => {
85                write!(f, "No COLRv1 glyph found for glyph id: {glyph_id}")
86            }
87            PaintError::PaintCycleDetected => write!(f, "Paint cycle detected in COLRv1 glyph."),
88            PaintError::DepthLimitExceeded => write!(f, "Depth limit exceeded in COLRv1 glyph."),
89        }
90    }
91}
92
93impl From<ReadError> for PaintError {
94    fn from(value: ReadError) -> Self {
95        PaintError::ParseError(value)
96    }
97}
98
99/// A color stop of a gradient.
100///
101/// All gradient callbacks of [`ColorPainter`] normalize color stops to be in the range of 0
102/// to 1.
103#[derive(Clone, PartialEq, Debug, Default)]
104#[cfg_attr(test, derive(Serialize, Deserialize))]
105// This repr(C) is required so that C-side FFI's
106// are able to cast the ColorStop slice to a C-side array pointer.
107#[repr(C)]
108pub struct ColorStop {
109    pub offset: f32,
110    /// Specifies a color from the `CPAL` table.
111    pub palette_index: u16,
112    /// Additional alpha value, to be multiplied with the color above before use.
113    pub alpha: f32,
114}
115
116// Design considerations for choosing a slice of ColorStops as `color_stop`
117// type: In principle, a local `Vec<ColorStop>` allocation would not required if
118// we're willing to walk the `ResolvedColorStop` iterator to find the minimum
119// and maximum color stops.  Then we could scale the color stops based on the
120// minimum and maximum. But performing the min/max search would require
121// re-applying the deltas at least once, after which we would pass the scaled
122// stops to client side and have the client sort the collected items once
123// again. If we do want to pre-ort them, and still use use an
124// `Iterator<Item=ColorStop>` instead as the `color_stops` field, then we would
125// need a Fontations-side allocations to sort, and an extra allocation on the
126// client side to `.collect()` from the provided iterator before passing it to
127// drawing API.
128//
129/// A fill type of a COLRv1 glyph (solid fill or various gradient types).
130///
131/// The client receives the information about the fill type in the
132/// [`fill`](ColorPainter::fill) callback of the [`ColorPainter`] trait.
133#[derive(Debug, PartialEq)]
134pub enum Brush<'a> {
135    /// A solid fill with the color specified by `palette_index`. The respective
136    /// color from the CPAL table then needs to be multiplied with `alpha`.
137    Solid { palette_index: u16, alpha: f32 },
138    /// A linear gradient, normalized from the P0, P1 and P2 representation in
139    /// the COLRv1 table to a linear gradient between two points `p0` and
140    /// `p1`. If there is only one color stop, the client should draw a solid
141    /// fill with that color. The `color_stops` are normalized to the range from
142    /// 0 to 1.
143    LinearGradient {
144        p0: Point<f32>,
145        p1: Point<f32>,
146        color_stops: &'a [ColorStop],
147        extend: Extend,
148    },
149    /// A radial gradient, with color stops normalized to the range of 0 to 1.
150    /// Caution: This normalization can mean that negative radii occur. It is
151    /// the client's responsibility to truncate the color line at the 0
152    /// position, interpolating between `r0` and `r1` and compute an
153    /// interpolated color at that position.
154    RadialGradient {
155        c0: Point<f32>,
156        r0: f32,
157        c1: Point<f32>,
158        r1: f32,
159        color_stops: &'a [ColorStop],
160        extend: Extend,
161    },
162    /// A sweep gradient, also called conical gradient. The color stops are
163    /// normalized to the range from 0 to 1 and the returned angles are to be
164    /// interpreted in _clockwise_ direction (swapped from the meaning in the
165    /// font file).  The stop normalization may mean that the angles may be
166    /// larger or smaller than the range of 0 to 360. Note that only the range
167    /// from 0 to 360 degrees is to be drawn, see
168    /// <https://learn.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients>.
169    SweepGradient {
170        c0: Point<f32>,
171        start_angle: f32,
172        end_angle: f32,
173        color_stops: &'a [ColorStop],
174        extend: Extend,
175    },
176}
177
178/// Signals success of request to draw a COLRv1 sub glyph from cache.
179///
180/// Result of [`paint_cached_color_glyph`](ColorPainter::paint_cached_color_glyph)
181/// through which the client signals whether a COLRv1 glyph referenced by
182/// another COLRv1 glyph was drawn from cache or whether the glyph's subgraph
183/// should be traversed by the skria side COLRv1 implementation.
184pub enum PaintCachedColorGlyph {
185    /// The specified COLRv1 glyph has been successfully painted client side.
186    Ok,
187    /// The client does not implement drawing COLRv1 glyphs from cache and the
188    /// Fontations side COLRv1 implementation is asked to traverse the
189    /// respective PaintColorGlyph sub graph.
190    Unimplemented,
191}
192
193/// A group of required painting callbacks to be provided by the client.
194///
195/// Each callback is executing a particular drawing or canvas transformation
196/// operation. The trait's callback functions are invoked when
197/// [`paint`](ColorGlyph::paint) is called with a [`ColorPainter`] trait
198/// object. The documentation for each function describes what actions are to be
199/// executed using the client side 2D graphics API, usually by performing some
200/// kind of canvas operation.
201pub trait ColorPainter {
202    /// Push the specified transform by concatenating it to the current
203    /// transformation matrix.
204    fn push_transform(&mut self, transform: Transform);
205
206    /// Restore the transformation matrix to the state before the previous
207    /// [`push_transform`](ColorPainter::push_transform) call.
208    fn pop_transform(&mut self);
209
210    /// Apply a clip path in the shape of glyph specified by `glyph_id`.
211    fn push_clip_glyph(&mut self, glyph_id: GlyphId);
212
213    /// Apply a clip rectangle specified by `clip_rect`.
214    fn push_clip_box(&mut self, clip_box: BoundingBox<f32>);
215
216    /// Restore the clip state to the state before a previous
217    /// [`push_clip_glyph`](ColorPainter::push_clip_glyph) or
218    /// [`push_clip_box`](ColorPainter::push_clip_box) call.
219    fn pop_clip(&mut self);
220
221    /// Fill the current clip area with the specified gradient fill.
222    fn fill(&mut self, brush: Brush<'_>);
223
224    /// Combined clip and fill operation.
225    ///
226    /// Apply the clip path determined by the specified `glyph_id`, then fill it
227    /// with the specified [`brush`](Brush), applying the `_brush_transform`
228    /// transformation matrix to the brush. The default implementation works
229    /// based on existing methods in this trait. It is recommended for clients
230    /// to override the default implementaition with a custom combined clip and
231    /// fill operation. In this way overriding likely results in performance
232    /// gains depending on performance characteristics of the 2D graphics stack
233    /// that these calls are mapped to.
234    fn fill_glyph(
235        &mut self,
236        glyph_id: GlyphId,
237        brush_transform: Option<Transform>,
238        brush: Brush<'_>,
239    ) {
240        self.push_clip_glyph(glyph_id);
241        if let Some(wrap_in_transform) = brush_transform {
242            self.push_transform(wrap_in_transform);
243            self.fill(brush);
244            self.pop_transform();
245        } else {
246            self.fill(brush);
247        }
248        self.pop_clip();
249    }
250
251    /// Optionally implement this method: Draw an unscaled COLRv1 glyph given
252    /// the current transformation matrix (as accumulated by
253    /// [`push_transform`](ColorPainter::push_transform) calls).
254    fn paint_cached_color_glyph(
255        &mut self,
256        _glyph: GlyphId,
257    ) -> Result<PaintCachedColorGlyph, PaintError> {
258        Ok(PaintCachedColorGlyph::Unimplemented)
259    }
260
261    /// Open a new layer, and merge the layer down using `composite_mode` when
262    /// [`pop_layer`](ColorPainter::pop_layer) is called, signalling that this layer is done drawing.
263    fn push_layer(&mut self, composite_mode: CompositeMode);
264    fn pop_layer(&mut self);
265}
266
267/// Distinguishes available color glyph formats.
268#[derive(Clone, Copy)]
269pub enum ColorGlyphFormat {
270    ColrV0,
271    ColrV1,
272}
273
274/// A representation of a color glyph that can be painted through a sequence of [`ColorPainter`] callbacks.
275#[derive(Clone)]
276pub struct ColorGlyph<'a> {
277    colr: colr::Colr<'a>,
278    root_paint_ref: ColorGlyphRoot<'a>,
279}
280
281#[derive(Clone)]
282enum ColorGlyphRoot<'a> {
283    V0Range(Range<usize>),
284    V1Paint(colr::Paint<'a>, PaintId, GlyphId, Result<u16, ReadError>),
285}
286
287impl<'a> ColorGlyph<'a> {
288    /// Returns the version of the color table from which this outline was
289    /// selected.
290    pub fn format(&self) -> ColorGlyphFormat {
291        match &self.root_paint_ref {
292            ColorGlyphRoot::V0Range(_) => ColorGlyphFormat::ColrV0,
293            ColorGlyphRoot::V1Paint(..) => ColorGlyphFormat::ColrV1,
294        }
295    }
296
297    /// Returns the bounding box.
298    ///
299    /// For COLRv1 glyphs, this is the clip box of the specified COLRv1 glyph,
300    /// or `None` if clip boxes are not present or if there is none for the
301    /// particular glyph.
302    ///
303    /// Always returns `None` for COLRv0 glyphs because precomputed clip boxes
304    /// are never available.
305    ///
306    /// The `size` argument can optionally be used to scale the bounding box
307    /// to a particular font size and `location` allows specifying a variation
308    /// instance.
309    pub fn bounding_box(
310        &self,
311        location: impl Into<LocationRef<'a>>,
312        size: Size,
313    ) -> Option<BoundingBox<f32>> {
314        match &self.root_paint_ref {
315            ColorGlyphRoot::V1Paint(_paint, _paint_id, glyph_id, upem) => {
316                let instance =
317                    instance::ColrInstance::new(self.colr.clone(), location.into().coords());
318                let resolved_bounding_box = get_clipbox_font_units(&instance, *glyph_id);
319                resolved_bounding_box.map(|bounding_box| {
320                    let scale_factor = size.linear_scale((*upem).clone().unwrap_or(0));
321                    bounding_box.scale(scale_factor)
322                })
323            }
324            _ => None,
325        }
326    }
327
328    /// Evaluates the paint graph at the specified location in variation space
329    /// and emits the results to the given painter.
330    ///
331    ///
332    /// For a COLRv1 glyph, traverses the COLRv1 paint graph and invokes drawing callbacks on a
333    /// specified [`ColorPainter`] trait object.  The traversal operates in font
334    /// units and will call `ColorPainter` methods with font unit values. This
335    /// means, if you want to draw a COLRv1 glyph at a particular font size, the
336    /// canvas needs to have a transformation matrix applied so that it scales down
337    /// the drawing operations to `font_size / upem`.
338    ///
339    /// # Arguments
340    ///
341    /// * `glyph_id` the `GlyphId` to be drawn.
342    /// * `location` coordinates for specifying a variation instance. This can be empty.
343    /// * `painter` a client-provided [`ColorPainter`] implementation receiving drawing callbacks.
344    ///
345    pub fn paint(
346        &self,
347        location: impl Into<LocationRef<'a>>,
348        painter: &mut impl ColorPainter,
349    ) -> Result<(), PaintError> {
350        let instance = instance::ColrInstance::new(self.colr.clone(), location.into().coords());
351        match &self.root_paint_ref {
352            ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, _) => {
353                let clipbox = get_clipbox_font_units(&instance, *glyph_id);
354
355                if let Some(rect) = clipbox {
356                    painter.push_clip_box(rect);
357                }
358
359                let mut visited_set = VisitedSet::default();
360                visited_set.insert(*paint_id);
361                traverse_with_callbacks(
362                    &resolve_paint(&instance, paint)?,
363                    &instance,
364                    painter,
365                    &mut visited_set,
366                    0,
367                )?;
368
369                if clipbox.is_some() {
370                    painter.pop_clip();
371                }
372                Ok(())
373            }
374            ColorGlyphRoot::V0Range(range) => {
375                traverse_v0_range(range, &instance, painter)?;
376                Ok(())
377            }
378        }
379    }
380}
381
382/// Collection of color glyphs.
383#[derive(Clone)]
384pub struct ColorGlyphCollection<'a> {
385    colr: Option<colr::Colr<'a>>,
386    upem: Result<u16, ReadError>,
387}
388
389impl<'a> ColorGlyphCollection<'a> {
390    /// Creates a new collection of paintable color glyphs for the given font.
391    pub fn new(font: &impl TableProvider<'a>) -> Self {
392        let colr = font.colr().ok();
393        let upem = font.head().map(|h| h.units_per_em());
394
395        Self { colr, upem }
396    }
397
398    /// Returns the color glyph representation for the given glyph identifier,
399    /// given a specific format.
400    pub fn get_with_format(
401        &self,
402        glyph_id: GlyphId,
403        glyph_format: ColorGlyphFormat,
404    ) -> Option<ColorGlyph<'a>> {
405        let colr = self.colr.clone()?;
406
407        let root_paint_ref = match glyph_format {
408            ColorGlyphFormat::ColrV0 => {
409                let layer_range = colr.v0_base_glyph(glyph_id).ok()??;
410                ColorGlyphRoot::V0Range(layer_range)
411            }
412            ColorGlyphFormat::ColrV1 => {
413                let (paint, paint_id) = colr.v1_base_glyph(glyph_id).ok()??;
414                ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, self.upem.clone())
415            }
416        };
417        Some(ColorGlyph {
418            colr,
419            root_paint_ref,
420        })
421    }
422
423    /// Returns a color glyph representation for the given glyph identifier if
424    /// available, preferring a COLRv1 representation over a COLRv0
425    /// representation.
426    pub fn get(&self, glyph_id: GlyphId) -> Option<ColorGlyph<'a>> {
427        self.get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
428            .or_else(|| self.get_with_format(glyph_id, ColorGlyphFormat::ColrV0))
429    }
430}
431
432#[cfg(test)]
433mod tests {
434
435    use crate::{
436        color::traversal_tests::test_glyph_defs::PAINTCOLRGLYPH_CYCLE,
437        prelude::{LocationRef, Size},
438        MetadataProvider,
439    };
440
441    use read_fonts::{types::BoundingBox, FontRef};
442
443    use super::{Brush, ColorPainter, CompositeMode, GlyphId, Transform};
444    use crate::color::traversal_tests::test_glyph_defs::{COLORED_CIRCLES_V0, COLORED_CIRCLES_V1};
445
446    #[test]
447    fn has_colrv1_glyph_test() {
448        let colr_font = font_test_data::COLRV0V1_VARIABLE;
449        let font = FontRef::new(colr_font).unwrap();
450        let get_colrv1_glyph = |codepoint: &[char]| {
451            font.charmap().map(codepoint[0]).and_then(|glyph_id| {
452                font.color_glyphs()
453                    .get_with_format(glyph_id, crate::color::ColorGlyphFormat::ColrV1)
454            })
455        };
456
457        assert!(get_colrv1_glyph(COLORED_CIRCLES_V0).is_none());
458        assert!(get_colrv1_glyph(COLORED_CIRCLES_V1).is_some());
459    }
460    struct DummyColorPainter {}
461
462    impl DummyColorPainter {
463        pub fn new() -> Self {
464            Self {}
465        }
466    }
467
468    impl Default for DummyColorPainter {
469        fn default() -> Self {
470            Self::new()
471        }
472    }
473
474    impl ColorPainter for DummyColorPainter {
475        fn push_transform(&mut self, _transform: Transform) {}
476        fn pop_transform(&mut self) {}
477        fn push_clip_glyph(&mut self, _glyph: GlyphId) {}
478        fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {}
479        fn pop_clip(&mut self) {}
480        fn fill(&mut self, _brush: Brush) {}
481        fn push_layer(&mut self, _composite_mode: CompositeMode) {}
482        fn pop_layer(&mut self) {}
483    }
484
485    #[test]
486    fn paintcolrglyph_cycle_test() {
487        let colr_font = font_test_data::COLRV0V1_VARIABLE;
488        let font = FontRef::new(colr_font).unwrap();
489        let cycle_glyph_id = font.charmap().map(PAINTCOLRGLYPH_CYCLE[0]).unwrap();
490        let colrv1_glyph = font
491            .color_glyphs()
492            .get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
493
494        assert!(colrv1_glyph.is_some());
495        let mut color_painter = DummyColorPainter::new();
496
497        let result = colrv1_glyph
498            .unwrap()
499            .paint(LocationRef::default(), &mut color_painter);
500        // Expected to fail with an error as the glyph contains a paint cycle.
501        assert!(result.is_err());
502    }
503
504    #[test]
505    fn no_cliplist_test() {
506        let colr_font = font_test_data::COLRV1_NO_CLIPLIST;
507        let font = FontRef::new(colr_font).unwrap();
508        let cycle_glyph_id = GlyphId::new(1);
509        let colrv1_glyph = font
510            .color_glyphs()
511            .get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
512
513        assert!(colrv1_glyph.is_some());
514        let mut color_painter = DummyColorPainter::new();
515
516        let result = colrv1_glyph
517            .unwrap()
518            .paint(LocationRef::default(), &mut color_painter);
519        assert!(result.is_ok());
520    }
521
522    #[test]
523    fn colrv0_no_bbox_test() {
524        let colr_font = font_test_data::COLRV0V1;
525        let font = FontRef::new(colr_font).unwrap();
526        let colrv0_glyph_id = GlyphId::new(168);
527        let colrv0_glyph = font
528            .color_glyphs()
529            .get_with_format(colrv0_glyph_id, super::ColorGlyphFormat::ColrV0)
530            .unwrap();
531        assert!(colrv0_glyph
532            .bounding_box(LocationRef::default(), Size::unscaled())
533            .is_none());
534    }
535}