Skip to main content

read_fonts/model/glyph/
color.rs

1//! Color glyphs.
2//!
3//! A `COLR` glyph is painted through a sequence of [`PaintSink`] callbacks
4//! for transforms, clips, fills and layers. Version 0 glyphs are layers of
5//! solid-filled outlines. Version 1 adds gradients, transforms and
6//! compositing. [`ColorGlyph::paint`] paints both versions in font units at
7//! the font's location.
8//!
9//! # Examples
10//!
11//! Paint a glyph:
12//!
13//! ```
14//! # use read_fonts::{model::{glyph::color::PaintSink, Error, Font}, types::GlyphId};
15//! # fn paint(font: &Font, sink: &mut impl PaintSink, glyph: GlyphId) -> Result<(), Error> {
16//! let glyph = font.color_glyphs().get(glyph).expect("a color glyph");
17//! glyph.paint(sink)
18//! # }
19//! ```
20//!
21//! Measure a glyph. This uses the clip box if the glyph has
22//! one, and the bounds of what it paints otherwise:
23//!
24//! ```
25//! # use read_fonts::{model::Font, types::GlyphId};
26//! # fn measure(font: &Font, glyph: GlyphId) {
27//! if let Some(glyph) = font.color_glyphs().get(glyph) {
28//!     let extents = glyph.extents().expect("a well formed paint graph");
29//!     println!("{extents:?}");
30//! }
31//! # }
32//! ```
33
34mod bounds;
35mod decycler;
36mod instance;
37mod traversal;
38
39#[cfg(test)]
40pub(crate) mod test_glyph_defs;
41#[cfg(all(test, feature = "serde", feature = "std"))]
42mod traversal_tests;
43
44use crate::{
45    model::{font::ColrTables, glyph::outline::OutlineGlyphs, metrics::GlyphExtents, Error, Font},
46    tables::{colr, cpal::Cpal},
47    types::{BoundingBox, GlyphId, Matrix, NameId, Point},
48};
49use core::ops::Range;
50use instance::{ColrInstance, PaintId};
51use traversal::{traverse_v0_range, traverse_with_callbacks, PaintDecycler, TraversalState};
52
53#[doc(inline)]
54pub use crate::tables::colr::{CompositeMode, Extend};
55#[doc(inline)]
56pub use crate::tables::cpal::{ColorRecord as Color, PaletteType};
57
58/// A color stop of a gradient.
59///
60/// Gradients passed to a [`PaintSink`] always have their stops normalized to
61/// the range 0 to 1.
62#[derive(Copy, Clone, PartialEq, Debug, Default)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64// `repr(C)` so a C caller can read a slice of these as an array.
65#[repr(C)]
66pub struct ColorStop {
67    /// The position of the stop along the gradient, from 0 to 1.
68    pub offset: f32,
69    /// The index of the color in a `CPAL` palette.
70    pub palette_index: u16,
71    /// A multiplier applied to the alpha of the color.
72    pub alpha: f32,
73}
74
75/// A solid color or gradient used to fill a region.
76///
77/// Gradients are normalized. A linear gradient is reduced to two points with
78/// its stops between them. Other gradients have their stops in the range 0
79/// to 1, with their geometry adjusted to match.
80#[derive(Debug, Clone, PartialEq)]
81pub enum Brush<'a> {
82    /// A solid fill with the color at `palette_index`, multiplied by `alpha`.
83    Solid { palette_index: u16, alpha: f32 },
84    /// A linear gradient from `p0` to `p1`, reduced from the three points in
85    /// the table.
86    ///
87    /// With one stop, the fill is the solid color of that stop.
88    LinearGradient {
89        p0: Point<f32>,
90        p1: Point<f32>,
91        color_stops: &'a [ColorStop],
92        extend: Extend,
93    },
94    /// A radial gradient between two circles.
95    ///
96    /// Normalizing the stops can produce a negative radius. In that case, the
97    /// color line is truncated where the interpolated radius reaches zero.
98    RadialGradient {
99        c0: Point<f32>,
100        r0: f32,
101        c1: Point<f32>,
102        r1: f32,
103        color_stops: &'a [ColorStop],
104        extend: Extend,
105    },
106    /// A sweep gradient around `c0`, with angles in degrees clockwise.
107    ///
108    /// This is the reverse of the direction in the table. Normalizing the
109    /// stops can move the angles outside 0 to 360. Only that range is drawn.
110    SweepGradient {
111        c0: Point<f32>,
112        start_angle: f32,
113        end_angle: f32,
114        color_stops: &'a [ColorStop],
115        extend: Extend,
116    },
117}
118
119/// Whether a sink painted a nested color glyph from its own cache.
120///
121/// Returned from [`PaintSink::paint_cached_color_glyph`] when one color
122/// glyph paints another.
123pub enum PaintCachedColorGlyph {
124    /// The sink painted the glyph itself.
125    Ok,
126    /// The sink has no cached copy, so the paint graph of the glyph is
127    /// traversed.
128    Unimplemented,
129}
130
131/// Receives the operations that paint a color glyph.
132///
133/// Each callback is one canvas operation. [`ColorGlyph::paint`] calls them
134/// with values in font units. A sink that draws at a specific size scales
135/// them by `size / upem`.
136pub trait PaintSink {
137    /// Concatenates `transform` to the current transformation matrix.
138    fn push_transform(&mut self, transform: Matrix<f32>);
139
140    /// Restores the transformation matrix from before the last
141    /// [`push_transform`](Self::push_transform).
142    fn pop_transform(&mut self);
143
144    /// Clips to the outline of `glyph`.
145    fn push_clip_glyph(&mut self, glyph: GlyphId);
146
147    /// Clips to `clip_box`.
148    fn push_clip_box(&mut self, clip_box: BoundingBox<f32>);
149
150    /// Restores the clip from before the last [`push_clip_glyph`] or
151    /// [`push_clip_box`].
152    ///
153    /// [`push_clip_glyph`]: Self::push_clip_glyph
154    /// [`push_clip_box`]: Self::push_clip_box
155    fn pop_clip(&mut self);
156
157    /// Fills the current clip with `brush`.
158    fn fill(&mut self, brush: Brush<'_>);
159
160    /// Clips to the outline of `glyph` and fills it with `brush`, with
161    /// `brush_transform` applied to the brush alone.
162    ///
163    /// The default implementation uses the other callbacks. Override it if
164    /// the graphics library can fill a path in one call.
165    fn fill_glyph(
166        &mut self,
167        glyph: GlyphId,
168        brush_transform: Option<Matrix<f32>>,
169        brush: Brush<'_>,
170    ) {
171        self.push_clip_glyph(glyph);
172        if let Some(wrap_in_transform) = brush_transform {
173            self.push_transform(wrap_in_transform);
174            self.fill(brush);
175            self.pop_transform();
176        } else {
177            self.fill(brush);
178        }
179        self.pop_clip();
180    }
181
182    /// Paints a nested color glyph from the sink's cache, if it has one.
183    ///
184    /// The glyph is painted unscaled under the current transformation
185    /// matrix. The default implementation has no cache, so the paint graph
186    /// of the glyph is traversed.
187    fn paint_cached_color_glyph(
188        &mut self,
189        _glyph: GlyphId,
190    ) -> Result<PaintCachedColorGlyph, Error> {
191        Ok(PaintCachedColorGlyph::Unimplemented)
192    }
193
194    /// Pushes a new layer that [`pop_layer`](Self::pop_layer) composites
195    /// onto the layer below with `composite_mode`.
196    fn push_layer(&mut self, composite_mode: CompositeMode);
197
198    /// Composites the last layer onto the layer below, using the mode passed
199    /// to the matching [`push_layer`](Self::push_layer).
200    fn pop_layer(&mut self) {}
201
202    /// Same as [`pop_layer`](Self::pop_layer), but also receives the
203    /// composite mode.
204    ///
205    /// Use this for a graphics library that needs the mode when the layer is
206    /// popped. Only one of the two methods needs an implementation. The
207    /// default calls [`pop_layer`](Self::pop_layer).
208    fn pop_layer_with_mode(&mut self, _composite_mode: CompositeMode) {
209        self.pop_layer();
210    }
211}
212
213/// The version of `COLR` that defines a glyph.
214#[derive(Clone, Copy, PartialEq, Eq, Debug)]
215pub enum ColorGlyphVersion {
216    /// Layered outlines, each filled with a solid color.
217    ColrV0,
218    /// A paint graph.
219    ColrV1,
220}
221
222/// Provides access to the color glyphs in a font.
223///
224/// Glyphs are painted at the location of the font. This type is cheap to
225/// create and copy because it borrows tables that the font has already
226/// parsed.
227#[derive(Clone, Copy)]
228pub struct ColorGlyphs<'a> {
229    font: &'a Font,
230    tables: Option<&'a ColrTables<'a>>,
231}
232
233impl<'a> ColorGlyphs<'a> {
234    pub(crate) fn new(font: &'a Font) -> Self {
235        Self {
236            font,
237            tables: font.colr_tables(),
238        }
239    }
240
241    /// Returns the font that contains these glyphs.
242    pub fn font(&self) -> &'a Font {
243        self.font
244    }
245
246    /// Returns true if the font has no `COLR` table.
247    pub fn is_empty(&self) -> bool {
248        self.tables.is_none()
249    }
250
251    /// Returns the version of the `COLR` table, or `None` if the font has no
252    /// `COLR` table.
253    pub fn version(&self) -> Option<u16> {
254        Some(self.tables?.colr.version())
255    }
256
257    /// Returns the color glyph for `glyph` painted with the given `version`
258    /// of `COLR`, if the table has one.
259    pub fn get_with_version(
260        &self,
261        glyph: GlyphId,
262        version: ColorGlyphVersion,
263    ) -> Option<ColorGlyph<'a>> {
264        let colr = &self.tables?.colr;
265        let root = match version {
266            ColorGlyphVersion::ColrV0 => Root::V0(colr.v0_base_glyph(glyph)?),
267            ColorGlyphVersion::ColrV1 => {
268                let (paint, paint_id) = colr.v1_base_glyph(glyph)?;
269                Root::V1(paint, paint_id)
270            }
271        };
272        Some(ColorGlyph {
273            glyphs: *self,
274            glyph,
275            root,
276        })
277    }
278
279    /// Returns the color glyph for `glyph`.
280    ///
281    /// Uses version 1 of the table if it has the glyph, and version 0
282    /// otherwise.
283    pub fn get(&self, glyph: GlyphId) -> Option<ColorGlyph<'a>> {
284        self.get_with_version(glyph, ColorGlyphVersion::ColrV1)
285            .or_else(|| self.get_with_version(glyph, ColorGlyphVersion::ColrV0))
286    }
287
288    /// Returns the ink bounds of `glyph` in font units, measured as HarfBuzz
289    /// measures a color glyph.
290    ///
291    /// Uses the clip box of a version 1 glyph if it has one. Otherwise the
292    /// glyph is painted, from either version, and its fills are measured.
293    /// Returns `None` if the table has no color glyph for `glyph` or
294    /// painting it fails. A fill clipped to an outline measures the outline
295    /// in `outlines`.
296    pub(crate) fn extents(
297        &self,
298        glyph: GlyphId,
299        outlines: OutlineGlyphs<'a>,
300    ) -> Option<GlyphExtents<f32>> {
301        self.get(glyph)?.extents_with(outlines).ok()
302    }
303
304    fn instance(&self) -> Option<ColrInstance<'a>> {
305        Some(ColrInstance::new(
306            self.tables?,
307            self.font.normalized_coords(),
308            self.font.colr_scalars(),
309        ))
310    }
311}
312
313/// A color glyph, ready to paint through a [`PaintSink`].
314#[derive(Clone)]
315pub struct ColorGlyph<'a> {
316    glyphs: ColorGlyphs<'a>,
317    glyph: GlyphId,
318    root: Root<'a>,
319}
320
321#[derive(Clone)]
322enum Root<'a> {
323    V0(Range<usize>),
324    V1(colr::Paint<'a>, PaintId),
325}
326
327impl<'a> ColorGlyph<'a> {
328    /// Returns the glyph that this color glyph paints.
329    pub fn glyph(&self) -> GlyphId {
330        self.glyph
331    }
332
333    /// Returns the `COLR` version used to paint this glyph.
334    pub fn version(&self) -> ColorGlyphVersion {
335        match &self.root {
336            Root::V0(_) => ColorGlyphVersion::ColrV0,
337            Root::V1(..) => ColorGlyphVersion::ColrV1,
338        }
339    }
340
341    /// Returns the clip box for the glyph, in font units at the font's
342    /// location, if the table defines one.
343    ///
344    /// Only version 1 tables define clip boxes.
345    pub fn clip_box(&self) -> Option<BoundingBox<f32>> {
346        traversal::get_clipbox_font_units(&self.glyphs.instance()?, self.glyph)
347    }
348
349    /// Returns the clip box used by [`extents`](Self::extents), with
350    /// variation deltas rounded to whole units as in HarfBuzz.
351    fn clip_box_for_extents(&self) -> Option<BoundingBox<f32>> {
352        let instance = self.glyphs.instance()?;
353        let clip_box = instance.v1_clip_box(self.glyph)?;
354        Some(instance::resolve_clip_box_for_extents(&instance, &clip_box))
355    }
356
357    /// Paints the glyph into `sink`, in font units at the font's location.
358    ///
359    /// A version 1 glyph is clipped to its clip box, if it has one, and its
360    /// paint graph is traversed from the root. A version 0 glyph fills each
361    /// of its layers in order.
362    pub fn paint(&self, sink: &mut impl PaintSink) -> Result<(), Error> {
363        self.paint_dyn(sink)
364    }
365
366    /// Calls [`paint`](Self::paint) through a trait object, so the traversal
367    /// is compiled once instead of once per sink type.
368    fn paint_dyn(&self, sink: &mut dyn PaintSink) -> Result<(), Error> {
369        let instance = self.glyphs.instance().ok_or(Error::Malformed)?;
370        match &self.root {
371            Root::V1(paint, paint_id) => {
372                let clipbox = traversal::get_clipbox_font_units(&instance, self.glyph);
373                if let Some(rect) = clipbox {
374                    sink.push_clip_box(rect);
375                }
376                let mut decycler = PaintDecycler::default();
377                let mut cycle_guard = decycler.enter(*paint_id)?;
378                let mut state = TraversalState::new(instance, sink);
379                let result = state.resolve_paint(paint).and_then(|resolved| {
380                    traverse_with_callbacks(&resolved, &mut state, &mut cycle_guard, 0)
381                });
382                if clipbox.is_some() {
383                    sink.pop_clip();
384                }
385                result
386            }
387            Root::V0(range) => traverse_v0_range(range, &instance, sink),
388        }
389    }
390
391    /// Returns the ink bounds of the glyph in font units.
392    ///
393    /// Uses the clip box if the table defines one. Otherwise the glyph is
394    /// painted and the bounds of its fills are returned. A fill clipped to an
395    /// outline is measured by the control box of that outline. A glyph that
396    /// paints nothing has zero extents.
397    pub fn extents(&self) -> Result<GlyphExtents<f32>, Error> {
398        self.extents_with(self.glyphs.font.outline_glyphs())
399    }
400
401    /// [`extents`](Self::extents), measuring clip glyphs in `outlines`.
402    fn extents_with(&self, outlines: OutlineGlyphs<'a>) -> Result<GlyphExtents<f32>, Error> {
403        if let Some(clip_box) = self.clip_box_for_extents() {
404            return Ok(clip_box.into());
405        }
406        let mut sink = bounds::BoundsSink::new(outlines);
407        self.paint_dyn(&mut sink)?;
408        Ok(sink.bounds().into())
409    }
410}
411
412/// A single color palette.
413#[derive(Clone, Copy)]
414pub struct Palette<'a> {
415    cpal: &'a Cpal<'a>,
416    /// The color records for this palette.
417    colors: &'a [Color],
418    /// The palette index in the table.
419    index: u32,
420}
421
422impl<'a> Palette<'a> {
423    /// Returns the colors in the palette.
424    pub fn colors(&self) -> &'a [Color] {
425        self.colors
426    }
427
428    /// Returns whether the palette is meant for a light or dark background,
429    /// if the table provides this.
430    pub fn palette_type(&self) -> Option<PaletteType> {
431        self.cpal
432            .palette_types_array()?
433            .ok()?
434            .get(self.index as usize)
435            .map(|p| p.get())
436    }
437
438    /// Returns the name ID for the palette label, if the table has one.
439    pub fn label(&self) -> Option<NameId> {
440        let name_id = self
441            .cpal
442            .palette_labels_array()?
443            .ok()?
444            .get(self.index as usize)?
445            .get();
446        Some(name_id).filter(|name_id| name_id.to_u16() != 0xFFFF)
447    }
448
449    /// Returns the palette index in the table.
450    pub fn index(&self) -> u32 {
451        self.index
452    }
453}
454
455/// Provides access to the color palettes in a font.
456#[derive(Clone, Copy)]
457pub struct Palettes<'a> {
458    cpal: Option<&'a Cpal<'a>>,
459}
460
461impl<'a> Palettes<'a> {
462    pub(crate) fn new(font: &'a Font) -> Self {
463        Self { cpal: font.cpal() }
464    }
465
466    /// Returns the number of palettes, or zero if the font has no `CPAL`
467    /// table.
468    pub fn len(&self) -> u32 {
469        self.cpal.map_or(0, |cpal| cpal.num_palettes() as u32)
470    }
471
472    /// Returns true if there are no palettes.
473    pub fn is_empty(&self) -> bool {
474        self.len() == 0
475    }
476
477    /// Returns the palette at `index`. Index zero is the default palette.
478    pub fn get(&self, index: u32) -> Option<Palette<'a>> {
479        let cpal = self.cpal?;
480        let start = usize::from(cpal.color_record_indices().get(index as usize)?.get());
481        let count = usize::from(cpal.num_palette_entries());
482        let colors = cpal
483            .color_records_array()?
484            .ok()?
485            .get(start..start + count)?;
486        Some(Palette {
487            cpal,
488            colors,
489            index,
490        })
491    }
492
493    /// Returns an iterator over the palettes, in index order.
494    pub fn iter(&self) -> impl Iterator<Item = Palette<'a>> + 'a {
495        let this = *self;
496        (0..this.len()).filter_map(move |index| this.get(index))
497    }
498
499    /// Returns the name ID for the label of the color at `color_index`, if
500    /// the table has one.
501    ///
502    /// The label applies to that color in every palette.
503    pub fn color_label(&self, color_index: u32) -> Option<NameId> {
504        let name_id = self
505            .cpal?
506            .palette_entry_labels_array()?
507            .ok()?
508            .get(color_index as usize)?
509            .get();
510        Some(name_id).filter(|name_id| name_id.to_u16() != 0xFFFF)
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use super::{
517        test_glyph_defs::{COLORED_CIRCLES_V0, COLORED_CIRCLES_V1, PAINTCOLRGLYPH_CYCLE},
518        *,
519    };
520    use crate::{model::glyph::outline::ControlBoundsSink, FontRef, TableProvider};
521    use alloc::vec::Vec;
522
523    fn font(data: &[u8]) -> Font {
524        Font::new(data.to_vec(), 0).unwrap()
525    }
526
527    fn map(font: &Font, ch: char) -> GlyphId {
528        font.charmap().map_unicode(ch).unwrap()
529    }
530
531    #[derive(Default)]
532    struct DummySink;
533
534    impl PaintSink for DummySink {
535        fn push_transform(&mut self, _transform: Matrix<f32>) {}
536        fn pop_transform(&mut self) {}
537        fn push_clip_glyph(&mut self, _glyph: GlyphId) {}
538        fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {}
539        fn pop_clip(&mut self) {}
540        fn fill(&mut self, _brush: Brush) {}
541        fn push_layer(&mut self, _composite_mode: CompositeMode) {}
542        fn pop_layer(&mut self) {}
543    }
544
545    #[test]
546    fn has_colrv1_glyph() {
547        let font = font(font_test_data::COLRV0V1_VARIABLE);
548        let glyphs = font.color_glyphs();
549        assert_eq!(glyphs.version(), Some(1));
550        let v1 = |ch| glyphs.get_with_version(map(&font, ch), ColorGlyphVersion::ColrV1);
551        assert!(v1(COLORED_CIRCLES_V0[0]).is_none());
552        assert!(v1(COLORED_CIRCLES_V1[0]).is_some());
553        assert_eq!(
554            glyphs
555                .get(map(&font, COLORED_CIRCLES_V0[0]))
556                .unwrap()
557                .version(),
558            ColorGlyphVersion::ColrV0
559        );
560    }
561
562    #[test]
563    fn a_font_without_colr_has_no_color_glyphs() {
564        let font = font(font_test_data::VAZIRMATN_VAR);
565        let glyphs = font.color_glyphs();
566        assert!(glyphs.is_empty());
567        assert_eq!(glyphs.version(), None);
568        assert!(glyphs.get(GlyphId::new(1)).is_none());
569        assert!(font.palettes().is_empty());
570    }
571
572    #[test]
573    fn paintcolrglyph_cycle_fails() {
574        let font = font(font_test_data::COLRV0V1_VARIABLE);
575        let glyph = font
576            .color_glyphs()
577            .get_with_version(
578                map(&font, PAINTCOLRGLYPH_CYCLE[0]),
579                ColorGlyphVersion::ColrV1,
580            )
581            .unwrap();
582        assert_eq!(glyph.paint(&mut DummySink), Err(Error::LimitExceeded));
583    }
584
585    #[test]
586    fn paint_glyph2_preserves_boundary_glyph_ids() {
587        use font_test_data::bebuffer::BeBuffer;
588
589        #[derive(Default)]
590        struct Sink {
591            glyphs: Vec<GlyphId>,
592            fills: Vec<(u16, f32)>,
593            clip_depth: usize,
594        }
595        impl PaintSink for Sink {
596            fn push_transform(&mut self, _: Matrix<f32>) {
597                panic!("unexpected transform");
598            }
599            fn pop_transform(&mut self) {
600                panic!("unexpected transform");
601            }
602            fn push_clip_glyph(&mut self, glyph: GlyphId) {
603                self.glyphs.push(glyph);
604                self.clip_depth += 1;
605            }
606            fn push_clip_box(&mut self, _: BoundingBox<f32>) {
607                panic!("unexpected clip box");
608            }
609            fn pop_clip(&mut self) {
610                self.clip_depth = self.clip_depth.checked_sub(1).unwrap();
611            }
612            fn fill(&mut self, brush: Brush) {
613                assert_eq!(self.clip_depth, 1);
614                match brush {
615                    Brush::Solid {
616                        palette_index,
617                        alpha,
618                    } => self.fills.push((palette_index, alpha)),
619                    _ => panic!("unexpected brush"),
620                }
621            }
622            fn push_layer(&mut self, _: CompositeMode) {
623                panic!("unexpected layer");
624            }
625            fn pop_layer(&mut self) {
626                panic!("unexpected layer");
627            }
628        }
629
630        for gid in [65535u32, 65536, 0xffffff] {
631            let mut colr = font_test_data::colr::paint_glyph2_colr().to_vec();
632            let paint_offset = 34 + 10;
633            colr[paint_offset + 4..paint_offset + 7].copy_from_slice(&gid.to_be_bytes()[1..]);
634            let bytes = BeBuffer::new()
635                .push(0x00010000u32)
636                .push(1u16)
637                .push(16u16)
638                .push(0u16)
639                .push(0u16)
640                .extend(*b"COLR")
641                .push(0u32)
642                .push(28u32)
643                .push(colr.len() as u32)
644                .extend(colr.iter().copied());
645            let font = font(&bytes);
646            let colors = font.color_glyphs();
647            let glyph = colors
648                .get_with_version(GlyphId::new(42), ColorGlyphVersion::ColrV1)
649                .unwrap();
650            assert!(colors
651                .get_with_version(GlyphId::new(43), ColorGlyphVersion::ColrV1)
652                .is_none());
653            let mut sink = Sink::default();
654            for _ in 0..2 {
655                glyph.paint(&mut sink).unwrap();
656                assert_eq!(sink.clip_depth, 0);
657            }
658            assert_eq!(sink.glyphs, [GlyphId::new(gid); 2]);
659            assert_eq!(sink.fills, [(7, 1.0); 2]);
660        }
661    }
662
663    #[test]
664    fn no_cliplist_paints() {
665        let font = font(font_test_data::COLRV1_NO_CLIPLIST);
666        let glyph = font
667            .color_glyphs()
668            .get_with_version(GlyphId::new(1), ColorGlyphVersion::ColrV1)
669            .unwrap();
670        assert!(glyph.clip_box().is_none());
671        assert!(glyph.paint(&mut DummySink).is_ok());
672    }
673
674    #[test]
675    fn colrv0_glyph_has_no_clip_box() {
676        let font = font(font_test_data::COLRV0V1);
677        let glyph = font
678            .color_glyphs()
679            .get_with_version(GlyphId::new(168), ColorGlyphVersion::ColrV0)
680            .unwrap();
681        assert!(glyph.clip_box().is_none());
682    }
683
684    #[test]
685    fn extents_are_the_clip_box_where_there_is_one() {
686        let font = font(font_test_data::COLRV0V1_VARIABLE);
687        let glyphs = font.color_glyphs();
688        let mut checked = 0;
689        for ch in super::test_glyph_defs::CLIPBOX {
690            let glyph = glyphs.get(map(&font, *ch)).unwrap();
691            if let Some(clip_box) = glyph.clip_box() {
692                assert_eq!(glyph.extents().unwrap(), GlyphExtents::from(clip_box));
693                checked += 1;
694            }
695        }
696        assert!(checked > 0);
697    }
698
699    #[test]
700    fn extents_of_layered_outlines_are_their_union() {
701        let font = font(font_test_data::COLRV0V1_VARIABLE);
702        let gid = map(&font, COLORED_CIRCLES_V0[0]);
703        let glyph = font.color_glyphs().get(gid).unwrap();
704        assert_eq!(glyph.version(), ColorGlyphVersion::ColrV0);
705        // The same union, taken by hand from the layers' outlines.
706        let colr = FontRef::new(font_test_data::COLRV0V1_VARIABLE)
707            .unwrap()
708            .colr()
709            .unwrap();
710        let outlines = font.outline_glyphs();
711        let mut pen = ControlBoundsSink::new();
712        for layer in colr.v0_base_glyph(gid).unwrap() {
713            let (layer_glyph, _) = colr.v0_layer(layer).unwrap();
714            let outline = outlines.get(layer_glyph.into()).unwrap();
715            outline
716                .draw(
717                    &crate::model::glyph::outline::DrawOptions::default(),
718                    crate::model::glyph::outline::DrawMemory::Scratch,
719                    &mut pen,
720                )
721                .unwrap();
722        }
723        assert_eq!(
724            glyph.extents().unwrap(),
725            GlyphExtents::from(pen.bounding_box().unwrap())
726        );
727    }
728
729    #[test]
730    fn extents_follow_a_translation() {
731        let font = font(font_test_data::COLRV0V1_VARIABLE);
732        let glyphs = font.color_glyphs();
733        // Each translate test glyph composites one fill over a copy of itself
734        // moved by these offsets, so its box is the union of the two.
735        let offsets = [
736            (0.0, 0.0),
737            (0.0, 100.0),
738            (0.0, -100.0),
739            (100.0, 0.0),
740            (-100.0, 0.0),
741            (200.0, 200.0),
742            (-200.0, -200.0),
743        ];
744        let extents: Vec<_> = super::test_glyph_defs::PAINT_TRANSLATE
745            .iter()
746            .map(|ch| glyphs.get(map(&font, *ch)).unwrap().extents().unwrap())
747            .collect();
748        assert_eq!(extents.len(), offsets.len());
749        let base = extents[0];
750        for (extents, (dx, dy)) in extents.iter().zip(offsets) {
751            let expected = GlyphExtents {
752                x_bearing: base.x_bearing.min(base.x_bearing + dx),
753                y_bearing: base.y_bearing.max(base.y_bearing + dy),
754                width: base.width + dx.abs(),
755                height: base.height + dy.abs(),
756            };
757            assert_eq!(*extents, expected);
758        }
759    }
760
761    #[test]
762    fn version_0_glyphs_have_extents() {
763        let font = font(font_test_data::COLRV0V1_VARIABLE);
764        let glyphs = font.color_glyphs();
765        let outlines = font.outline_glyphs();
766        let gid = map(&font, COLORED_CIRCLES_V0[0]);
767        assert!(glyphs.extents(gid, outlines).is_some());
768        // A glyph the table does not paint has no extents.
769        assert!(glyphs.extents(GlyphId::new(0), outlines).is_none());
770    }
771
772    #[test]
773    fn palettes() {
774        let font = font(font_test_data::COLRV0V1);
775        let palettes = font.palettes();
776        assert_eq!(palettes.len(), 3);
777        let indices: Vec<_> = palettes.iter().map(|palette| palette.index()).collect();
778        assert_eq!(indices, [0, 1, 2]);
779
780        let first = palettes.get(0).unwrap();
781        assert_eq!(first.colors().len(), 14);
782        assert_eq!(
783            first.colors().first(),
784            Some(&Color {
785                blue: 0,
786                green: 0,
787                red: 255,
788                alpha: 255
789            })
790        );
791        assert_eq!(first.colors().get(14), None);
792        assert_eq!(first.palette_type(), Some(PaletteType::empty()));
793        assert_eq!(first.index(), 0);
794
795        let second = palettes.get(1).unwrap();
796        assert_eq!(
797            second.colors().first(),
798            Some(&Color {
799                blue: 74,
800                green: 41,
801                red: 42,
802                alpha: 255
803            })
804        );
805        assert_eq!(
806            second.palette_type(),
807            Some(PaletteType::USABLE_WITH_DARK_BACKGROUND)
808        );
809
810        let third = palettes.get(2).unwrap();
811        assert_eq!(
812            third.colors().first(),
813            Some(&Color {
814                blue: 24,
815                green: 113,
816                red: 252,
817                alpha: 255
818            })
819        );
820        assert_eq!(
821            third.palette_type(),
822            Some(PaletteType::USABLE_WITH_LIGHT_BACKGROUND)
823        );
824
825        assert!(palettes.get(3).is_none());
826    }
827
828    /// The Ecuador and El Salvador flags in Noto Color Emoji have the most
829    /// complex paint graphs, visiting about 6700 paints. Painting them checks
830    /// that the traversal budget is large enough.
831    #[test]
832    fn paint_flags_requiring_increased_node_budget() {
833        let font = font(font_test_data::NOTO_COLOR_EMOJI_FLAGS);
834        let colr = FontRef::new(font_test_data::NOTO_COLOR_EMOJI_FLAGS)
835            .unwrap()
836            .colr()
837            .unwrap();
838        let glyph_ids: Vec<_> = colr
839            .base_glyph_list()
840            .unwrap()
841            .unwrap()
842            .base_glyph_paint_records()
843            .iter()
844            .map(|record| GlyphId::from(record.glyph_id()))
845            .collect();
846        assert!(!glyph_ids.is_empty());
847        for gid in glyph_ids {
848            font.color_glyphs()
849                .get_with_version(gid, ColorGlyphVersion::ColrV1)
850                .unwrap_or_else(|| panic!("no COLRv1 glyph for {gid}"))
851                .paint(&mut DummySink)
852                .unwrap_or_else(|e| panic!("failed to paint {gid}: {e}"));
853        }
854    }
855}