Skip to main content

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 traversal;
40
41#[cfg(test)]
42mod traversal_tests;
43
44use raw::{
45    tables::{colr, cpal},
46    types::BigEndian,
47    FontRef,
48};
49#[cfg(test)]
50use serde::{Deserialize, Serialize};
51
52pub use read_fonts::tables::colr::{CompositeMode, Extend};
53
54use read_fonts::{
55    types::{BoundingBox, GlyphId, Point},
56    TableProvider,
57};
58
59#[doc(inline)]
60pub use cpal::ColorRecord as Color;
61
62use std::{fmt::Debug, ops::Range};
63
64use traversal::{
65    get_clipbox_font_units, traverse_v0_range, traverse_with_callbacks, PaintDecycler,
66};
67
68use crate::string::StringId;
69use crate::{
70    color::traversal::TraversalState,
71    prelude::{LocationRef, Size},
72};
73
74use instance::PaintId;
75
76/// A transformation matrix.
77pub type Transform = read_fonts::types::Matrix<f32>;
78
79/// An error during drawing a COLR glyph.
80///
81/// This covers inconsistencies in the COLRv1 paint graph as well as downstream
82/// parse errors from read-fonts.
83#[derive(Debug, Clone)]
84pub enum PaintError {
85    /// The font data does not make sense: absent where it was required, too
86    /// short, or self-inconsistent.
87    ///
88    /// Carries nothing, for the reason given on
89    /// [`DrawError::Malformed`][crate::outline::DrawError::Malformed].
90    Malformed,
91    GlyphNotFound(GlyphId),
92    PaintCycleDetected,
93    DepthLimitExceeded,
94}
95
96impl std::fmt::Display for PaintError {
97    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98        match self {
99            PaintError::Malformed => write!(f, "font data was absent or malformed"),
100            PaintError::GlyphNotFound(glyph_id) => {
101                write!(f, "No COLRv1 glyph found for glyph id: {glyph_id}")
102            }
103            PaintError::PaintCycleDetected => write!(f, "Paint cycle detected in COLRv1 glyph."),
104            PaintError::DepthLimitExceeded => write!(f, "Depth limit exceeded in COLRv1 glyph."),
105        }
106    }
107}
108
109impl core::error::Error for PaintError {
110    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
111        None
112    }
113}
114
115/// A color stop of a gradient.
116///
117/// All gradient callbacks of [`ColorPainter`] normalize color stops to be in the range of 0
118/// to 1.
119#[derive(Copy, Clone, PartialEq, Debug, Default)]
120#[cfg_attr(test, derive(Serialize, Deserialize))]
121// This repr(C) is required so that C-side FFI's
122// are able to cast the ColorStop slice to a C-side array pointer.
123#[repr(C)]
124pub struct ColorStop {
125    pub offset: f32,
126    /// Specifies a color from the `CPAL` table.
127    pub palette_index: u16,
128    /// Additional alpha value, to be multiplied with the color above before use.
129    pub alpha: f32,
130}
131
132// Design considerations for choosing a slice of ColorStops as `color_stop`
133// type: In principle, a local `Vec<ColorStop>` allocation would not required if
134// we're willing to walk the `ResolvedColorStop` iterator to find the minimum
135// and maximum color stops.  Then we could scale the color stops based on the
136// minimum and maximum. But performing the min/max search would require
137// re-applying the deltas at least once, after which we would pass the scaled
138// stops to client side and have the client sort the collected items once
139// again. If we do want to pre-ort them, and still use use an
140// `Iterator<Item=ColorStop>` instead as the `color_stops` field, then we would
141// need a Fontations-side allocations to sort, and an extra allocation on the
142// client side to `.collect()` from the provided iterator before passing it to
143// drawing API.
144//
145/// A fill type of a COLRv1 glyph (solid fill or various gradient types).
146///
147/// The client receives the information about the fill type in the
148/// [`fill`](ColorPainter::fill) callback of the [`ColorPainter`] trait.
149#[derive(Debug, Clone, PartialEq)]
150pub enum Brush<'a> {
151    /// A solid fill with the color specified by `palette_index`. The respective
152    /// color from the CPAL table then needs to be multiplied with `alpha`.
153    Solid { palette_index: u16, alpha: f32 },
154    /// A linear gradient, normalized from the P0, P1 and P2 representation in
155    /// the COLRv1 table to a linear gradient between two points `p0` and
156    /// `p1`. If there is only one color stop, the client should draw a solid
157    /// fill with that color. The `color_stops` are normalized to the range from
158    /// 0 to 1.
159    LinearGradient {
160        p0: Point<f32>,
161        p1: Point<f32>,
162        color_stops: &'a [ColorStop],
163        extend: Extend,
164    },
165    /// A radial gradient, with color stops normalized to the range of 0 to 1.
166    /// Caution: This normalization can mean that negative radii occur. It is
167    /// the client's responsibility to truncate the color line at the 0
168    /// position, interpolating between `r0` and `r1` and compute an
169    /// interpolated color at that position.
170    RadialGradient {
171        c0: Point<f32>,
172        r0: f32,
173        c1: Point<f32>,
174        r1: f32,
175        color_stops: &'a [ColorStop],
176        extend: Extend,
177    },
178    /// A sweep gradient, also called conical gradient. The color stops are
179    /// normalized to the range from 0 to 1 and the returned angles are to be
180    /// interpreted in _clockwise_ direction (swapped from the meaning in the
181    /// font file).  The stop normalization may mean that the angles may be
182    /// larger or smaller than the range of 0 to 360. Note that only the range
183    /// from 0 to 360 degrees is to be drawn, see
184    /// <https://learn.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients>.
185    SweepGradient {
186        c0: Point<f32>,
187        start_angle: f32,
188        end_angle: f32,
189        color_stops: &'a [ColorStop],
190        extend: Extend,
191    },
192}
193
194/// Signals success of request to draw a COLRv1 sub glyph from cache.
195///
196/// Result of [`paint_cached_color_glyph`](ColorPainter::paint_cached_color_glyph)
197/// through which the client signals whether a COLRv1 glyph referenced by
198/// another COLRv1 glyph was drawn from cache or whether the glyph's subgraph
199/// should be traversed by the skria side COLRv1 implementation.
200pub enum PaintCachedColorGlyph {
201    /// The specified COLRv1 glyph has been successfully painted client side.
202    Ok,
203    /// The client does not implement drawing COLRv1 glyphs from cache and the
204    /// Fontations side COLRv1 implementation is asked to traverse the
205    /// respective PaintColorGlyph sub graph.
206    Unimplemented,
207}
208
209/// A group of required painting callbacks to be provided by the client.
210///
211/// Each callback is executing a particular drawing or canvas transformation
212/// operation. The trait's callback functions are invoked when
213/// [`paint`](ColorGlyph::paint) is called with a [`ColorPainter`] trait
214/// object. The documentation for each function describes what actions are to be
215/// executed using the client side 2D graphics API, usually by performing some
216/// kind of canvas operation.
217pub trait ColorPainter {
218    /// Push the specified transform by concatenating it to the current
219    /// transformation matrix.
220    fn push_transform(&mut self, transform: Transform);
221
222    /// Restore the transformation matrix to the state before the previous
223    /// [`push_transform`](ColorPainter::push_transform) call.
224    fn pop_transform(&mut self);
225
226    /// Apply a clip path in the shape of glyph specified by `glyph_id`.
227    fn push_clip_glyph(&mut self, glyph_id: GlyphId);
228
229    /// Apply a clip rectangle specified by `clip_rect`.
230    fn push_clip_box(&mut self, clip_box: BoundingBox<f32>);
231
232    /// Restore the clip state to the state before a previous
233    /// [`push_clip_glyph`](ColorPainter::push_clip_glyph) or
234    /// [`push_clip_box`](ColorPainter::push_clip_box) call.
235    fn pop_clip(&mut self);
236
237    /// Fill the current clip area with the specified gradient fill.
238    fn fill(&mut self, brush: Brush<'_>);
239
240    /// Combined clip and fill operation.
241    ///
242    /// Apply the clip path determined by the specified `glyph_id`, then fill it
243    /// with the specified [`brush`](Brush), applying the `_brush_transform`
244    /// transformation matrix to the brush. The default implementation works
245    /// based on existing methods in this trait. It is recommended for clients
246    /// to override the default implementaition with a custom combined clip and
247    /// fill operation. In this way overriding likely results in performance
248    /// gains depending on performance characteristics of the 2D graphics stack
249    /// that these calls are mapped to.
250    fn fill_glyph(
251        &mut self,
252        glyph_id: GlyphId,
253        brush_transform: Option<Transform>,
254        brush: Brush<'_>,
255    ) {
256        self.push_clip_glyph(glyph_id);
257        if let Some(wrap_in_transform) = brush_transform {
258            self.push_transform(wrap_in_transform);
259            self.fill(brush);
260            self.pop_transform();
261        } else {
262            self.fill(brush);
263        }
264        self.pop_clip();
265    }
266
267    /// Optionally implement this method: Draw an unscaled COLRv1 glyph given
268    /// the current transformation matrix (as accumulated by
269    /// [`push_transform`](ColorPainter::push_transform) calls).
270    fn paint_cached_color_glyph(
271        &mut self,
272        _glyph: GlyphId,
273    ) -> Result<PaintCachedColorGlyph, PaintError> {
274        Ok(PaintCachedColorGlyph::Unimplemented)
275    }
276
277    /// Open a new layer, and merge the layer down using `composite_mode` when
278    /// [`pop_layer`](ColorPainter::pop_layer) is called, signalling that this layer is done drawing.
279    fn push_layer(&mut self, composite_mode: CompositeMode);
280
281    /// Merge the pushed layer down using `composite_mode` passed to the matching
282    /// [`push_layer`](ColorPainter::push_layer).
283    fn pop_layer(&mut self) {}
284
285    /// Alternative version of [`push_layer`](ColorPainter::push_layer) where the
286    /// `composite_mode` is also passed to the method. This is useful for
287    /// graphics libraries that need the compositing mode at layer pop time
288    /// and do not want to manually track the mode.
289    ///
290    /// Only one of [`pop_layer`](ColorPainter::pop_layer) or this method
291    /// need to be implemented. By default, this simply calls
292    /// [`pop_layer`](ColorPainter::pop_layer).
293    fn pop_layer_with_mode(&mut self, _composite_mode: CompositeMode) {
294        self.pop_layer();
295    }
296}
297
298/// Distinguishes available color glyph formats.
299#[derive(Clone, Copy)]
300pub enum ColorGlyphFormat {
301    ColrV0,
302    ColrV1,
303}
304
305/// A representation of a color glyph that can be painted through a sequence of [`ColorPainter`] callbacks.
306#[derive(Clone)]
307pub struct ColorGlyph<'a> {
308    colr: colr::Colr<'a>,
309    root_paint_ref: ColorGlyphRoot<'a>,
310}
311
312#[derive(Clone)]
313enum ColorGlyphRoot<'a> {
314    V0Range(Range<usize>),
315    V1Paint(colr::Paint<'a>, PaintId, GlyphId, Option<u16>),
316}
317
318impl<'a> ColorGlyph<'a> {
319    /// Returns the version of the color table from which this outline was
320    /// selected.
321    pub fn format(&self) -> ColorGlyphFormat {
322        match &self.root_paint_ref {
323            ColorGlyphRoot::V0Range(_) => ColorGlyphFormat::ColrV0,
324            ColorGlyphRoot::V1Paint(..) => ColorGlyphFormat::ColrV1,
325        }
326    }
327
328    /// Returns the bounding box.
329    ///
330    /// For COLRv1 glyphs, this is the clip box of the specified COLRv1 glyph,
331    /// or `None` if clip boxes are not present or if there is none for the
332    /// particular glyph.
333    ///
334    /// Always returns `None` for COLRv0 glyphs because precomputed clip boxes
335    /// are never available.
336    ///
337    /// The `size` argument can optionally be used to scale the bounding box
338    /// to a particular font size and `location` allows specifying a variation
339    /// instance.
340    pub fn bounding_box(
341        &self,
342        location: impl Into<LocationRef<'a>>,
343        size: Size,
344    ) -> Option<BoundingBox<f32>> {
345        match &self.root_paint_ref {
346            ColorGlyphRoot::V1Paint(_paint, _paint_id, glyph_id, upem) => {
347                let instance =
348                    instance::ColrInstance::new(self.colr.clone(), location.into().coords());
349                let resolved_bounding_box = get_clipbox_font_units(&instance, *glyph_id);
350                resolved_bounding_box.map(|bounding_box| {
351                    let scale_factor = size.linear_scale(upem.unwrap_or(0));
352                    bounding_box.scale(scale_factor)
353                })
354            }
355            _ => None,
356        }
357    }
358
359    /// Evaluates the paint graph at the specified location in variation space
360    /// and emits the results to the given painter.
361    ///
362    ///
363    /// For a COLRv1 glyph, traverses the COLRv1 paint graph and invokes drawing callbacks on a
364    /// specified [`ColorPainter`] trait object.  The traversal operates in font
365    /// units and will call `ColorPainter` methods with font unit values. This
366    /// means, if you want to draw a COLRv1 glyph at a particular font size, the
367    /// canvas needs to have a transformation matrix applied so that it scales down
368    /// the drawing operations to `font_size / upem`.
369    ///
370    /// # Arguments
371    ///
372    /// * `glyph_id` the `GlyphId` to be drawn.
373    /// * `location` coordinates for specifying a variation instance. This can be empty.
374    /// * `painter` a client-provided [`ColorPainter`] implementation receiving drawing callbacks.
375    ///
376    pub fn paint(
377        &self,
378        location: impl Into<LocationRef<'a>>,
379        painter: &mut impl ColorPainter,
380    ) -> Result<(), PaintError> {
381        let instance =
382            instance::ColrInstance::new(self.colr.clone(), location.into().effective_coords());
383        match &self.root_paint_ref {
384            ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, _) => {
385                let clipbox = get_clipbox_font_units(&instance, *glyph_id);
386                if let Some(rect) = clipbox {
387                    painter.push_clip_box(rect);
388                }
389                let mut decycler = PaintDecycler::default();
390                let mut cycle_guard = decycler.enter(*paint_id)?;
391                let mut state = TraversalState::new(instance, painter);
392                traverse_with_callbacks(
393                    &state.resolve_paint(paint)?,
394                    &mut state,
395                    &mut cycle_guard,
396                    0,
397                )?;
398                if clipbox.is_some() {
399                    painter.pop_clip();
400                }
401                Ok(())
402            }
403            ColorGlyphRoot::V0Range(range) => {
404                traverse_v0_range(range, &instance, painter)?;
405                Ok(())
406            }
407        }
408    }
409}
410
411/// Collection of color glyphs.
412#[derive(Clone)]
413pub struct ColorGlyphCollection<'a> {
414    colr: Option<colr::Colr<'a>>,
415    upem: Option<u16>,
416}
417
418impl<'a> ColorGlyphCollection<'a> {
419    /// Creates a new collection of paintable color glyphs for the given font.
420    pub fn new(font: &FontRef<'a>) -> Self {
421        let colr = font.colr().ok();
422        let upem = font.head().map(|h| h.units_per_em()).ok();
423
424        Self { colr, upem }
425    }
426
427    /// Returns the color glyph representation for the given glyph identifier,
428    /// given a specific format.
429    pub fn get_with_format(
430        &self,
431        glyph_id: GlyphId,
432        glyph_format: ColorGlyphFormat,
433    ) -> Option<ColorGlyph<'a>> {
434        let colr = self.colr.clone()?;
435
436        let root_paint_ref = match glyph_format {
437            ColorGlyphFormat::ColrV0 => {
438                let layer_range = colr.v0_base_glyph(glyph_id)?;
439                ColorGlyphRoot::V0Range(layer_range)
440            }
441            ColorGlyphFormat::ColrV1 => {
442                let (paint, paint_id) = colr.v1_base_glyph(glyph_id)?;
443                ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, self.upem)
444            }
445        };
446        Some(ColorGlyph {
447            colr,
448            root_paint_ref,
449        })
450    }
451
452    /// Returns a color glyph representation for the given glyph identifier if
453    /// available, preferring a COLRv1 representation over a COLRv0
454    /// representation.
455    pub fn get(&self, glyph_id: GlyphId) -> Option<ColorGlyph<'a>> {
456        self.get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
457            .or_else(|| self.get_with_format(glyph_id, ColorGlyphFormat::ColrV0))
458    }
459}
460
461/// A single color palette.
462pub struct ColorPalette<'a> {
463    cpal: cpal::Cpal<'a>,
464    /// Preparsed subarray of color records for just this palette.
465    sub_array: &'a [Color],
466    /// This palette's index in the CPAL table.
467    index: u16,
468}
469
470impl ColorPalette<'_> {
471    /// Returns the colors contained within this palette.
472    pub fn colors(&self) -> &[Color] {
473        self.sub_array
474    }
475
476    /// Returns this palette's type flags (currently, whether this palette is appropriate for use on
477    /// a light and/or dark background). This may not always be present.
478    pub fn palette_type(&self) -> Option<cpal::PaletteType> {
479        self.cpal
480            .palette_types_array()?
481            .ok()?
482            .get(usize::from(self.index))
483            .map(|p| p.get())
484    }
485
486    /// Returns this palette's label/name, if present.
487    pub fn label(&self) -> Option<StringId> {
488        self.cpal
489            .palette_labels_array()?
490            .ok()?
491            .get(usize::from(self.index))
492            .and_then(|p| {
493                let name_id = p.get();
494                Some(name_id).filter(|name_id| name_id.to_u16() != 0xFFFF)
495            })
496    }
497
498    /// Returns this palette's index in the CPAL table.
499    pub fn index(&self) -> u16 {
500        self.index
501    }
502}
503
504/// Collection of color palettes for color glyphs.
505pub struct ColorPalettes<'a> {
506    cpal: Option<cpal::Cpal<'a>>,
507}
508
509impl<'a> ColorPalettes<'a> {
510    /// Creates a new collection of color palettes for the given font.
511    pub fn new(font: &FontRef<'a>) -> Self {
512        Self {
513            cpal: font.cpal().ok(),
514        }
515    }
516
517    /// Returns the total number of palettes in this collection (0 if this collection's font has no
518    /// CPAL table).
519    pub fn len(&self) -> u16 {
520        self.cpal.as_ref().map_or(0, |cpal| cpal.num_palettes())
521    }
522
523    /// Returns true if the collection is empty.
524    pub fn is_empty(&self) -> bool {
525        self.len() == 0
526    }
527
528    /// Returns the color palette at the given index. The palette at index 0 is the default palette.
529    pub fn get(&self, index: u16) -> Option<ColorPalette<'_>> {
530        let cpal = self.cpal.clone()?;
531
532        let start_index: &BigEndian<u16> = cpal.color_record_indices().get(usize::from(index))?;
533        let start_index = usize::from(start_index.get());
534        let num_palette_entries = usize::from(cpal.num_palette_entries());
535
536        // Get the slice of the color records array containing just the chosen palette's colors.
537        let color_records_array = cpal.color_records_array()?.ok()?;
538        let sub_array = color_records_array.get(start_index..start_index + num_palette_entries)?;
539
540        Some(ColorPalette {
541            cpal,
542            sub_array,
543            index,
544        })
545    }
546
547    /// Returns the label/name for a given color, if present (labels are per-color, but shared
548    /// across all palettes).
549    pub fn color_label(&self, color_index: u16) -> Option<StringId> {
550        let name_id = self
551            .cpal
552            .as_ref()?
553            .palette_entry_labels_array()?
554            .ok()?
555            .get(usize::from(color_index))?
556            .get();
557        Some(name_id).filter(|name_id| name_id.to_u16() != 0xFFFF)
558    }
559}
560
561#[cfg(test)]
562mod tests {
563
564    use crate::{
565        color::traversal_tests::test_glyph_defs::PAINTCOLRGLYPH_CYCLE,
566        prelude::{LocationRef, Size},
567        MetadataProvider,
568    };
569
570    use raw::{tables::cpal, TableProvider};
571    use read_fonts::{types::BoundingBox, FontRef};
572
573    use super::{Brush, ColorGlyphFormat, ColorPainter, CompositeMode, GlyphId, Transform};
574    use crate::color::traversal_tests::test_glyph_defs::{COLORED_CIRCLES_V0, COLORED_CIRCLES_V1};
575
576    #[test]
577    fn has_colrv1_glyph_test() {
578        let colr_font = font_test_data::COLRV0V1_VARIABLE;
579        let font = FontRef::new(colr_font).unwrap();
580        let get_colrv1_glyph = |codepoint: &[char]| {
581            font.charmap().map(codepoint[0]).and_then(|glyph_id| {
582                font.color_glyphs()
583                    .get_with_format(glyph_id, crate::color::ColorGlyphFormat::ColrV1)
584            })
585        };
586
587        assert!(get_colrv1_glyph(COLORED_CIRCLES_V0).is_none());
588        assert!(get_colrv1_glyph(COLORED_CIRCLES_V1).is_some());
589    }
590    struct DummyColorPainter {}
591
592    impl DummyColorPainter {
593        pub fn new() -> Self {
594            Self {}
595        }
596    }
597
598    impl Default for DummyColorPainter {
599        fn default() -> Self {
600            Self::new()
601        }
602    }
603
604    impl ColorPainter for DummyColorPainter {
605        fn push_transform(&mut self, _transform: Transform) {}
606        fn pop_transform(&mut self) {}
607        fn push_clip_glyph(&mut self, _glyph: GlyphId) {}
608        fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {}
609        fn pop_clip(&mut self) {}
610        fn fill(&mut self, _brush: Brush) {}
611        fn push_layer(&mut self, _composite_mode: CompositeMode) {}
612        fn pop_layer(&mut self) {}
613    }
614
615    #[test]
616    fn paintcolrglyph_cycle_test() {
617        let colr_font = font_test_data::COLRV0V1_VARIABLE;
618        let font = FontRef::new(colr_font).unwrap();
619        let cycle_glyph_id = font.charmap().map(PAINTCOLRGLYPH_CYCLE[0]).unwrap();
620        let colrv1_glyph = font
621            .color_glyphs()
622            .get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
623
624        assert!(colrv1_glyph.is_some());
625        let mut color_painter = DummyColorPainter::new();
626
627        let result = colrv1_glyph
628            .unwrap()
629            .paint(LocationRef::default(), &mut color_painter);
630        // Expected to fail with an error as the glyph contains a paint cycle.
631        assert!(result.is_err());
632    }
633
634    #[test]
635    fn paint_glyph2_public_painter_preserves_boundary_glyph_ids() {
636        use font_test_data::bebuffer::BeBuffer;
637
638        #[derive(Default)]
639        struct Painter {
640            glyphs: Vec<GlyphId>,
641            fills: Vec<(u16, f32)>,
642            clip_depth: usize,
643        }
644        impl ColorPainter for Painter {
645            fn push_transform(&mut self, _: Transform) {
646                panic!("unexpected transform");
647            }
648            fn pop_transform(&mut self) {
649                panic!("unexpected transform");
650            }
651            fn push_clip_glyph(&mut self, glyph: GlyphId) {
652                self.glyphs.push(glyph);
653                self.clip_depth += 1;
654            }
655            fn push_clip_box(&mut self, _: BoundingBox<f32>) {
656                panic!("unexpected clip box");
657            }
658            fn pop_clip(&mut self) {
659                self.clip_depth = self.clip_depth.checked_sub(1).unwrap();
660            }
661            fn fill(&mut self, brush: Brush) {
662                assert_eq!(self.clip_depth, 1);
663                match brush {
664                    Brush::Solid {
665                        palette_index,
666                        alpha,
667                    } => self.fills.push((palette_index, alpha)),
668                    _ => panic!("unexpected brush"),
669                }
670            }
671            fn push_layer(&mut self, _: CompositeMode) {
672                panic!("unexpected layer");
673            }
674            fn pop_layer(&mut self) {
675                panic!("unexpected layer");
676            }
677        }
678
679        for gid in [65535u32, 65536, 0xffffff] {
680            let mut colr = font_test_data::colr::paint_glyph2_colr().to_vec();
681            let paint_offset = 34 + 10;
682            colr[paint_offset + 4..paint_offset + 7].copy_from_slice(&gid.to_be_bytes()[1..]);
683            let bytes = BeBuffer::new()
684                .push(0x00010000u32)
685                .push(1u16)
686                .push(16u16)
687                .push(0u16)
688                .push(0u16)
689                .extend(*b"COLR")
690                .push(0u32)
691                .push(28u32)
692                .push(colr.len() as u32)
693                .extend(colr.iter().copied());
694            let font = FontRef::new(&bytes).unwrap();
695            let colors = font.color_glyphs();
696            let glyph = colors
697                .get_with_format(GlyphId::new(42), ColorGlyphFormat::ColrV1)
698                .unwrap();
699            assert!(colors
700                .get_with_format(GlyphId::new(43), ColorGlyphFormat::ColrV1)
701                .is_none());
702            let mut painter = Painter::default();
703            for _ in 0..2 {
704                glyph.paint(LocationRef::default(), &mut painter).unwrap();
705                assert_eq!(painter.clip_depth, 0);
706            }
707            assert_eq!(painter.glyphs, [GlyphId::new(gid); 2]);
708            assert_eq!(painter.fills, [(7, 1.0); 2]);
709        }
710    }
711
712    #[test]
713    fn no_cliplist_test() {
714        let colr_font = font_test_data::COLRV1_NO_CLIPLIST;
715        let font = FontRef::new(colr_font).unwrap();
716        let cycle_glyph_id = GlyphId::new(1);
717        let colrv1_glyph = font
718            .color_glyphs()
719            .get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
720
721        assert!(colrv1_glyph.is_some());
722        let mut color_painter = DummyColorPainter::new();
723
724        let result = colrv1_glyph
725            .unwrap()
726            .paint(LocationRef::default(), &mut color_painter);
727        assert!(result.is_ok());
728    }
729
730    #[test]
731    fn colrv0_no_bbox_test() {
732        let colr_font = font_test_data::COLRV0V1;
733        let font = FontRef::new(colr_font).unwrap();
734        let colrv0_glyph_id = GlyphId::new(168);
735        let colrv0_glyph = font
736            .color_glyphs()
737            .get_with_format(colrv0_glyph_id, super::ColorGlyphFormat::ColrV0)
738            .unwrap();
739        assert!(colrv0_glyph
740            .bounding_box(LocationRef::default(), Size::unscaled())
741            .is_none());
742    }
743
744    #[test]
745    fn cpal_test() {
746        use crate::color::Color;
747        let cpal_font = font_test_data::COLRV0V1;
748        let font = FontRef::new(cpal_font).unwrap();
749        let palettes = font.color_palettes();
750        assert_eq!(palettes.len(), 3);
751
752        let first_palette = palettes.get(0).unwrap();
753        assert_eq!(first_palette.colors().len(), 14);
754        assert_eq!(
755            first_palette.colors().first(),
756            Some(&Color {
757                blue: 0,
758                green: 0,
759                red: 255,
760                alpha: 255
761            })
762        );
763        assert_eq!(first_palette.colors().get(14), None);
764        assert_eq!(
765            first_palette.palette_type(),
766            Some(cpal::PaletteType::empty())
767        );
768
769        let second_palette = palettes.get(1).unwrap();
770        assert_eq!(
771            second_palette.colors().first(),
772            Some(&Color {
773                blue: 74,
774                green: 41,
775                red: 42,
776                alpha: 255
777            })
778        );
779        assert_eq!(
780            second_palette.palette_type(),
781            Some(cpal::PaletteType::USABLE_WITH_DARK_BACKGROUND)
782        );
783
784        let third_palette = palettes.get(2).unwrap();
785        assert_eq!(
786            third_palette.colors().first(),
787            Some(&Color {
788                blue: 24,
789                green: 113,
790                red: 252,
791                alpha: 255
792            })
793        );
794        assert_eq!(
795            third_palette.palette_type(),
796            Some(cpal::PaletteType::USABLE_WITH_LIGHT_BACKGROUND)
797        );
798
799        assert!(palettes.get(3).is_none());
800    }
801
802    /// The Ecuador πŸ‡ͺπŸ‡¨ (U+1F1EA U+1F1E8) and El Salvador πŸ‡ΈπŸ‡» (U+1F1F8 U+1F1FB) flags have the most
803    /// complex COLRv1 paint graphs in Noto Color Emoji, requiring traversal of ~6700 paint
804    /// nodes. This exceeds the original traversal budget of 4096 nodes, so rendering them validates
805    /// that the increased budget is sufficient.
806    #[test]
807    fn paint_flags_requiring_increased_node_budget() {
808        let font = FontRef::new(font_test_data::NOTO_COLOR_EMOJI_FLAGS).unwrap();
809        let glyph_ids = font
810            .colr()
811            .unwrap()
812            .base_glyph_list()
813            .unwrap()
814            .unwrap()
815            .base_glyph_paint_records()
816            .iter()
817            .map(|record| GlyphId::from(record.glyph_id()))
818            .collect::<Vec<_>>();
819        assert!(!glyph_ids.is_empty());
820        for gid in glyph_ids {
821            font.color_glyphs()
822                .get_with_format(gid, ColorGlyphFormat::ColrV1)
823                .unwrap_or_else(|| panic!("no COLRv1 glyph for {gid}"))
824                .paint(LocationRef::default(), &mut DummyColorPainter::default())
825                .unwrap_or_else(|e| panic!("failed to paint {gid}: {e}"));
826        }
827    }
828}