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