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