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