1use super::{
4 super::{
5 path,
6 pen::PathStyle,
7 unscaled::{UnscaledOutlineSink, UnscaledPoint},
8 DrawError, LocationRef, OutlineGlyph, OutlinePen,
9 },
10 metrics::Scale,
11 QuirksMode,
12};
13use crate::collections::SmallVec;
14use core::ops::Range;
15use raw::{
16 tables::glyf::{PointFlags, PointMarker},
17 types::{F26Dot6, F2Dot14, GlyphId},
18};
19
20#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
27#[repr(i8)]
28pub enum Direction {
29 #[default]
31 None = 4,
32 Right = 1,
34 Left = -1,
36 Up = 2,
38 Down = -2,
40}
41
42impl Direction {
43 pub fn new(dx: i32, dy: i32) -> Self {
47 let (dir, long_arm, short_arm) = if dy >= dx {
48 if dy >= -dx {
49 (Direction::Up, dy, dx)
50 } else {
51 (Direction::Left, -dx, dy)
52 }
53 } else if dy >= -dx {
54 (Direction::Right, dx, dy)
55 } else {
56 (Direction::Down, -dy, dx)
57 };
58 if long_arm <= 14 * short_arm.abs() {
61 Direction::None
62 } else {
63 dir
64 }
65 }
66
67 pub fn is_opposite(self, other: Self) -> bool {
68 self as i8 + other as i8 == 0
69 }
70
71 pub fn is_same_axis(self, other: Self) -> bool {
72 (self as i8).abs() == (other as i8).abs()
73 }
74
75 pub(crate) fn normalize(self) -> Self {
76 match self {
78 Self::Left => Self::Right,
79 Self::Down => Self::Up,
80 _ => self,
81 }
82 }
83}
84
85#[derive(Copy, Clone, PartialEq, Eq, Debug)]
87pub(crate) enum Orientation {
88 Clockwise,
89 CounterClockwise,
90}
91
92#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
96pub(crate) struct Point {
97 pub flags: PointFlags,
99 pub fx: i32,
101 pub fy: i32,
103 pub ox: i32,
105 pub oy: i32,
107 pub x: i32,
109 pub y: i32,
111 pub in_dir: Direction,
113 pub out_dir: Direction,
115 pub u: i32,
117 pub v: i32,
119 pub next_ix: u16,
121 pub prev_ix: u16,
123}
124
125impl Point {
126 pub fn is_on_curve(&self) -> bool {
127 self.flags.is_on_curve()
128 }
129
130 pub fn next(&self) -> usize {
132 self.next_ix as usize
133 }
134
135 pub fn prev(&self) -> usize {
137 self.prev_ix as usize
138 }
139
140 #[inline(always)]
141 fn as_contour_point(&self) -> path::ContourPoint<F26Dot6> {
142 path::ContourPoint {
143 x: F26Dot6::from_bits(self.x),
144 y: F26Dot6::from_bits(self.y),
145 flags: self.flags,
146 }
147 }
148}
149
150const MAX_INLINE_POINTS: usize = 96;
154const MAX_INLINE_CONTOURS: usize = 8;
155
156#[derive(Default)]
157pub(crate) struct Outline {
158 pub units_per_em: i32,
159 pub orientation: Option<Orientation>,
160 pub points: SmallVec<Point, MAX_INLINE_POINTS>,
161 pub contours: SmallVec<Contour, MAX_INLINE_CONTOURS>,
162 pub advance: i32,
163}
164
165impl Outline {
166 pub fn fill(
168 &mut self,
169 glyph: &OutlineGlyph,
170 coords: &[F2Dot14],
171 quirks: QuirksMode,
172 ) -> Result<(), DrawError> {
173 self.clear();
174 self.units_per_em = glyph.units_per_em() as i32;
175 self.advance = glyph.draw_unscaled(LocationRef::new(coords), None, self)?;
176 self.analyze_and_validate(glyph.glyph_id(), quirks)
177 }
178
179 fn analyze_and_validate(&mut self, gid: GlyphId, quirks: QuirksMode) -> Result<(), DrawError> {
180 const MAX_LEN: usize = u16::MAX as usize + 1;
183 if self.points.len() > MAX_LEN || self.contours.len() > MAX_LEN {
184 return Err(DrawError::TooManyPoints(gid));
185 }
186 let near_limit = 20 * self.units_per_em / 2048;
188 self.link_points();
189 self.mark_near_points(near_limit);
190 self.compute_directions(near_limit);
191 self.simplify_topology();
192 if quirks == QuirksMode::Aot {
193 self.check_remaining_weak_points(is_corner_flat_aot);
194 } else {
195 self.check_remaining_weak_points(is_corner_flat_jit);
196 }
197 self.compute_orientation();
198 Ok(())
199 }
200
201 pub fn scale(&mut self, scale: &Scale) {
204 use super::metrics::fixed_mul;
205 for point in &mut self.points {
206 let x = fixed_mul(point.fx, scale.x_scale) + scale.x_delta;
207 let y = fixed_mul(point.fy, scale.y_scale) + scale.y_delta;
208 point.ox = x;
209 point.x = x;
210 point.oy = y;
211 point.y = y;
212 }
213 }
214
215 pub fn clear(&mut self) {
216 self.units_per_em = 0;
217 self.points.clear();
218 self.contours.clear();
219 self.advance = 0;
220 }
221
222 pub fn to_path(
223 &self,
224 style: PathStyle,
225 pen: &mut impl OutlinePen,
226 ) -> Result<(), path::ToPathError> {
227 for contour in &self.contours {
228 let Some(points) = self.points.get(contour.range()) else {
229 continue;
230 };
231 if let (Some(first_point), Some(last_point)) = (
232 points.first().map(Point::as_contour_point),
233 points.last().map(Point::as_contour_point),
234 ) {
235 path::contour_to_path(
236 points.iter().map(Point::as_contour_point),
237 first_point,
238 last_point,
239 style,
240 pen,
241 )?;
242 }
243 }
244 Ok(())
245 }
246}
247
248impl Outline {
249 fn link_points(&mut self) {
251 let points = self.points.as_mut_slice();
252 for contour in &self.contours {
253 let Some(points) = points.get_mut(contour.range()) else {
254 continue;
255 };
256 let first_ix = contour.first();
257 let mut prev_ix = contour.last() as u16;
258 for (ix, point) in points.iter_mut().enumerate() {
259 let ix = (ix + first_ix) as u16;
260 point.prev_ix = prev_ix;
261 prev_ix = ix;
262 point.next_ix = ix.wrapping_add(1);
266 }
267 points.last_mut().unwrap().next_ix = first_ix as u16;
268 }
269 }
270
271 fn mark_near_points(&mut self, near_limit: i32) {
275 let points = self.points.as_mut_slice();
276 for contour in &self.contours {
277 let mut prev_ix = contour.last();
278 for ix in contour.range() {
279 let point = points[ix];
280 let prev = &mut points[prev_ix];
281 let out_x = point.fx - prev.fx;
283 let out_y = point.fy - prev.fy;
284 if out_x.abs() + out_y.abs() < near_limit {
285 prev.flags.set_marker(PointMarker::NEAR);
286 }
287 prev_ix = ix;
288 }
289 }
290 }
291
292 fn compute_directions(&mut self, near_limit: i32) {
296 let near_limit2 = 2 * near_limit - 1;
297 let points = self.points.as_mut_slice();
298 for contour in &self.contours {
299 let mut first_ix = contour.first();
301 let mut ix = first_ix;
302 let mut prev_ix = contour.prev(first_ix);
303 let mut point = points[first_ix];
304 while prev_ix != first_ix {
305 let prev = points[prev_ix];
306 let out_x = point.fx - prev.fx;
307 let out_y = point.fy - prev.fy;
308 if out_x.abs() + out_y.abs() >= near_limit2 {
310 break;
311 }
312 point = prev;
313 ix = prev_ix;
314 prev_ix = contour.prev(prev_ix);
315 }
316 first_ix = ix;
317 let first = &mut points[first_ix];
320 first.u = first_ix as _;
321 first.v = first_ix as _;
322 let mut next_ix = first_ix;
323 let mut ix = first_ix;
324 let mut out_x = 0;
327 let mut out_y = 0;
328 loop {
329 let point_ix = next_ix;
330 next_ix = contour.next(point_ix);
331 let point = points[point_ix];
332 let next = &mut points[next_ix];
333 out_x += next.fx - point.fx;
335 out_y += next.fy - point.fy;
336 if out_x.abs() + out_y.abs() < near_limit {
337 next.flags.set_marker(PointMarker::WEAK_INTERPOLATION);
338 if next_ix == first_ix {
341 break;
342 }
343 continue;
344 }
345 let out_dir = Direction::new(out_x, out_y);
346 next.in_dir = out_dir;
347 next.v = ix as _;
348 let cur = &mut points[ix];
349 cur.u = next_ix as _;
350 cur.out_dir = out_dir;
351 let mut inter_ix = contour.next(ix);
353 while inter_ix != next_ix {
354 let point = &mut points[inter_ix];
355 point.in_dir = out_dir;
356 point.out_dir = out_dir;
357 inter_ix = contour.next(inter_ix);
358 }
359 ix = next_ix;
360 points[ix].u = first_ix as _;
361 points[first_ix].v = ix as _;
362 out_x = 0;
363 out_y = 0;
364 if next_ix == first_ix {
365 break;
366 }
367 }
368 }
369 }
370
371 fn simplify_topology(&mut self) {
375 let points = self.points.as_mut_slice();
376 for i in 0..points.len() {
377 let point = points[i];
378 if point.flags.has_marker(PointMarker::WEAK_INTERPOLATION) {
379 continue;
380 }
381 if point.in_dir == Direction::None && point.out_dir == Direction::None {
382 let u_index = point.u as usize;
383 let v_index = point.v as usize;
384 let next_u = points[u_index];
385 let prev_v = points[v_index];
386 let in_x = point.fx - prev_v.fx;
387 let in_y = point.fy - prev_v.fy;
388 let out_x = next_u.fx - point.fx;
389 let out_y = next_u.fy - point.fy;
390 if (in_x ^ out_x) >= 0 && (in_y ^ out_y) >= 0 {
391 points[i].flags.set_marker(PointMarker::WEAK_INTERPOLATION);
393 points[v_index].u = u_index as _;
394 points[u_index].v = v_index as _;
395 }
396 }
397 }
398 }
399
400 fn check_remaining_weak_points(&mut self, is_corner_flat: impl Fn(i32, i32, i32, i32) -> bool) {
404 let points = self.points.as_mut_slice();
405 for i in 0..points.len() {
406 let point = points[i];
407 let mut make_weak = false;
408 if point.flags.has_marker(PointMarker::WEAK_INTERPOLATION) {
409 continue;
411 }
412 if !point.flags.is_on_curve() {
413 make_weak = true;
415 } else if point.out_dir == point.in_dir {
416 if point.out_dir != Direction::None {
417 make_weak = true;
420 } else {
421 let u_index = point.u as usize;
422 let v_index = point.v as usize;
423 let next_u = points[u_index];
424 let prev_v = points[v_index];
425 if is_corner_flat(
426 point.fx - prev_v.fx,
427 point.fy - prev_v.fy,
428 next_u.fx - point.fx,
429 next_u.fy - point.fy,
430 ) {
431 make_weak = true;
433 points[v_index].u = u_index as _;
434 points[u_index].v = v_index as _;
435 }
436 }
437 } else if point.in_dir.is_opposite(point.out_dir) {
438 make_weak = true;
440 }
441 if make_weak {
442 points[i].flags.set_marker(PointMarker::WEAK_INTERPOLATION);
443 }
444 }
445 }
446
447 fn compute_orientation(&mut self) {
451 self.orientation = None;
452 let points = self.points.as_slice();
453 if points.is_empty() {
454 return;
455 }
456 fn point_to_i64(point: &Point) -> (i64, i64) {
457 (point.fx as i64, point.fy as i64)
458 }
459 let mut area = 0i64;
460 for contour in &self.contours {
461 let last_ix = contour.last();
462 let first_ix = contour.first();
463 let (mut prev_x, mut prev_y) = point_to_i64(&points[last_ix]);
464 for point in &points[first_ix..=last_ix] {
465 let (x, y) = point_to_i64(point);
466 area += (y - prev_y) * (x + prev_x);
467 (prev_x, prev_y) = (x, y);
468 }
469 }
470 use core::cmp::Ordering;
471 self.orientation = match area.cmp(&0) {
472 Ordering::Less => Some(Orientation::CounterClockwise),
473 Ordering::Greater => Some(Orientation::Clockwise),
474 Ordering::Equal => None,
475 };
476 }
477}
478
479fn is_corner_flat_aot(in_x: i32, in_y: i32, out_x: i32, out_y: i32) -> bool {
481 let d_in = in_x.abs() + in_y.abs();
482 let d_out = out_x.abs() + out_y.abs();
483 let d_corner = (in_x + out_x).abs() + (in_y + out_y).abs();
484 (d_in + d_out - d_corner) < (d_corner >> 4)
485}
486
487fn is_corner_flat_jit(in_x: i32, in_y: i32, out_x: i32, out_y: i32) -> bool {
490 let ax = in_x + out_x;
491 let ay = in_y + out_y;
492 fn hypot(x: i32, y: i32) -> i32 {
493 let x = x.abs();
494 let y = y.abs();
495 if x > y {
496 x + ((3 * y) >> 3)
497 } else {
498 y + ((3 * x) >> 3)
499 }
500 }
501 let d_in = hypot(in_x, in_y);
502 let d_out = hypot(out_x, out_y);
503 let d_hypot = hypot(ax, ay);
504 (d_in + d_out - d_hypot) < (d_hypot >> 4)
505}
506
507#[derive(Copy, Clone, Default, Debug)]
508pub(crate) struct Contour {
509 first_ix: u16,
510 last_ix: u16,
511}
512
513impl Contour {
514 pub fn first(self) -> usize {
515 self.first_ix as usize
516 }
517
518 pub fn last(self) -> usize {
519 self.last_ix as usize
520 }
521
522 pub fn next(self, index: usize) -> usize {
523 if index >= self.last_ix as usize {
524 self.first_ix as usize
525 } else {
526 index + 1
527 }
528 }
529
530 pub fn prev(self, index: usize) -> usize {
531 if index <= self.first_ix as usize {
532 self.last_ix as usize
533 } else {
534 index - 1
535 }
536 }
537
538 pub fn range(self) -> Range<usize> {
539 self.first()..self.last() + 1
540 }
541}
542
543impl UnscaledOutlineSink for Outline {
544 fn reserve(&mut self, additional: usize) {
545 self.points.reserve(additional);
546 }
547
548 fn push(&mut self, point: UnscaledPoint) -> Result<(), DrawError> {
549 let new_point = Point {
550 flags: point.flags,
551 fx: point.x as i32,
552 fy: point.y as i32,
553 ..Default::default()
554 };
555 let new_point_ix: u16 = self
556 .points
557 .len()
558 .try_into()
559 .map_err(|_| DrawError::InsufficientMemory)?;
560 if point.is_contour_start {
561 self.contours.push(Contour {
562 first_ix: new_point_ix,
563 last_ix: new_point_ix,
564 });
565 } else if let Some(last_contour) = self.contours.last_mut() {
566 last_contour.last_ix += 1;
567 } else {
568 self.contours.push(Contour {
571 first_ix: new_point_ix,
572 last_ix: new_point_ix,
573 });
574 }
575 self.points.push(new_point);
576 Ok(())
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::super::super::{pen::SvgPen, DrawSettings};
583 use super::*;
584 use crate::{prelude::Size, MetadataProvider};
585 use raw::{types::GlyphId, FontRef, TableProvider};
586
587 #[test]
588 fn direction_from_vectors() {
589 assert_eq!(Direction::new(-100, 0), Direction::Left);
590 assert_eq!(Direction::new(100, 0), Direction::Right);
591 assert_eq!(Direction::new(0, -100), Direction::Down);
592 assert_eq!(Direction::new(0, 100), Direction::Up);
593 assert_eq!(Direction::new(7, 100), Direction::Up);
594 assert_eq!(Direction::new(8, 100), Direction::None);
596 }
597
598 #[test]
599 fn direction_axes() {
600 use Direction::*;
601 let hori = [Left, Right];
602 let vert = [Up, Down];
603 for h in hori {
604 for h2 in hori {
605 assert!(h.is_same_axis(h2));
606 if h != h2 {
607 assert!(h.is_opposite(h2));
608 } else {
609 assert!(!h.is_opposite(h2));
610 }
611 }
612 for v in vert {
613 assert!(!h.is_same_axis(v));
614 assert!(!h.is_opposite(v));
615 }
616 }
617 for v in vert {
618 for v2 in vert {
619 assert!(v.is_same_axis(v2));
620 if v != v2 {
621 assert!(v.is_opposite(v2));
622 } else {
623 assert!(!v.is_opposite(v2));
624 }
625 }
626 }
627 }
628
629 #[test]
630 fn fill_outline() {
631 let outline = make_outline(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, 8);
632 use Direction::*;
633 let expected = &[
634 (107, 0, Left, Left, 3),
636 (85, 0, Left, None, 2),
637 (55, 26, None, Up, 2),
638 (55, 71, Up, Up, 3),
639 (55, 332, Up, Up, 3),
640 (55, 360, Up, None, 2),
641 (67, 411, None, None, 2),
642 (93, 459, None, None, 2),
643 (112, 481, None, Up, 1),
644 (112, 504, Up, Right, 1),
645 (168, 504, Right, Down, 1),
646 (168, 483, Down, None, 1),
647 (153, 473, None, None, 2),
648 (126, 428, None, None, 2),
649 (109, 366, None, Down, 2),
650 (109, 332, Down, Down, 3),
651 (109, 109, Down, Right, 1),
652 (407, 109, Right, Right, 3),
653 (427, 109, Right, None, 2),
654 (446, 136, None, None, 2),
655 (453, 169, None, Up, 2),
656 (453, 178, Up, Up, 3),
657 (453, 374, Up, Up, 3),
658 (453, 432, Up, None, 2),
659 (400, 483, None, Left, 2),
660 (362, 483, Left, Left, 3),
661 (109, 483, Left, Left, 3),
662 (86, 483, Left, None, 2),
663 (62, 517, None, Up, 2),
664 (62, 555, Up, Up, 3),
665 (62, 566, Up, None, 2),
666 (64, 587, None, None, 2),
667 (71, 619, None, None, 2),
668 (76, 647, None, Right, 1),
669 (103, 647, Right, Down, 9),
670 (103, 644, Down, Down, 3),
671 (103, 619, Down, None, 2),
672 (131, 592, None, Right, 2),
673 (155, 592, Right, Right, 3),
674 (386, 592, Right, Right, 3),
675 (437, 592, Right, None, 2),
676 (489, 552, None, None, 2),
677 (507, 485, None, Down, 2),
678 (507, 443, Down, Down, 3),
679 (507, 75, Down, Down, 3),
680 (507, 40, Down, None, 2),
681 (470, 0, None, Left, 2),
682 (436, 0, Left, Left, 3),
683 ];
684 let points = outline
685 .points
686 .iter()
687 .map(|point| {
688 (
689 point.fx,
690 point.fy,
691 point.in_dir,
692 point.out_dir,
693 point.flags.to_bits(),
694 )
695 })
696 .collect::<Vec<_>>();
697 assert_eq!(&points, expected);
698 }
699
700 #[test]
701 fn fill_sanity_checks_u16_index_bounds() {
702 let mut outline = Outline::default();
703 let gid = GlyphId::new(42);
704 let quirks = QuirksMode::Jit;
705 outline
706 .points
707 .resize_and_fill(u16::MAX as usize + 1, Point::default());
708 outline
709 .contours
710 .resize_and_fill(u16::MAX as usize + 1, Contour::default());
711 assert!(outline.analyze_and_validate(gid, quirks).is_ok());
712 outline
713 .points
714 .resize_and_fill(u16::MAX as usize + 2, Point::default());
715 assert!(matches!(
716 outline.analyze_and_validate(gid, quirks),
717 Err(DrawError::TooManyPoints(err_gid)) if err_gid == gid
718 ));
719 outline.points.clear();
720 outline
721 .contours
722 .resize_and_fill(u16::MAX as usize + 2, Contour::default());
723 assert!(matches!(
724 outline.analyze_and_validate(gid, quirks),
725 Err(DrawError::TooManyPoints(err_gid)) if err_gid == gid
726 ));
727 }
728
729 #[test]
730 fn link_points_handles_max_points_last_next_overflow() {
731 let mut outline = Outline::default();
732 outline
733 .points
734 .resize_and_fill(u16::MAX as usize + 1, Point::default());
735 outline.contours.push(Contour {
736 first_ix: 0,
737 last_ix: u16::MAX,
738 });
739 let quirks = QuirksMode::Jit;
740 outline.analyze_and_validate(0u32.into(), quirks).unwrap();
741 assert_eq!(outline.points[0].prev(), u16::MAX as usize);
744 assert_eq!(outline.points[0].next(), 1);
745 assert_eq!(
746 outline.points[u16::MAX as usize].prev(),
747 u16::MAX as usize - 1
748 );
749 assert_eq!(outline.points[u16::MAX as usize].next(), 0);
750 }
751
752 #[test]
753 fn orientation() {
754 let tt_outline = make_outline(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, 8);
755 assert_eq!(tt_outline.orientation, Some(Orientation::CounterClockwise));
757 let ps_outline = make_outline(font_test_data::CANTARELL_VF_TRIMMED, 4);
758 assert_eq!(ps_outline.orientation, Some(Orientation::Clockwise));
760 }
761
762 fn make_outline(font_data: &[u8], glyph_id: u32) -> Outline {
763 let font = FontRef::new(font_data).unwrap();
764 let glyphs = font.outline_glyphs();
765 let glyph = glyphs.get(GlyphId::from(glyph_id)).unwrap();
766 let mut outline = Outline::default();
767 outline.fill(&glyph, &[], Default::default()).unwrap();
768 outline
769 }
770
771 #[test]
772 fn mostly_off_curve_to_path_scan_backward() {
773 compare_path_conversion(font_test_data::MOSTLY_OFF_CURVE, PathStyle::FreeType);
774 }
775
776 #[test]
777 fn mostly_off_curve_to_path_scan_forward() {
778 compare_path_conversion(font_test_data::MOSTLY_OFF_CURVE, PathStyle::HarfBuzz);
779 }
780
781 #[test]
782 fn starting_off_curve_to_path_scan_backward() {
783 compare_path_conversion(font_test_data::STARTING_OFF_CURVE, PathStyle::FreeType);
784 }
785
786 #[test]
787 fn starting_off_curve_to_path_scan_forward() {
788 compare_path_conversion(font_test_data::STARTING_OFF_CURVE, PathStyle::HarfBuzz);
789 }
790
791 #[test]
792 fn cubic_to_path_scan_backward() {
793 compare_path_conversion(font_test_data::CUBIC_GLYF, PathStyle::FreeType);
794 }
795
796 #[test]
797 fn cubic_to_path_scan_forward() {
798 compare_path_conversion(font_test_data::CUBIC_GLYF, PathStyle::HarfBuzz);
799 }
800
801 #[test]
802 fn cff_to_path_scan_backward() {
803 compare_path_conversion(font_test_data::CANTARELL_VF_TRIMMED, PathStyle::FreeType);
804 }
805
806 #[test]
807 fn cff_to_path_scan_forward() {
808 compare_path_conversion(font_test_data::CANTARELL_VF_TRIMMED, PathStyle::HarfBuzz);
809 }
810
811 fn compare_path_conversion(font_data: &[u8], path_style: PathStyle) {
815 let font = FontRef::new(font_data).unwrap();
816 let glyph_count = font.maxp().unwrap().num_glyphs();
817 let glyphs = font.outline_glyphs();
818 let mut results = Vec::new();
819 for gid in 0..glyph_count {
821 let glyph = glyphs.get(GlyphId::from(gid)).unwrap();
822 let mut base_svg = SvgPen::default();
824 let settings = DrawSettings::unhinted(Size::unscaled(), LocationRef::default())
825 .with_path_style(path_style);
826 glyph.draw(settings, &mut base_svg).unwrap();
827 let base_svg = base_svg.to_string();
828 let mut outline = Outline::default();
830 outline.fill(&glyph, &[], Default::default()).unwrap();
831 for point in &mut outline.points {
835 point.x = point.fx << 6;
836 point.y = point.fy << 6;
837 }
838 let mut autohint_svg = SvgPen::default();
839 outline.to_path(path_style, &mut autohint_svg).unwrap();
840 let autohint_svg = autohint_svg.to_string();
841 if base_svg != autohint_svg {
842 results.push((gid, base_svg, autohint_svg));
843 }
844 }
845 if !results.is_empty() {
846 let report: String = results
847 .into_iter()
848 .map(|(gid, expected, got)| {
849 format!("[glyph {gid}]\nexpected: {expected}\n got: {got}")
850 })
851 .collect::<Vec<_>>()
852 .join("\n");
853 panic!("outline to path comparison failed:\n{report}");
854 }
855 }
856}