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