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