1use super::{
4 decycler::{Decycler, DecyclerError},
5 instance::{resolve_clip_box, ColrInstance, MaybeBrush, ResolvedColorStop, ResolvedPaint},
6 Brush, ColorStop, Error, PaintCachedColorGlyph, PaintSink,
7};
8use crate::{
9 limits::MAX_RECURSION_DEPTH,
10 tables::colr::{CompositeMode, Paint},
11 types::{BoundingBox, GlyphId},
12};
13use core::ops::Range;
14
15#[cfg(feature = "libm")]
16#[allow(unused_imports)]
17use core_maths::CoreFloat;
18
19pub(super) type PaintDecycler = Decycler<usize, MAX_RECURSION_DEPTH>;
23
24const MAX_INLINE_COLOR_STOPS: usize = 32;
31
32pub(super) type ColorStopVec = smallvec::SmallVec<[ColorStop; MAX_INLINE_COLOR_STOPS]>;
33
34impl From<DecyclerError> for Error {
35 fn from(_: DecyclerError) -> Self {
38 Self::LimitExceeded
39 }
40}
41
42const MAX_NODES: u32 = 16384;
51
52pub(super) fn get_clipbox_font_units(
53 colr_instance: &ColrInstance,
54 glyph: GlyphId,
55) -> Option<BoundingBox<f32>> {
56 let maybe_clipbox = (*colr_instance).v1_clip_box(glyph)?;
57 Some(resolve_clip_box(colr_instance, &maybe_clipbox))
58}
59
60impl From<ResolvedColorStop> for ColorStop {
61 fn from(resolved_stop: ResolvedColorStop) -> Self {
62 ColorStop {
63 offset: resolved_stop.offset,
64 alpha: resolved_stop.alpha,
65 palette_index: resolved_stop.palette_index,
66 }
67 }
68}
69
70pub(super) struct TraversalState<'a> {
71 instance: ColrInstance<'a>,
72 sink: &'a mut dyn PaintSink,
73 stops_buf: ColorStopVec,
74 nodes_left: u32,
75}
76
77impl<'a> TraversalState<'a> {
78 pub(super) fn new(instance: ColrInstance<'a>, sink: &'a mut dyn PaintSink) -> Self {
79 Self {
80 instance,
81 sink,
82 stops_buf: ColorStopVec::new(),
83 nodes_left: MAX_NODES,
84 }
85 }
86
87 pub(super) fn resolve_paint(&mut self, paint: &Paint<'a>) -> Result<ResolvedPaint<'a>, Error> {
88 self.nodes_left = self.nodes_left.checked_sub(1).ok_or(Error::LimitExceeded)?;
89 super::instance::resolve_paint(&self.instance, paint)
90 }
91}
92
93pub(super) fn traverse_with_callbacks<'a>(
94 paint: &ResolvedPaint<'a>,
95 state: &mut TraversalState<'a>,
96 decycler: &mut PaintDecycler,
97 recurse_depth: usize,
98) -> Result<(), Error> {
99 if recurse_depth >= MAX_RECURSION_DEPTH {
100 return Err(Error::LimitExceeded);
101 }
102 match paint {
103 ResolvedPaint::ColrLayers { range } => {
104 for layer_index in range.clone() {
105 let (layer_paint, paint_id) = state
107 .instance
108 .v1_layer(layer_index)
109 .ok_or(Error::Malformed)?;
110 let mut cycle_guard = decycler.enter(paint_id)?;
111 traverse_with_callbacks(
112 &state.resolve_paint(&layer_paint)?,
113 state,
114 &mut cycle_guard,
115 recurse_depth + 1,
116 )?;
117 }
118 Ok(())
119 }
120 ResolvedPaint::Solid { .. }
121 | ResolvedPaint::LinearGradient { .. }
122 | ResolvedPaint::RadialGradient { .. }
123 | ResolvedPaint::SweepGradient { .. } => {
124 if let MaybeBrush::Some(brush) =
125 paint.as_brush(&state.instance, &mut state.stops_buf)?
126 {
127 state.sink.fill(brush);
128 }
129 Ok(())
130 }
131 ResolvedPaint::Glyph { glyph, paint } => {
132 let glyph = *glyph;
133 let mut next_paint = state.resolve_paint(paint)?;
136 let mut intermediate_transform = None;
138 while let Some((transform, child_paint)) = next_paint.as_transform() {
139 intermediate_transform = Some(match intermediate_transform {
140 Some(existing_transform) => existing_transform * transform,
141 None => transform,
142 });
143 next_paint = state.resolve_paint(&child_paint)?;
144 }
145 match next_paint.as_brush(&state.instance, &mut state.stops_buf)? {
148 MaybeBrush::Some(brush) => {
149 state.sink.fill_glyph(glyph, intermediate_transform, brush);
150 return Ok(());
151 }
152 MaybeBrush::NonRendering => {
155 return Ok(());
156 }
157 MaybeBrush::None => {}
159 }
160 state.sink.push_clip_glyph(glyph);
163 if let Some(transform) = intermediate_transform {
164 state.sink.push_transform(transform);
165 }
166 let result = traverse_with_callbacks(&next_paint, state, decycler, recurse_depth + 1);
167 if intermediate_transform.is_some() {
168 state.sink.pop_transform();
169 }
170 state.sink.pop_clip();
171 result
172 }
173 ResolvedPaint::ColrGlyph { glyph } => {
174 let glyph = (*glyph).into();
175 match state.instance.v1_base_glyph(glyph) {
176 Some((base_glyph, base_glyph_paint_id)) => {
177 let mut cycle_guard = decycler.enter(base_glyph_paint_id)?;
178 let draw_result = state.sink.paint_cached_color_glyph(glyph)?;
179 match draw_result {
180 PaintCachedColorGlyph::Ok => Ok(()),
181 PaintCachedColorGlyph::Unimplemented => {
182 let clipbox = get_clipbox_font_units(&state.instance, glyph);
183
184 if let Some(rect) = clipbox {
185 state.sink.push_clip_box(rect);
186 }
187 let result = traverse_unresolved_paint(
188 &base_glyph,
189 state,
190 &mut cycle_guard,
191 recurse_depth + 1,
192 );
193 if clipbox.is_some() {
194 state.sink.pop_clip();
195 }
196 result
197 }
198 }
199 }
200 None => Err(Error::MissingResource),
201 }
202 }
203 ResolvedPaint::Transform {
204 paint: next_paint, ..
205 }
206 | ResolvedPaint::Translate {
207 paint: next_paint, ..
208 }
209 | ResolvedPaint::Scale {
210 paint: next_paint, ..
211 }
212 | ResolvedPaint::Rotate {
213 paint: next_paint, ..
214 }
215 | ResolvedPaint::Skew {
216 paint: next_paint, ..
217 } => {
218 state
219 .sink
220 .push_transform(paint.as_transform().ok_or(Error::Malformed)?.0);
221 let result = traverse_unresolved_paint(next_paint, state, decycler, recurse_depth + 1);
222 state.sink.pop_transform();
223 result
224 }
225 ResolvedPaint::Composite {
226 source_paint,
227 mode,
228 backdrop_paint,
229 } => {
230 state.sink.push_layer(CompositeMode::SrcOver);
231 let mut result =
232 traverse_unresolved_paint(backdrop_paint, state, decycler, recurse_depth + 1);
233 if result.is_err() {
234 state.sink.pop_layer_with_mode(CompositeMode::SrcOver);
235 return result;
236 }
237 state.sink.push_layer(*mode);
238 result = traverse_unresolved_paint(source_paint, state, decycler, recurse_depth + 1);
239 state.sink.pop_layer_with_mode(*mode);
240 state.sink.pop_layer_with_mode(CompositeMode::SrcOver);
241 result
242 }
243 }
244}
245
246fn traverse_unresolved_paint<'a>(
247 paint: &Paint<'a>,
248 state: &mut TraversalState<'a>,
249 decycler: &mut PaintDecycler,
250 recurse_depth: usize,
251) -> Result<(), Error> {
252 let resolved_paint = state.resolve_paint(paint)?;
253 traverse_with_callbacks(&resolved_paint, state, decycler, recurse_depth)
254}
255
256pub(super) fn traverse_v0_range(
257 range: &Range<usize>,
258 instance: &ColrInstance,
259 sink: &mut dyn PaintSink,
260) -> Result<(), Error> {
261 for layer_index in range.clone() {
262 let (layer_glyph, palette_index) =
263 (*instance).v0_layer(layer_index).ok_or(Error::Malformed)?;
264 sink.fill_glyph(
265 layer_glyph.into(),
266 None,
267 Brush::Solid {
268 palette_index,
269 alpha: 1.0,
270 },
271 );
272 }
273 Ok(())
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::{
280 model::{
281 font::ColrTables,
282 glyph::color::{
283 test_glyph_defs::CLIPBOX, Brush, ColorGlyphVersion, CompositeMode, Font, PaintSink,
284 },
285 },
286 types::{GlyphId16, Matrix},
287 FontData, FontRead, FontRef, TableProvider,
288 };
289 use alloc::vec::Vec;
290
291 #[test]
292 fn clipbox_test() {
293 let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
294 let model = Font::new(font_test_data::COLRV0V1_VARIABLE.to_vec(), 0).unwrap();
295 let test_glyph_id = model.charmap().map_unicode(CLIPBOX[0]).unwrap();
296 let upem = font.head().unwrap().units_per_em();
297
298 let base_bounding_box = BoundingBox {
299 x_min: 0.0,
300 x_max: upem as f32 / 2.0,
301 y_min: upem as f32 / 2.0,
302 y_max: upem as f32,
303 };
304 const CLIPBOX_SHIFT: f32 = 200.0122;
306
307 macro_rules! test_entry {
308 ($axis:literal, $shift:expr, $field:ident) => {
309 (
310 $axis,
311 $shift,
312 BoundingBox {
313 $field: base_bounding_box.$field + ($shift),
314 ..base_bounding_box
315 },
316 )
317 };
318 }
319
320 let test_data_expectations = [
321 ("", 0.0, base_bounding_box),
322 test_entry!("CLXI", CLIPBOX_SHIFT, x_min),
323 test_entry!("CLXA", -CLIPBOX_SHIFT, x_max),
324 test_entry!("CLYI", CLIPBOX_SHIFT, y_min),
325 test_entry!("CLYA", -CLIPBOX_SHIFT, y_max),
326 ];
327
328 let tables = ColrTables::new(font.colr().unwrap());
329 for axis_test in test_data_expectations {
330 let instance = model
331 .instance_builder()
332 .variations([(axis_test.0, axis_test.1)])
333 .build();
334 let color_instance = ColrInstance::new(&tables, instance.normalized_coords(), &[]);
335 let clip_box = get_clipbox_font_units(&color_instance, test_glyph_id);
336 assert!(clip_box.is_some());
337 assert!(
338 clip_box.unwrap() == axis_test.2,
339 "Clip boxes do not match. Actual: {:?}, expected: {:?}",
340 clip_box.unwrap(),
341 axis_test.2
342 );
343 }
344 }
345
346 struct NopSink;
347
348 impl PaintSink for NopSink {
349 fn push_transform(&mut self, _transform: Matrix<f32>) {}
350
351 fn pop_transform(&mut self) {}
352
353 fn push_clip_glyph(&mut self, _glyph_id: GlyphId) {}
354
355 fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {}
356
357 fn pop_clip(&mut self) {}
358
359 fn fill(&mut self, _brush: Brush<'_>) {}
360
361 fn push_layer(&mut self, _composite_mode: CompositeMode) {}
362
363 fn pop_layer(&mut self) {}
364 }
365
366 #[derive(Default)]
367 struct StackTrackingSink {
368 transform_pushes: usize,
369 transform_pops: usize,
370 clip_pushes: usize,
371 clip_pops: usize,
372 layer_pushes: usize,
373 layer_pops: usize,
374 clip_glyphs: Vec<GlyphId>,
375 filled_glyphs: Vec<GlyphId>,
376 fills: usize,
377 }
378
379 impl PaintSink for StackTrackingSink {
380 fn push_transform(&mut self, _transform: Matrix<f32>) {
381 self.transform_pushes += 1;
382 }
383
384 fn pop_transform(&mut self) {
385 self.transform_pops += 1;
386 }
387
388 fn push_clip_glyph(&mut self, glyph: GlyphId) {
389 self.clip_pushes += 1;
390 self.clip_glyphs.push(glyph);
391 }
392
393 fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {
394 self.clip_pushes += 1;
395 }
396
397 fn pop_clip(&mut self) {
398 self.clip_pops += 1;
399 }
400
401 fn fill(&mut self, _brush: Brush<'_>) {
402 self.fills += 1;
403 }
404
405 fn fill_glyph(
406 &mut self,
407 glyph: GlyphId,
408 _brush_transform: Option<Matrix<f32>>,
409 _brush: Brush<'_>,
410 ) {
411 self.filled_glyphs.push(glyph);
412 self.fills += 1;
413 }
414
415 fn push_layer(&mut self, _composite_mode: CompositeMode) {
416 self.layer_pushes += 1;
417 }
418
419 fn pop_layer(&mut self) {
420 self.layer_pops += 1;
421 }
422 }
423
424 fn colr_tables(font: &FontRef<'static>) -> ColrTables<'static> {
425 ColrTables::new(font.colr().unwrap())
426 }
427
428 #[test]
429 fn transform_error_unwinds_transform_stack() {
430 let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
431 let tables = colr_tables(&font);
432 let instance = ColrInstance::new(&tables, &[], &[]);
433 let model = Font::new(font_test_data::COLRV0V1_VARIABLE.to_vec(), 0).unwrap();
434 let glyph = model.charmap().map_unicode(CLIPBOX[0]).unwrap();
435 let (paint, _) = instance.v1_base_glyph(glyph).unwrap();
436 let mut sink = StackTrackingSink::default();
437 let mut state = TraversalState::new(instance, &mut sink);
438 let mut decycler = PaintDecycler::new();
439 state.nodes_left = 1;
441 let paint = ResolvedPaint::Translate {
442 dx: 0.0,
443 dy: 0.0,
444 paint,
445 };
446 let result = traverse_with_callbacks(&paint, &mut state, &mut decycler, 0);
447 assert!(matches!(result, Err(Error::LimitExceeded)));
448 assert_eq!(sink.transform_pushes, sink.transform_pops);
449 assert_ne!(sink.transform_pushes, 0);
450 }
451
452 #[test]
453 fn composite_error_unwinds_layer_stack() {
454 let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
455 let tables = colr_tables(&font);
456 let instance = ColrInstance::new(&tables, &[], &[]);
457 let model = Font::new(font_test_data::COLRV0V1_VARIABLE.to_vec(), 0).unwrap();
458 let glyph = model.charmap().map_unicode(CLIPBOX[0]).unwrap();
459 let (inner_paint, _) = instance.v1_base_glyph(glyph).unwrap();
460 let mut sink = StackTrackingSink::default();
461 let mut state = TraversalState::new(instance, &mut sink);
462 let mut decycler = PaintDecycler::new();
463 state.nodes_left = 0;
464 let paint = ResolvedPaint::Composite {
465 source_paint: inner_paint.clone(),
466 mode: CompositeMode::SrcOver,
467 backdrop_paint: inner_paint,
468 };
469 let result = traverse_with_callbacks(&paint, &mut state, &mut decycler, 0);
470 assert!(matches!(result, Err(Error::LimitExceeded)));
471 assert_eq!(sink.layer_pushes, sink.layer_pops);
472 assert_ne!(sink.layer_pushes, 0);
473 }
474
475 #[test]
476 fn clipbox_error_unwinds_clip_stack() {
477 let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
478 let tables = colr_tables(&font);
479 let instance = ColrInstance::new(&tables, &[], &[]);
480 let model = Font::new(font_test_data::COLRV0V1_VARIABLE.to_vec(), 0).unwrap();
481 let glyph = model.charmap().map_unicode(CLIPBOX[0]).unwrap();
482 let mut sink = StackTrackingSink::default();
483 let mut state = TraversalState::new(instance, &mut sink);
484 let mut decycler = PaintDecycler::new();
485 state.nodes_left = 0;
486 let paint = ResolvedPaint::ColrGlyph {
487 glyph: GlyphId16::new(glyph.to_u32() as u16),
488 };
489 let result = traverse_with_callbacks(&paint, &mut state, &mut decycler, 0);
490 assert!(matches!(result, Err(Error::LimitExceeded)));
491 assert_eq!(sink.clip_pushes, sink.clip_pops);
492 assert_ne!(sink.clip_pushes, 0);
493 }
494
495 #[test]
496 fn no_panic_on_empty_colorline() {
497 let test_case: &[u8] = &[
499 0, 1, 0, 0, 0, 3, 32, 32, 32, 32, 32, 32, 0, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32,
500 32, 32, 32, 32, 32, 67, 79, 76, 82, 32, 32, 32, 32, 0, 0, 0, 229, 0, 0, 0, 178, 99,
501 109, 97, 112, 32, 32, 32, 32, 0, 0, 0, 10, 0, 0, 1, 32, 32, 32, 32, 255, 32, 32, 32, 0,
502 4, 32, 255, 32, 32, 0, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32, 32, 32, 32, 32, 32, 32,
503 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 32, 0, 0,
504 32, 32, 0, 0, 0, 57, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32, 32, 32, 32, 32, 32, 32,
505 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
506 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
507 32, 0, 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
508 32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
509 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 0, 4, 32, 32, 32, 32, 32, 32, 32,
510 32, 32, 0, 0, 0, 1, 32, 32, 32, 32, 32, 32, 255, 0, 0, 0, 40, 32, 32, 32, 32, 32, 32,
511 32, 255, 255, 32, 32, 32, 4, 0, 0, 32, 32, 32, 32, 32, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32,
512 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 32, 32, 32, 255, 255,
513 255, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
514 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
515 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255,
516 255, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
517 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
518 ];
519
520 let font = FontRef::new(test_case).unwrap();
521 font.cmap().unwrap();
522 font.colr().unwrap();
523
524 let model = Font::new(test_case.to_vec(), 0).unwrap();
525 let color_glyph = model
526 .color_glyphs()
527 .get_with_version(GlyphId::new(8447), ColorGlyphVersion::ColrV1)
528 .unwrap();
529 let _ = color_glyph.paint(&mut NopSink);
530 }
531
532 #[test]
533 fn visited_nodes_limit() {
534 let font = FontRef::new(font_test_data::COLRV0V1_VARIABLE).unwrap();
535 let tables = colr_tables(&font);
536 let gid = GlyphId::new(120);
537 let mut sink = NopSink;
538 let mut traverse_with_limit = |limit| {
539 let instance = ColrInstance::new(&tables, &[], &[]);
540 let mut state = TraversalState::new(instance, &mut sink);
541 let mut decycler = PaintDecycler::new();
542 state.nodes_left = limit;
543 let paint = state
544 .resolve_paint(&state.instance.v1_base_glyph(gid).unwrap().0)
545 .unwrap();
546 traverse_with_callbacks(&paint, &mut state, &mut decycler, 0)
547 .map(|_| limit - state.nodes_left)
548 };
549 let node_count = traverse_with_limit(MAX_NODES).unwrap();
551 let result = traverse_with_limit(node_count - 1);
553 assert!(matches!(result, Err(Error::LimitExceeded)));
554 }
555
556 #[test]
557 fn a_minimal_colr_reads_without_variation_tables() {
558 let bytes = [0u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
560 let colr = crate::tables::colr::Colr::read(FontData::new(&bytes)).unwrap();
561 let tables = ColrTables::new(colr);
562 assert!(tables.index_map.is_none());
563 assert!(tables.var_store.is_none());
564 }
565}