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