xref: /aosp_15_r20/external/cronet/third_party/rust/chromium_crates_io/vendor/skrifa-0.15.5/src/color/mod.rs (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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::{scale::*, instance::{Size, Location}, color::{ColorGlyphFormat, ColorPainter, PaintError}, MetadataProvider};
9 //! # use read_fonts::types::GlyphId;
10 //! # fn get_colr_bb(font: read_fonts::FontRef, color_painter_impl : &mut impl ColorPainter, glyph_id : GlyphId, size: Size) -> Result<(), PaintError> {
11 //! match font.color_glyphs()
12 //!       .get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
13 //!       .expect("Glyph not found.")
14 //!       .bounding_box(&Location::default(), size)
15 //! {
16 //!   Some(bounding_box) => {
17 //!       println!("Bounding box is {:?}", bounding_box);
18 //!   }
19 //!   None => {
20 //!       println!("Glyph has no clip box.");
21 //!   }
22 //! }
23 //! # Ok(())
24 //! # }
25 //! ```
26 //!
27 //! ## Paint a COLRv1 glyph given a font, and a glyph id and a [`ColorPainter`] implementation:
28 //! ```
29 //! # use core::result::Result;
30 //! # use skrifa::{scale::*, instance::{Size, Location}, color::{ColorGlyphFormat, ColorPainter, PaintError}, MetadataProvider};
31 //! # use read_fonts::types::GlyphId;
32 //! # fn paint_colr(font: read_fonts::FontRef, color_painter_impl : &mut impl ColorPainter, glyph_id : GlyphId) -> Result<(), PaintError> {
33 //! let color_glyph = font.color_glyphs()
34 //!                     .get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
35 //!                     .expect("Glyph not found");
36 //! color_glyph.paint(&Location::default(), color_painter_impl)
37 //! # }
38 //! ```
39 //!
40 mod instance;
41 mod transform;
42 mod traversal;
43 
44 #[cfg(test)]
45 mod traversal_tests;
46 
47 use raw::tables::colr;
48 #[cfg(test)]
49 use serde::{Deserialize, Serialize};
50 
51 pub use read_fonts::tables::colr::{CompositeMode, Extend};
52 
53 use read_fonts::{
54     types::{BoundingBox, GlyphId, Point},
55     ReadError, TableProvider,
56 };
57 
58 use std::{collections::HashSet, fmt::Debug, ops::Range};
59 
60 use traversal::{
61     get_clipbox_font_units, traverse_v0_range, traverse_with_callbacks, NonRandomHasherState,
62 };
63 
64 pub use transform::Transform;
65 
66 use crate::prelude::{LocationRef, Size};
67 
68 use self::instance::{resolve_paint, PaintId};
69 
70 /// An error during drawing a COLR glyph.
71 ///
72 /// This covers inconsistencies in the COLRv1 paint graph as well as downstream
73 /// parse errors from read-fonts.
74 #[derive(Debug, Clone)]
75 pub enum PaintError {
76     ParseError(ReadError),
77     GlyphNotFound(GlyphId),
78     PaintCycleDetected,
79 }
80 
81 impl std::fmt::Display for PaintError {
fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result82     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83         match self {
84             PaintError::ParseError(read_error) => {
85                 write!(f, "Error parsing font data: {read_error}")
86             }
87             PaintError::GlyphNotFound(glyph_id) => {
88                 write!(f, "No COLRv1 glyph found for glyph id: {glyph_id}")
89             }
90             PaintError::PaintCycleDetected => write!(f, "Paint cycle detected in COLRv1 glyph."),
91         }
92     }
93 }
94 
95 impl From<ReadError> for PaintError {
from(value: ReadError) -> Self96     fn from(value: ReadError) -> Self {
97         PaintError::ParseError(value)
98     }
99 }
100 
101 /// A color stop of a gradient.
102 ///
103 /// All gradient callbacks of [`ColorPainter`] normalize color stops to be in the range of 0
104 /// to 1.
105 #[derive(Clone, PartialEq, Debug, Default)]
106 #[cfg_attr(test, derive(Serialize, Deserialize))]
107 // This repr(C) is required so that C-side FFI's
108 // are able to cast the ColorStop slice to a C-side array pointer.
109 #[repr(C)]
110 pub struct ColorStop {
111     pub offset: f32,
112     /// Specifies a color from the `CPAL` table.
113     pub palette_index: u16,
114     /// Additional alpha value, to be multiplied with the color above before use.
115     pub alpha: f32,
116 }
117 
118 // Design considerations for choosing a slice of ColorStops as `color_stop`
119 // type: In principle, a local `Vec<ColorStop>` allocation would not required if
120 // we're willing to walk the `ResolvedColorStop` iterator to find the minimum
121 // and maximum color stops.  Then we could scale the color stops based on the
122 // minimum and maximum. But performing the min/max search would require
123 // re-applying the deltas at least once, after which we would pass the scaled
124 // stops to client side and have the client sort the collected items once
125 // again. If we do want to pre-ort them, and still use use an
126 // `Iterator<Item=ColorStop>`` instead as the `color_stops` field, then we would
127 // need a Fontations-side allocations to sort, and an extra allocation on the
128 // client side to `.collect()` from the provided iterator before passing it to
129 // drawing API.
130 //
131 /// A fill type of a COLRv1 glyph (solid fill or various gradient types).
132 ///
133 /// The client receives the information about the fill type in the
134 /// [`fill``](ColorPainter::fill) callback of the [`ColorPainter`] trait.
135 #[derive(Debug, PartialEq)]
136 pub enum Brush<'a> {
137     /// A solid fill with the color specified by `palette_index`. The respective
138     /// color from the CPAL table then needs to be multiplied with `alpha`.
139     Solid { palette_index: u16, alpha: f32 },
140     /// A linear gradient, normalized from the P0, P1 and P2 representation in
141     /// the COLRv1 table to a linear gradient between two points `p0` and
142     /// `p1`. If there is only one color stop, the client should draw a solid
143     /// fill with that color. The `color_stops` are normalized to the range from
144     /// 0 to 1.
145     LinearGradient {
146         p0: Point<f32>,
147         p1: Point<f32>,
148         color_stops: &'a [ColorStop],
149         extend: Extend,
150     },
151     /// A radial gradient, with color stops normalized to the range of 0 to
152     /// 1. Caution: This normalization can mean that negative radii occur. It is
153     /// the client's responsibility to truncate the color line at the 0
154     /// position, interpolating between `r0` and `r1` and compute an
155     /// interpolated color at that position.
156     RadialGradient {
157         c0: Point<f32>,
158         r0: f32,
159         c1: Point<f32>,
160         r1: f32,
161         color_stops: &'a [ColorStop],
162         extend: Extend,
163     },
164     /// A sweep gradient, also called conical gradient. The color stops are
165     /// normalized to the range from 0 to 1 and the returned angles are to be
166     /// interpreted in _clockwise_ direction (swapped from the meaning in the
167     /// font file).  The stop normalization may mean that the angles may be
168     /// larger or smaller than the range of 0 to 360. Note that only the range
169     /// from 0 to 360 degrees is to be drawn, see
170     /// <https://learn.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients>.
171     SweepGradient {
172         c0: Point<f32>,
173         start_angle: f32,
174         end_angle: f32,
175         color_stops: &'a [ColorStop],
176         extend: Extend,
177     },
178 }
179 
180 /// Signals success of request to draw a COLRv1 sub glyph from cache.
181 ///
182 /// Result of [`paint_cached_color_glyph`](ColorPainter::paint_cached_color_glyph)
183 /// through which the client signals whether a COLRv1 glyph referenced by
184 /// another COLRv1 glyph was drawn from cache or whether the glyph's subgraph
185 /// should be traversed by the skria side COLRv1 implementation.
186 pub enum PaintCachedColorGlyph {
187     /// The specified COLRv1 glyph has been successfully painted client side.
188     Ok,
189     /// The client does not implement drawing COLRv1 glyphs from cache and the
190     /// Fontations side COLRv1 implementation is asked to traverse the
191     /// respective PaintColorGlyph sub graph.
192     Unimplemented,
193 }
194 
195 /// A group of required painting callbacks to be provided by the client.
196 ///
197 /// Each callback is executing a particular drawing or canvas transformation
198 /// operation. The trait's callback functions are invoked when
199 /// [`paint`](ColorGlyph::paint) is called with a [`ColorPainter`] trait
200 /// object. The documentation for each function describes what actions are to be
201 /// executed using the client side 2D graphics API, usually by performing some
202 /// kind of canvas operation.
203 pub trait ColorPainter {
204     /// Push the specified transform by concatenating it to the current
205     /// transformation matrix.
push_transform(&mut self, transform: Transform)206     fn push_transform(&mut self, transform: Transform);
207     /// Restore the transformation matrix to the state before the previous
208     /// [`push_transform`](ColorPainter::push_transform) call.
pop_transform(&mut self)209     fn pop_transform(&mut self);
210 
211     /// Apply a clip path in the shape of glyph specified by `glyph_id`.
push_clip_glyph(&mut self, glyph_id: GlyphId)212     fn push_clip_glyph(&mut self, glyph_id: GlyphId);
213     /// Apply a clip rectangle specified by `clip_rect`.
push_clip_box(&mut self, clip_box: BoundingBox<f32>)214     fn push_clip_box(&mut self, clip_box: BoundingBox<f32>);
215     /// Restore the clip state to the state before a previous
216     /// [`push_clip_glyph`](ColorPainter::push_clip_glyph) or
217     /// [`push_clip_box`](ColorPainter::push_clip_box) call.
pop_clip(&mut self)218     fn pop_clip(&mut self);
219 
220     /// Fill the current clip area with the specified gradient fill.
fill(&mut self, brush: Brush<'_>)221     fn fill(&mut self, brush: Brush<'_>);
222 
223     /// Combined clip and fill operation.
224     ///
225     /// Apply the clip path determined by the specified `glyph_id`, then fill it
226     /// with the specified [`brush`](Brush), applying the `_brush_transform`
227     /// transformation matrix to the brush. The default implementation works
228     /// based on existing methods in this trait. It is recommended for clients
229     /// to override the default implementaition with a custom combined clip and
230     /// fill operation. In this way overriding likely results in performance
231     /// gains depending on performance characteristics of the 2D graphics stack
232     /// that these calls are mapped to.
fill_glyph( &mut self, glyph_id: GlyphId, brush_transform: Option<Transform>, brush: Brush<'_>, )233     fn fill_glyph(
234         &mut self,
235         glyph_id: GlyphId,
236         brush_transform: Option<Transform>,
237         brush: Brush<'_>,
238     ) {
239         self.push_clip_glyph(glyph_id);
240         if let Some(wrap_in_transform) = brush_transform {
241             self.push_transform(wrap_in_transform);
242             self.fill(brush);
243             self.pop_transform();
244         } else {
245             self.fill(brush);
246         }
247         self.pop_clip();
248     }
249 
250     /// Optionally implement this method: Draw an unscaled COLRv1 glyph given
251     /// the current transformation matrix (as accumulated by
252     /// [`push_transform`](ColorPainter::push_transform) calls).
paint_cached_color_glyph( &mut self, _glyph: GlyphId, ) -> Result<PaintCachedColorGlyph, PaintError>253     fn paint_cached_color_glyph(
254         &mut self,
255         _glyph: GlyphId,
256     ) -> Result<PaintCachedColorGlyph, PaintError> {
257         Ok(PaintCachedColorGlyph::Unimplemented)
258     }
259 
260     /// Open a new layer, and merge the layer down using `composite_mode` when
261     /// [`pop_layer`](ColorPainter::pop_layer) is called, signalling that this layer is done drawing.
push_layer(&mut self, composite_mode: CompositeMode)262     fn push_layer(&mut self, composite_mode: CompositeMode);
pop_layer(&mut self)263     fn pop_layer(&mut self);
264 }
265 
266 /// Distinguishes available color glyph formats.
267 #[derive(Clone, Copy)]
268 pub enum ColorGlyphFormat {
269     ColrV0,
270     ColrV1,
271 }
272 
273 /// A representation of a color glyph that can be painted through a sequence of [`ColorPainter`] callbacks.
274 #[derive(Clone)]
275 pub struct ColorGlyph<'a> {
276     colr: colr::Colr<'a>,
277     root_paint_ref: ColorGlyphRoot<'a>,
278 }
279 
280 #[derive(Clone)]
281 enum ColorGlyphRoot<'a> {
282     V0Range(Range<usize>),
283     V1Paint(colr::Paint<'a>, PaintId, GlyphId, Result<u16, ReadError>),
284 }
285 
286 impl<'a> ColorGlyph<'a> {
287     /// Returns the version of the color table from which this outline was
288     /// selected.
format(&self) -> ColorGlyphFormat289     pub fn format(&self) -> ColorGlyphFormat {
290         match &self.root_paint_ref {
291             ColorGlyphRoot::V0Range(_) => ColorGlyphFormat::ColrV0,
292             ColorGlyphRoot::V1Paint(..) => ColorGlyphFormat::ColrV1,
293         }
294     }
295 
296     /// Returns the bounding box. For COLRv1 glyphs, this is clipbox of the
297     /// specified COLRv1 glyph, or `None` if there is
298     /// none for the particular glyph.  The `size` argument can optionally be used
299     /// to scale the bounding box to a particular font size. `location` allows
300     /// specifycing a variation instance.
bounding_box( &self, location: impl Into<LocationRef<'a>>, size: Size, ) -> Option<BoundingBox<f32>>301     pub fn bounding_box(
302         &self,
303         location: impl Into<LocationRef<'a>>,
304         size: Size,
305     ) -> Option<BoundingBox<f32>> {
306         let instance = instance::ColrInstance::new(self.colr.clone(), location.into().coords());
307 
308         match &self.root_paint_ref {
309             ColorGlyphRoot::V1Paint(_paint, _paint_id, glyph_id, upem) => {
310                 let resolved_bounding_box = get_clipbox_font_units(&instance, *glyph_id).ok()?;
311                 resolved_bounding_box.map(|bounding_box| {
312                     let scale_factor = size.linear_scale((*upem).clone().unwrap_or(0));
313                     bounding_box.scale(scale_factor)
314                 })
315             }
316             _ => todo!(),
317         }
318     }
319 
320     /// Evaluates the paint graph at the specified location in variation space
321     /// and emits the results to the given painter.
322     ///
323     ///
324     /// For a COLRv1 glyph, traverses the COLRv1 paint graph and invokes drawing callbacks on a
325     /// specified [`ColorPainter`] trait object.  The traversal operates in font
326     /// units and will call `ColorPainter` methods with font unit values. This
327     /// means, if you want to draw a COLRv1 glyph at a particular font size, the
328     /// canvas needs to have a transformation matrix applied so that it scales down
329     /// the drawing operations to `font_size / upem`.
330     ///
331     /// # Arguments
332     ///
333     /// * `glyph_id` the `GlyphId` to be drawn.
334     /// * `location` coordinates for specifying a variation instance. This can be empty.
335     /// * `painter` a client-provided [`ColorPainter`] implementation receiving drawing callbacks.
336     ///
paint( &self, location: impl Into<LocationRef<'a>>, painter: &mut impl ColorPainter, ) -> Result<(), PaintError>337     pub fn paint(
338         &self,
339         location: impl Into<LocationRef<'a>>,
340         painter: &mut impl ColorPainter,
341     ) -> Result<(), PaintError> {
342         let instance = instance::ColrInstance::new(self.colr.clone(), location.into().coords());
343         match &self.root_paint_ref {
344             ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, _) => {
345                 let clipbox = get_clipbox_font_units(&instance, *glyph_id)?;
346 
347                 if let Some(rect) = clipbox {
348                     painter.push_clip_box(rect);
349                 }
350 
351                 let mut visited_set = HashSet::with_hasher(NonRandomHasherState);
352                 visited_set.insert(*paint_id);
353                 traverse_with_callbacks(
354                     &resolve_paint(&instance, paint)?,
355                     &instance,
356                     painter,
357                     &mut visited_set,
358                 )?;
359 
360                 if clipbox.is_some() {
361                     painter.pop_clip();
362                 }
363                 Ok(())
364             }
365             ColorGlyphRoot::V0Range(range) => {
366                 traverse_v0_range(range, &instance, painter)?;
367                 Ok(())
368             }
369         }
370     }
371 }
372 
373 /// Collection of color glyphs.
374 #[derive(Clone)]
375 pub struct ColorGlyphCollection<'a> {
376     colr: Option<colr::Colr<'a>>,
377     upem: Result<u16, ReadError>,
378 }
379 
380 impl<'a> ColorGlyphCollection<'a> {
381     /// Creates a new collection of paintable color glyphs for the given font.
new(font: &impl TableProvider<'a>) -> Self382     pub fn new(font: &impl TableProvider<'a>) -> Self {
383         let colr = font.colr().ok();
384         let upem = font.head().map(|h| h.units_per_em());
385 
386         Self { colr, upem }
387     }
388 
389     /// Returns the color glyph representation for the given glyph identifier,
390     /// given a specific format.
get_with_format( &self, glyph_id: GlyphId, glyph_format: ColorGlyphFormat, ) -> Option<ColorGlyph<'a>>391     pub fn get_with_format(
392         &self,
393         glyph_id: GlyphId,
394         glyph_format: ColorGlyphFormat,
395     ) -> Option<ColorGlyph<'a>> {
396         let colr = self.colr.clone()?;
397 
398         let root_paint_ref = match glyph_format {
399             ColorGlyphFormat::ColrV0 => {
400                 let layer_range = colr.v0_base_glyph(glyph_id).ok()??;
401                 ColorGlyphRoot::V0Range(layer_range)
402             }
403             ColorGlyphFormat::ColrV1 => {
404                 let (paint, paint_id) = colr.v1_base_glyph(glyph_id).ok()??;
405                 ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, self.upem.clone())
406             }
407         };
408         Some(ColorGlyph {
409             colr,
410             root_paint_ref,
411         })
412     }
413 
414     /// Returns a color glyph representation for the given glyph identifier if
415     /// available, preferring a COLRv1 representation over a COLRv0
416     /// representation.
get(&self, glyph_id: GlyphId) -> Option<ColorGlyph<'a>>417     pub fn get(&self, glyph_id: GlyphId) -> Option<ColorGlyph<'a>> {
418         self.get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
419             .or_else(|| self.get_with_format(glyph_id, ColorGlyphFormat::ColrV0))
420     }
421 }
422 
423 #[cfg(test)]
424 mod tests {
425 
426     use crate::{
427         color::traversal_tests::test_glyph_defs::PAINTCOLRGLYPH_CYCLE, prelude::LocationRef,
428         MetadataProvider,
429     };
430 
431     use read_fonts::{types::BoundingBox, FontRef};
432 
433     use super::{Brush, ColorPainter, CompositeMode, GlyphId, Transform};
434     use crate::color::traversal_tests::test_glyph_defs::{COLORED_CIRCLES_V0, COLORED_CIRCLES_V1};
435 
436     #[test]
has_colrv1_glyph_test()437     fn has_colrv1_glyph_test() {
438         let colr_font = font_test_data::COLRV0V1_VARIABLE;
439         let font = FontRef::new(colr_font).unwrap();
440         let get_colrv1_glyph = |codepoint: &[char]| {
441             font.charmap().map(codepoint[0]).and_then(|glyph_id| {
442                 font.color_glyphs()
443                     .get_with_format(glyph_id, crate::color::ColorGlyphFormat::ColrV1)
444             })
445         };
446 
447         assert!(get_colrv1_glyph(COLORED_CIRCLES_V0).is_none());
448         assert!(get_colrv1_glyph(COLORED_CIRCLES_V1).is_some());
449     }
450     struct DummyColorPainter {}
451 
452     impl DummyColorPainter {
new() -> Self453         pub fn new() -> Self {
454             Self {}
455         }
456     }
457 
458     impl Default for DummyColorPainter {
default() -> Self459         fn default() -> Self {
460             Self::new()
461         }
462     }
463 
464     impl ColorPainter for DummyColorPainter {
push_transform(&mut self, _transform: Transform)465         fn push_transform(&mut self, _transform: Transform) {}
pop_transform(&mut self)466         fn pop_transform(&mut self) {}
push_clip_glyph(&mut self, _glyph: GlyphId)467         fn push_clip_glyph(&mut self, _glyph: GlyphId) {}
push_clip_box(&mut self, _clip_box: BoundingBox<f32>)468         fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {}
pop_clip(&mut self)469         fn pop_clip(&mut self) {}
fill(&mut self, _brush: Brush)470         fn fill(&mut self, _brush: Brush) {}
push_layer(&mut self, _composite_mode: CompositeMode)471         fn push_layer(&mut self, _composite_mode: CompositeMode) {}
pop_layer(&mut self)472         fn pop_layer(&mut self) {}
473     }
474 
475     #[test]
paintcolrglyph_cycle_test()476     fn paintcolrglyph_cycle_test() {
477         let colr_font = font_test_data::COLRV0V1_VARIABLE;
478         let font = FontRef::new(colr_font).unwrap();
479         let cycle_glyph_id = font.charmap().map(PAINTCOLRGLYPH_CYCLE[0]).unwrap();
480         let colrv1_glyph = font
481             .color_glyphs()
482             .get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
483 
484         assert!(colrv1_glyph.is_some());
485         let mut color_painter = DummyColorPainter::new();
486 
487         let result = colrv1_glyph
488             .unwrap()
489             .paint(LocationRef::default(), &mut color_painter);
490         // Expected to fail with an error as the glyph contains a paint cycle.
491         assert!(result.is_err());
492     }
493 }
494