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