skrifa/color/
traversal.rs

1use std::{cmp::Ordering, ops::Range};
2
3use read_fonts::{
4    tables::colr::{CompositeMode, Extend},
5    types::{BoundingBox, GlyphId, Point},
6};
7
8use super::{
9    instance::{
10        resolve_clip_box, resolve_paint, ColorStops, ColrInstance, ResolvedColorStop, ResolvedPaint,
11    },
12    Brush, ColorPainter, ColorStop, PaintCachedColorGlyph, PaintError, Transform,
13};
14
15use crate::decycler::{Decycler, DecyclerError};
16
17#[cfg(feature = "libm")]
18#[allow(unused_imports)]
19use core_maths::*;
20
21pub(crate) type PaintDecycler = Decycler<usize, MAX_TRAVERSAL_DEPTH>;
22
23// Avoid heap allocations for any gradient with <= 32 color stops. This number
24// was chosen to keep stack size < 512 bytes.
25//
26// The largest gradient in Noto Color Emoji has 13 stops.
27//
28// Only one ColorStopVec will be created per paint graph traversal.
29//
30// Usage of SmallVec as a response to Behdad's wonderful memory usage analysis:
31// <https://docs.google.com/document/d/1S47f3E--yqvFdG7lmmufxRoFi_wMzotC03v8UvS_p54/edit?tab=t.0#heading=h.bfj7urloz3oe>
32const MAX_INLINE_COLOR_STOPS: usize = 32;
33
34pub(crate) type ColorStopVec = crate::collections::SmallVec<ColorStop, MAX_INLINE_COLOR_STOPS>;
35
36impl From<DecyclerError> for PaintError {
37    fn from(value: DecyclerError) -> Self {
38        match value {
39            DecyclerError::CycleDetected => Self::PaintCycleDetected,
40            DecyclerError::DepthLimitExceeded => Self::DepthLimitExceeded,
41        }
42    }
43}
44
45/// Depth at which we will stop traversing and return an error.
46///
47/// Used to prevent stack overflows. Also allows us to avoid using a HashSet
48/// in no_std builds.
49///
50/// This limit matches the one used in HarfBuzz:
51/// HB_MAX_NESTING_LEVEL: <https://github.com/harfbuzz/harfbuzz/blob/c2f8f35a6cfce43b88552b3eb5c05062ac7007b2/src/hb-limits.hh#L53>
52/// hb_paint_context_t: <https://github.com/harfbuzz/harfbuzz/blob/c2f8f35a6cfce43b88552b3eb5c05062ac7007b2/src/OT/Color/COLR/COLR.hh#L74>
53const MAX_TRAVERSAL_DEPTH: usize = 64;
54
55pub(crate) fn get_clipbox_font_units(
56    colr_instance: &ColrInstance,
57    glyph_id: GlyphId,
58) -> Option<BoundingBox<f32>> {
59    let maybe_clipbox = (*colr_instance).v1_clip_box(glyph_id).ok().flatten()?;
60    Some(resolve_clip_box(colr_instance, &maybe_clipbox))
61}
62
63impl From<ResolvedColorStop> for ColorStop {
64    fn from(resolved_stop: ResolvedColorStop) -> Self {
65        ColorStop {
66            offset: resolved_stop.offset,
67            alpha: resolved_stop.alpha,
68            palette_index: resolved_stop.palette_index,
69        }
70    }
71}
72
73fn make_sorted_resolved_stops(
74    stops: &ColorStops,
75    instance: &ColrInstance,
76    out_stops: &mut ColorStopVec,
77) {
78    let color_stop_iter = stops.resolve(instance).map(|stop| stop.into());
79    out_stops.clear();
80    for stop in color_stop_iter {
81        out_stops.push(stop);
82    }
83    out_stops.sort_by(|a, b| a.offset.partial_cmp(&b.offset).unwrap_or(Ordering::Equal));
84}
85
86struct CollectFillGlyphPainter<'a> {
87    brush_transform: Option<Transform>,
88    glyph_id: GlyphId,
89    parent_painter: &'a mut dyn ColorPainter,
90    pub optimization_success: bool,
91}
92
93impl<'a> CollectFillGlyphPainter<'a> {
94    fn new(parent_painter: &'a mut dyn ColorPainter, glyph_id: GlyphId) -> Self {
95        Self {
96            brush_transform: None,
97            glyph_id,
98            parent_painter,
99            optimization_success: true,
100        }
101    }
102}
103
104impl ColorPainter for CollectFillGlyphPainter<'_> {
105    fn push_transform(&mut self, transform: Transform) {
106        if self.optimization_success {
107            match self.brush_transform {
108                None => {
109                    self.brush_transform = Some(transform);
110                }
111                Some(ref mut existing_transform) => {
112                    *existing_transform *= transform;
113                }
114            }
115        }
116    }
117
118    fn pop_transform(&mut self) {
119        // Since we only support fill and and transform operations, we need to
120        // ignore a popped transform, as this would be called after traversing
121        // the graph backup after a fill was performed, but we want to preserve
122        // the transform in order to be able to return it.
123    }
124
125    fn fill(&mut self, brush: Brush<'_>) {
126        if self.optimization_success {
127            self.parent_painter
128                .fill_glyph(self.glyph_id, self.brush_transform, brush);
129        }
130    }
131
132    fn push_clip_glyph(&mut self, _: GlyphId) {
133        self.optimization_success = false;
134    }
135
136    fn push_clip_box(&mut self, _: BoundingBox<f32>) {
137        self.optimization_success = false;
138    }
139
140    fn pop_clip(&mut self) {
141        self.optimization_success = false;
142    }
143
144    fn push_layer(&mut self, _: CompositeMode) {
145        self.optimization_success = false;
146    }
147
148    fn pop_layer(&mut self) {
149        self.optimization_success = false;
150    }
151}
152
153pub(crate) fn traverse_with_callbacks(
154    paint: &ResolvedPaint,
155    instance: &ColrInstance,
156    painter: &mut impl ColorPainter,
157    decycler: &mut PaintDecycler,
158    resolved_stops: &mut ColorStopVec,
159    recurse_depth: usize,
160) -> Result<(), PaintError> {
161    if recurse_depth >= MAX_TRAVERSAL_DEPTH {
162        return Err(PaintError::DepthLimitExceeded);
163    }
164    match paint {
165        ResolvedPaint::ColrLayers { range } => {
166            for layer_index in range.clone() {
167                // Perform cycle detection with paint id here, second part of the tuple.
168                let (layer_paint, paint_id) = (*instance).v1_layer(layer_index)?;
169                let mut cycle_guard = decycler.enter(paint_id)?;
170                traverse_with_callbacks(
171                    &resolve_paint(instance, &layer_paint)?,
172                    instance,
173                    painter,
174                    &mut cycle_guard,
175                    resolved_stops,
176                    recurse_depth + 1,
177                )?;
178            }
179            Ok(())
180        }
181        ResolvedPaint::Solid {
182            palette_index,
183            alpha,
184        } => {
185            painter.fill(Brush::Solid {
186                palette_index: *palette_index,
187                alpha: *alpha,
188            });
189            Ok(())
190        }
191        ResolvedPaint::LinearGradient {
192            x0,
193            y0,
194            x1,
195            y1,
196            x2,
197            y2,
198            color_stops,
199            extend,
200        } => {
201            let mut p0 = Point::new(*x0, *y0);
202            let p1 = Point::new(*x1, *y1);
203            let p2 = Point::new(*x2, *y2);
204
205            let dot_product = |a: Point<f32>, b: Point<f32>| -> f32 { a.x * b.x + a.y * b.y };
206            let cross_product = |a: Point<f32>, b: Point<f32>| -> f32 { a.x * b.y - a.y * b.x };
207            let project_onto = |vector: Point<f32>, point: Point<f32>| -> Point<f32> {
208                let length = (point.x * point.x + point.y * point.y).sqrt();
209                if length == 0.0 {
210                    return Point::default();
211                }
212                let mut point_normalized = point / length;
213                point_normalized *= dot_product(vector, point) / length;
214                point_normalized
215            };
216
217            make_sorted_resolved_stops(color_stops, instance, resolved_stops);
218
219            // If p0p1 or p0p2 are degenerate probably nothing should be drawn.
220            // If p0p1 and p0p2 are parallel then one side is the first color and the other side is
221            // the last color, depending on the direction.
222            // For now, just use the first color.
223            if p1 == p0 || p2 == p0 || cross_product(p1 - p0, p2 - p0) == 0.0 {
224                if let Some(stop) = resolved_stops.first() {
225                    painter.fill(Brush::Solid {
226                        palette_index: stop.palette_index,
227                        alpha: stop.alpha,
228                    });
229                };
230                return Ok(());
231            }
232
233            // Follow implementation note in nanoemoji:
234            // https://github.com/googlefonts/nanoemoji/blob/0ac6e7bb4d8202db692574d8530a9b643f1b3b3c/src/nanoemoji/svg.py#L188
235            // to compute a new gradient end point P3 as the orthogonal
236            // projection of the vector from p0 to p1 onto a line perpendicular
237            // to line p0p2 and passing through p0.
238            let mut perpendicular_to_p2 = p2 - p0;
239            perpendicular_to_p2 = Point::new(perpendicular_to_p2.y, -perpendicular_to_p2.x);
240            let mut p3 = p0 + project_onto(p1 - p0, perpendicular_to_p2);
241
242            match (
243                resolved_stops.first().cloned(),
244                resolved_stops.last().cloned(),
245            ) {
246                (None, _) | (_, None) => {}
247                (Some(first_stop), Some(last_stop)) => {
248                    let mut color_stop_range = last_stop.offset - first_stop.offset;
249
250                    // Nothing can be drawn for this situation.
251                    if color_stop_range == 0.0 && extend != &Extend::Pad {
252                        return Ok(());
253                    }
254
255                    // In the Pad case, for providing normalized stops in the 0 to 1 range to the client,
256                    // insert a color stop at the end. Adding this stop will paint the equivalent gradient,
257                    // because: All font-specified color stops are in the same spot, mode is pad, so
258                    // everything before this spot is painted with the first color, everything after this spot
259                    // is painted with the last color. Not adding this stop would skip the projection below along
260                    // the p0-p3 axis and result in specifying non-normalized color stops to the shader.
261
262                    if color_stop_range == 0.0 && extend == &Extend::Pad {
263                        let mut extra_stop = last_stop;
264                        extra_stop.offset += 1.0;
265                        resolved_stops.push(extra_stop);
266
267                        color_stop_range = 1.0;
268                    }
269
270                    debug_assert!(color_stop_range != 0.0);
271
272                    if color_stop_range != 1.0 || first_stop.offset != 0.0 {
273                        let p0_p3 = p3 - p0;
274                        let p0_offset = p0_p3 * first_stop.offset;
275                        let p3_offset = p0_p3 * last_stop.offset;
276
277                        p3 = p0 + p3_offset;
278                        p0 += p0_offset;
279
280                        let scale_factor = 1.0 / color_stop_range;
281                        let start_offset = first_stop.offset;
282
283                        for stop in resolved_stops.iter_mut() {
284                            stop.offset = (stop.offset - start_offset) * scale_factor;
285                        }
286                    }
287
288                    painter.fill(Brush::LinearGradient {
289                        p0,
290                        p1: p3,
291                        color_stops: resolved_stops.as_slice(),
292                        extend: *extend,
293                    });
294                }
295            }
296
297            Ok(())
298        }
299        ResolvedPaint::RadialGradient {
300            x0,
301            y0,
302            radius0,
303            x1,
304            y1,
305            radius1,
306            color_stops,
307            extend,
308        } => {
309            let mut c0 = Point::new(*x0, *y0);
310            let mut c1 = Point::new(*x1, *y1);
311            let mut radius0 = *radius0;
312            let mut radius1 = *radius1;
313
314            make_sorted_resolved_stops(color_stops, instance, resolved_stops);
315
316            match (
317                resolved_stops.first().cloned(),
318                resolved_stops.last().cloned(),
319            ) {
320                (None, _) | (_, None) => {}
321                (Some(first_stop), Some(last_stop)) => {
322                    let mut color_stop_range = last_stop.offset - first_stop.offset;
323                    // Nothing can be drawn for this situation.
324                    if color_stop_range == 0.0 && extend != &Extend::Pad {
325                        return Ok(());
326                    }
327
328                    // In the Pad case, for providing normalized stops in the 0 to 1 range to the client,
329                    // insert a color stop at the end. See LinearGradient for more details.
330
331                    if color_stop_range == 0.0 && extend == &Extend::Pad {
332                        let mut extra_stop = last_stop;
333                        extra_stop.offset += 1.0;
334                        resolved_stops.push(extra_stop);
335                        color_stop_range = 1.0;
336                    }
337
338                    debug_assert!(color_stop_range != 0.0);
339
340                    // If the colorStopRange is 0 at this point, the default behavior of the shader is to
341                    // clamp to 1 color stops that are above 1, clamp to 0 for color stops that are below 0,
342                    // and repeat the outer color stops at 0 and 1 if the color stops are inside the
343                    // range. That will result in the correct rendering.
344                    if color_stop_range != 1.0 || first_stop.offset != 0.0 {
345                        let c0_to_c1 = c1 - c0;
346                        let radius_diff = radius1 - radius0;
347                        let scale_factor = 1.0 / color_stop_range;
348
349                        let c0_offset = c0_to_c1 * first_stop.offset;
350                        let c1_offset = c0_to_c1 * last_stop.offset;
351                        let stops_start_offset = first_stop.offset;
352
353                        // Order of reassignments is important to avoid shadowing variables.
354                        c1 = c0 + c1_offset;
355                        c0 += c0_offset;
356                        radius1 = radius0 + radius_diff * last_stop.offset;
357                        radius0 += radius_diff * first_stop.offset;
358
359                        for stop in resolved_stops.iter_mut() {
360                            stop.offset = (stop.offset - stops_start_offset) * scale_factor;
361                        }
362                    }
363
364                    painter.fill(Brush::RadialGradient {
365                        c0,
366                        r0: radius0,
367                        c1,
368                        r1: radius1,
369                        color_stops: resolved_stops.as_slice(),
370                        extend: *extend,
371                    });
372                }
373            }
374            Ok(())
375        }
376        ResolvedPaint::SweepGradient {
377            center_x,
378            center_y,
379            start_angle,
380            end_angle,
381            color_stops,
382            extend,
383        } => {
384            // OpenType 1.9.1 adds a shift to the angle to ease specification of a 0 to 360
385            // degree sweep.
386            let sweep_angle_to_degrees = |angle| angle * 180.0 + 180.0;
387
388            let start_angle = sweep_angle_to_degrees(start_angle);
389            let end_angle = sweep_angle_to_degrees(end_angle);
390
391            // Stop normalization for sweep:
392
393            let sector_angle = end_angle - start_angle;
394
395            make_sorted_resolved_stops(color_stops, instance, resolved_stops);
396            if resolved_stops.is_empty() {
397                return Ok(());
398            }
399
400            match (
401                resolved_stops.first().cloned(),
402                resolved_stops.last().cloned(),
403            ) {
404                (None, _) | (_, None) => {}
405                (Some(first_stop), Some(last_stop)) => {
406                    let mut color_stop_range = last_stop.offset - first_stop.offset;
407
408                    let mut start_angle_scaled = start_angle + sector_angle * first_stop.offset;
409                    let mut end_angle_scaled = start_angle + sector_angle * last_stop.offset;
410
411                    let start_offset = first_stop.offset;
412
413                    // Nothing can be drawn for this situation.
414                    if color_stop_range == 0.0 && extend != &Extend::Pad {
415                        return Ok(());
416                    }
417
418                    // In the Pad case, if the color_stop_range is 0 insert a color stop at the end before
419                    // normalizing. Adding this stop will paint the equivalent gradient, because: All font
420                    // specified color stops are in the same spot, mode is pad, so everything before this
421                    // spot is painted with the first color, everything after this spot is painted with
422                    // the last color. Not adding this stop will skip the projection and result in
423                    // specifying non-normalized color stops to the shader.
424                    if color_stop_range == 0.0 && extend == &Extend::Pad {
425                        let mut offset_last = last_stop;
426                        offset_last.offset += 1.0;
427                        resolved_stops.push(offset_last);
428                        color_stop_range = 1.0;
429                    }
430
431                    debug_assert!(color_stop_range != 0.0);
432
433                    let scale_factor = 1.0 / color_stop_range;
434
435                    for shift_stop in resolved_stops.iter_mut() {
436                        shift_stop.offset = (shift_stop.offset - start_offset) * scale_factor;
437                    }
438
439                    // /* https://docs.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients
440                    //  * "The angles are expressed in counter-clockwise degrees from
441                    //  * the direction of the positive x-axis on the design
442                    //  * grid. [...]  The color line progresses from the start angle
443                    //  * to the end angle in the counter-clockwise direction;" -
444                    //  * Convert angles and stops from counter-clockwise to clockwise
445                    //  * for the shader if the gradient is not already reversed due to
446                    //  * start angle being larger than end angle. */
447                    start_angle_scaled = 360.0 - start_angle_scaled;
448                    end_angle_scaled = 360.0 - end_angle_scaled;
449
450                    if start_angle_scaled >= end_angle_scaled {
451                        (start_angle_scaled, end_angle_scaled) =
452                            (end_angle_scaled, start_angle_scaled);
453                        resolved_stops.reverse();
454                        for stop in resolved_stops.iter_mut() {
455                            stop.offset = 1.0 - stop.offset;
456                        }
457                    }
458
459                    // https://learn.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients
460                    // "If the color line's extend mode is reflect or repeat
461                    // and start and end angle are equal, nothing shall be drawn."
462                    if start_angle_scaled == end_angle_scaled && extend != &Extend::Pad {
463                        return Ok(());
464                    }
465
466                    painter.fill(Brush::SweepGradient {
467                        c0: Point::new(*center_x, *center_y),
468                        start_angle: start_angle_scaled,
469                        end_angle: end_angle_scaled,
470                        color_stops: resolved_stops.as_slice(),
471                        extend: *extend,
472                    });
473                }
474            }
475            Ok(())
476        }
477
478        ResolvedPaint::Glyph { glyph_id, paint } => {
479            let glyph_id = (*glyph_id).into();
480            let mut optimizer = CollectFillGlyphPainter::new(painter, glyph_id);
481            let mut result = traverse_with_callbacks(
482                &resolve_paint(instance, paint)?,
483                instance,
484                &mut optimizer,
485                decycler,
486                resolved_stops,
487                recurse_depth + 1,
488            );
489
490            // In case the optimization was not successful, just push a clip, and continue unoptimized traversal.
491            if !optimizer.optimization_success {
492                painter.push_clip_glyph(glyph_id);
493                result = traverse_with_callbacks(
494                    &resolve_paint(instance, paint)?,
495                    instance,
496                    painter,
497                    decycler,
498                    resolved_stops,
499                    recurse_depth + 1,
500                );
501                painter.pop_clip();
502            }
503
504            result
505        }
506        ResolvedPaint::ColrGlyph { glyph_id } => {
507            let glyph_id = (*glyph_id).into();
508            match (*instance).v1_base_glyph(glyph_id)? {
509                Some((base_glyph, base_glyph_paint_id)) => {
510                    let mut cycle_guard = decycler.enter(base_glyph_paint_id)?;
511                    let draw_result = painter.paint_cached_color_glyph(glyph_id)?;
512                    match draw_result {
513                        PaintCachedColorGlyph::Ok => Ok(()),
514                        PaintCachedColorGlyph::Unimplemented => {
515                            let clipbox = get_clipbox_font_units(instance, glyph_id);
516
517                            if let Some(rect) = clipbox {
518                                painter.push_clip_box(rect);
519                            }
520
521                            let result = traverse_with_callbacks(
522                                &resolve_paint(instance, &base_glyph)?,
523                                instance,
524                                painter,
525                                &mut cycle_guard,
526                                resolved_stops,
527                                recurse_depth + 1,
528                            );
529                            if clipbox.is_some() {
530                                painter.pop_clip();
531                            }
532                            result
533                        }
534                    }
535                }
536                None => Err(PaintError::GlyphNotFound(glyph_id)),
537            }
538        }
539        ResolvedPaint::Transform {
540            paint: next_paint, ..
541        }
542        | ResolvedPaint::Translate {
543            paint: next_paint, ..
544        }
545        | ResolvedPaint::Scale {
546            paint: next_paint, ..
547        }
548        | ResolvedPaint::Rotate {
549            paint: next_paint, ..
550        }
551        | ResolvedPaint::Skew {
552            paint: next_paint, ..
553        } => {
554            painter.push_transform(paint.try_into()?);
555            let result = traverse_with_callbacks(
556                &resolve_paint(instance, next_paint)?,
557                instance,
558                painter,
559                decycler,
560                resolved_stops,
561                recurse_depth + 1,
562            );
563            painter.pop_transform();
564            result
565        }
566        ResolvedPaint::Composite {
567            source_paint,
568            mode,
569            backdrop_paint,
570        } => {
571            painter.push_layer(CompositeMode::SrcOver);
572            let mut result = traverse_with_callbacks(
573                &resolve_paint(instance, backdrop_paint)?,
574                instance,
575                painter,
576                decycler,
577                resolved_stops,
578                recurse_depth + 1,
579            );
580            result?;
581            painter.push_layer(*mode);
582            result = traverse_with_callbacks(
583                &resolve_paint(instance, source_paint)?,
584                instance,
585                painter,
586                decycler,
587                resolved_stops,
588                recurse_depth + 1,
589            );
590            painter.pop_layer_with_mode(*mode);
591            painter.pop_layer_with_mode(CompositeMode::SrcOver);
592            result
593        }
594    }
595}
596
597pub(crate) fn traverse_v0_range(
598    range: &Range<usize>,
599    instance: &ColrInstance,
600    painter: &mut impl ColorPainter,
601) -> Result<(), PaintError> {
602    for layer_index in range.clone() {
603        let (layer_glyph, palette_index) = (*instance).v0_layer(layer_index)?;
604        painter.fill_glyph(
605            layer_glyph.into(),
606            None,
607            Brush::Solid {
608                palette_index,
609                alpha: 1.0,
610            },
611        );
612    }
613    Ok(())
614}
615
616#[cfg(test)]
617mod tests {
618    use raw::types::GlyphId;
619    use read_fonts::{types::BoundingBox, FontRef, TableProvider};
620
621    use crate::{
622        color::{
623            instance::ColrInstance, traversal::get_clipbox_font_units,
624            traversal_tests::test_glyph_defs::CLIPBOX, Brush, ColorGlyphFormat, ColorPainter,
625            CompositeMode, Transform,
626        },
627        prelude::LocationRef,
628        MetadataProvider,
629    };
630
631    #[test]
632    fn clipbox_test() {
633        let colr_font = font_test_data::COLRV0V1_VARIABLE;
634        let font = FontRef::new(colr_font).unwrap();
635        let test_glyph_id = font.charmap().map(CLIPBOX[0]).unwrap();
636        let upem = font.head().unwrap().units_per_em();
637
638        let base_bounding_box = BoundingBox {
639            x_min: 0.0,
640            x_max: upem as f32 / 2.0,
641            y_min: upem as f32 / 2.0,
642            y_max: upem as f32,
643        };
644        // Fractional value needed to match variation scaling of clipbox.
645        const CLIPBOX_SHIFT: f32 = 200.0122;
646
647        macro_rules! test_entry {
648            ($axis:literal, $shift:expr, $field:ident) => {
649                (
650                    $axis,
651                    $shift,
652                    BoundingBox {
653                        $field: base_bounding_box.$field + ($shift),
654                        ..base_bounding_box
655                    },
656                )
657            };
658        }
659
660        let test_data_expectations = [
661            ("", 0.0, base_bounding_box),
662            test_entry!("CLXI", CLIPBOX_SHIFT, x_min),
663            test_entry!("CLXA", -CLIPBOX_SHIFT, x_max),
664            test_entry!("CLYI", CLIPBOX_SHIFT, y_min),
665            test_entry!("CLYA", -CLIPBOX_SHIFT, y_max),
666        ];
667
668        for axis_test in test_data_expectations {
669            let axis_coordinate = (axis_test.0, axis_test.1);
670            let location = font.axes().location([axis_coordinate]);
671            let color_instance = ColrInstance::new(font.colr().unwrap(), location.coords());
672            let clip_box = get_clipbox_font_units(&color_instance, test_glyph_id);
673            assert!(clip_box.is_some());
674            assert!(
675                clip_box.unwrap() == axis_test.2,
676                "Clip boxes do not match. Actual: {:?}, expected: {:?}",
677                clip_box.unwrap(),
678                axis_test.2
679            );
680        }
681    }
682
683    struct NopPainter;
684
685    impl ColorPainter for NopPainter {
686        fn push_transform(&mut self, _transform: Transform) {
687            // nop
688        }
689
690        fn pop_transform(&mut self) {
691            // nop
692        }
693
694        fn push_clip_glyph(&mut self, _glyph_id: GlyphId) {
695            // nop
696        }
697
698        fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {
699            // nop
700        }
701
702        fn pop_clip(&mut self) {
703            // nop
704        }
705
706        fn fill(&mut self, _brush: Brush<'_>) {
707            // nop
708        }
709
710        fn push_layer(&mut self, _composite_mode: CompositeMode) {
711            // nop
712        }
713
714        fn pop_layer(&mut self) {
715            // nop
716        }
717    }
718
719    #[test]
720    fn no_panic_on_empty_colorline() {
721        // Minimized test case from <https://issues.oss-fuzz.com/issues/375768991>.
722        let test_case = &[
723            0, 1, 0, 0, 0, 3, 32, 32, 32, 32, 32, 32, 0, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32,
724            32, 32, 32, 32, 32, 67, 79, 76, 82, 32, 32, 32, 32, 0, 0, 0, 229, 0, 0, 0, 178, 99,
725            109, 97, 112, 32, 32, 32, 32, 0, 0, 0, 10, 0, 0, 1, 32, 32, 32, 32, 255, 32, 32, 32, 0,
726            4, 32, 255, 32, 32, 0, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32, 32, 32, 32, 32, 32, 32,
727            32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 32, 0, 0,
728            32, 32, 0, 0, 0, 57, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32, 32, 32, 32, 32, 32, 32,
729            32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
730            32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
731            32, 0, 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
732            32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
733            255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 0, 4, 32, 32, 32, 32, 32, 32, 32,
734            32, 32, 0, 0, 0, 1, 32, 32, 32, 32, 32, 32, 255, 0, 0, 0, 40, 32, 32, 32, 32, 32, 32,
735            32, 255, 255, 32, 32, 32, 4, 0, 0, 32, 32, 32, 32, 32, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32,
736            32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 32, 32, 32, 255, 255,
737            255, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
738            32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
739            32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255,
740            255, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
741            255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
742        ];
743
744        let font = FontRef::new(test_case).unwrap();
745        font.cmap().unwrap();
746        font.colr().unwrap();
747
748        let color_glyph = font
749            .color_glyphs()
750            .get_with_format(GlyphId::new(8447), ColorGlyphFormat::ColrV1)
751            .unwrap();
752        let _ = color_glyph.paint(LocationRef::default(), &mut NopPainter);
753    }
754}