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