Skip to main content

skrifa/color/
traversal.rs

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
22// Avoid heap allocations for any gradient with <= 32 color stops. This number
23// was chosen to keep stack size < 512 bytes.
24//
25// The largest gradient in Noto Color Emoji has 13 stops.
26//
27// Only one ColorStopVec will be created per paint graph traversal.
28//
29// Usage of SmallVec as a response to Behdad's wonderful memory usage analysis:
30// <https://docs.google.com/document/d/1S47f3E--yqvFdG7lmmufxRoFi_wMzotC03v8UvS_p54/edit?tab=t.0#heading=h.bfj7urloz3oe>
31const 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
44/// Depth at which we will stop traversing and return an error.
45///
46/// Used to prevent stack overflows. Also allows us to avoid using a HashSet
47/// in no_std builds.
48///
49/// This limit matches the one used in HarfBuzz:
50/// HB_MAX_NESTING_LEVEL: <https://github.com/harfbuzz/harfbuzz/blob/c2f8f35a6cfce43b88552b3eb5c05062ac7007b2/src/hb-limits.hh#L53>
51/// hb_paint_context_t: <https://github.com/harfbuzz/harfbuzz/blob/c2f8f35a6cfce43b88552b3eb5c05062ac7007b2/src/OT/Color/COLR/COLR.hh#L74>
52const MAX_TRAVERSAL_DEPTH: usize = 64;
53
54/// Maximum number of nodes visited during a single traversal.
55///
56/// Prevents excessive execution time on graphs with high fan-out. Set to the 16384 cap recommended
57/// by security analysis. Above HarfBuzz's limit of 2048
58/// (<https://github.com/harfbuzz/harfbuzz/blob/9f2f03173b7fee860cc00d999857d09fa4a362e2/src/hb-limits.hh#L96>)
59/// since we have seen some glyphs (Noto Emoji Color flags) use about ~6700 in practice.
60const 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                // Perform cycle detection with paint id here, second part of the tuple.
122                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            // Look for the pattern `(transform)* fill` and optimize it to a
147            // single paint call
148            let mut next_paint = state.resolve_paint(paint)?;
149            // Collect any chain of intermediate transforms
150            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            // If the next paint is a brush, we can optimize the traversal to a
159            // single fill_glyph call
160            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                // Valid brush but doesn't produce any rendering, so we can
168                // skip the glyph entirely
169                MaybeBrush::NonRendering => {
170                    return Ok(());
171                }
172                // Not a brush, fall through to the unoptimized path
173                MaybeBrush::None => {}
174            }
175            // In case the optimization was not successful, just push a clip,
176            // and continue unoptimized traversal.
177            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        // Fractional value needed to match variation scaling of clipbox.
325        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            // nop
368        }
369
370        fn pop_transform(&mut self) {
371            // nop
372        }
373
374        fn push_clip_glyph(&mut self, _glyph_id: GlyphId) {
375            // nop
376        }
377
378        fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {
379            // nop
380        }
381
382        fn pop_clip(&mut self) {
383            // nop
384        }
385
386        fn fill(&mut self, _brush: Brush<'_>) {
387            // nop
388        }
389
390        fn push_layer(&mut self, _composite_mode: CompositeMode) {
391            // nop
392        }
393
394        fn pop_layer(&mut self) {
395            // nop
396        }
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            // nop
432        }
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        // Minimized test case from <https://issues.oss-fuzz.com/issues/375768991>.
513        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        // Compute the actual number of nodes used by the glyph
564        let node_count = traverse_with_limit(MAX_NODES).unwrap();
565        // Now run with a reduced limit and verify that we get an error
566        let result = traverse_with_limit(node_count - 1);
567        assert!(matches!(result, Err(PaintError::DepthLimitExceeded)));
568    }
569}