skrifa/outline/autohint/
outline.rs

1//! Outline representation and helpers for autohinting.
2
3use super::{
4    super::{
5        path,
6        pen::PathStyle,
7        unscaled::{UnscaledOutlineSink, UnscaledPoint},
8        DrawError, LocationRef, OutlineGlyph, OutlinePen,
9    },
10    metrics::Scale,
11};
12use crate::collections::SmallVec;
13use core::ops::Range;
14use raw::{
15    tables::glyf::{PointFlags, PointMarker},
16    types::{F26Dot6, F2Dot14},
17};
18
19/// Hinting directions.
20///
21/// The values are such that `dir1 + dir2 == 0` when the directions are
22/// opposite.
23///
24/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L45>
25#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
26#[repr(i8)]
27pub(crate) enum Direction {
28    #[default]
29    None = 4,
30    Right = 1,
31    Left = -1,
32    Up = 2,
33    Down = -2,
34}
35
36impl Direction {
37    /// Computes a direction from a vector.
38    ///
39    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L751>
40    pub fn new(dx: i32, dy: i32) -> Self {
41        let (dir, long_arm, short_arm) = if dy >= dx {
42            if dy >= -dx {
43                (Direction::Up, dy, dx)
44            } else {
45                (Direction::Left, -dx, dy)
46            }
47        } else if dy >= -dx {
48            (Direction::Right, dx, dy)
49        } else {
50            (Direction::Down, -dy, dx)
51        };
52        // Return no direction if arm lengths do not differ enough.
53        // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L789>
54        if long_arm <= 14 * short_arm.abs() {
55            Direction::None
56        } else {
57            dir
58        }
59    }
60
61    pub fn is_opposite(self, other: Self) -> bool {
62        self as i8 + other as i8 == 0
63    }
64
65    pub fn is_same_axis(self, other: Self) -> bool {
66        (self as i8).abs() == (other as i8).abs()
67    }
68
69    pub fn normalize(self) -> Self {
70        // FreeType uses absolute value for this.
71        match self {
72            Self::Left => Self::Right,
73            Self::Down => Self::Up,
74            _ => self,
75        }
76    }
77}
78
79/// The overall orientation of an outline.
80#[derive(Copy, Clone, PartialEq, Eq, Debug)]
81pub(crate) enum Orientation {
82    Clockwise,
83    CounterClockwise,
84}
85
86/// Outline point with a lot of context for hinting.
87///
88/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L239>
89#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
90pub(crate) struct Point {
91    /// Describes the type and hinting state of the point.
92    pub flags: PointFlags,
93    /// X coordinate in font units.
94    pub fx: i32,
95    /// Y coordinate in font units.
96    pub fy: i32,
97    /// Scaled X coordinate.
98    pub ox: i32,
99    /// Scaled Y coordinate.
100    pub oy: i32,
101    /// Hinted X coordinate.
102    pub x: i32,
103    /// Hinted Y coordinate.
104    pub y: i32,
105    /// Direction of inwards vector.
106    pub in_dir: Direction,
107    /// Direction of outwards vector.
108    pub out_dir: Direction,
109    /// Context dependent coordinate.
110    pub u: i32,
111    /// Context dependent coordinate.
112    pub v: i32,
113    /// Index of next point in contour.
114    pub next_ix: u16,
115    /// Index of previous point in contour.
116    pub prev_ix: u16,
117}
118
119impl Point {
120    pub fn is_on_curve(&self) -> bool {
121        self.flags.is_on_curve()
122    }
123
124    /// Returns the index of the next point in the contour.
125    pub fn next(&self) -> usize {
126        self.next_ix as usize
127    }
128
129    /// Returns the index of the previous point in the contour.
130    pub fn prev(&self) -> usize {
131        self.prev_ix as usize
132    }
133
134    #[inline(always)]
135    fn as_contour_point(&self) -> path::ContourPoint<F26Dot6> {
136        path::ContourPoint {
137            x: F26Dot6::from_bits(self.x),
138            y: F26Dot6::from_bits(self.y),
139            flags: self.flags,
140        }
141    }
142}
143
144// Matches FreeType's inline usage
145//
146// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L332>
147const MAX_INLINE_POINTS: usize = 96;
148const MAX_INLINE_CONTOURS: usize = 8;
149
150#[derive(Default)]
151pub(crate) struct Outline {
152    pub units_per_em: i32,
153    pub orientation: Option<Orientation>,
154    pub points: SmallVec<Point, MAX_INLINE_POINTS>,
155    pub contours: SmallVec<Contour, MAX_INLINE_CONTOURS>,
156    pub advance: i32,
157}
158
159impl Outline {
160    /// Fills the outline from the given glyph.
161    pub fn fill(&mut self, glyph: &OutlineGlyph, coords: &[F2Dot14]) -> Result<(), DrawError> {
162        self.clear();
163        let advance = glyph.draw_unscaled(LocationRef::new(coords), None, self)?;
164        self.advance = advance;
165        self.units_per_em = glyph.units_per_em() as i32;
166        // Heuristic value
167        let near_limit = 20 * self.units_per_em / 2048;
168        self.link_points();
169        self.mark_near_points(near_limit);
170        self.compute_directions(near_limit);
171        self.simplify_topology();
172        self.check_remaining_weak_points();
173        self.compute_orientation();
174        Ok(())
175    }
176
177    /// Applies dimension specific scaling factors and deltas to each
178    /// point in the outline.
179    pub fn scale(&mut self, scale: &Scale) {
180        use super::metrics::fixed_mul;
181        for point in &mut self.points {
182            let x = fixed_mul(point.fx, scale.x_scale) + scale.x_delta;
183            let y = fixed_mul(point.fy, scale.y_scale) + scale.y_delta;
184            point.ox = x;
185            point.x = x;
186            point.oy = y;
187            point.y = y;
188        }
189    }
190
191    pub fn clear(&mut self) {
192        self.units_per_em = 0;
193        self.points.clear();
194        self.contours.clear();
195        self.advance = 0;
196    }
197
198    pub fn to_path(
199        &self,
200        style: PathStyle,
201        pen: &mut impl OutlinePen,
202    ) -> Result<(), path::ToPathError> {
203        for contour in &self.contours {
204            let Some(points) = self.points.get(contour.range()) else {
205                continue;
206            };
207            if let (Some(first_point), Some(last_point)) = (
208                points.first().map(Point::as_contour_point),
209                points.last().map(Point::as_contour_point),
210            ) {
211                path::contour_to_path(
212                    points.iter().map(Point::as_contour_point),
213                    first_point,
214                    last_point,
215                    style,
216                    pen,
217                )?;
218            }
219        }
220        Ok(())
221    }
222}
223
224impl Outline {
225    /// Sets next and previous indices for each point.
226    fn link_points(&mut self) {
227        let points = self.points.as_mut_slice();
228        for contour in &self.contours {
229            let Some(points) = points.get_mut(contour.range()) else {
230                continue;
231            };
232            let first_ix = contour.first() as u16;
233            let mut prev_ix = contour.last() as u16;
234            for (ix, point) in points.iter_mut().enumerate() {
235                let ix = ix as u16 + first_ix;
236                point.prev_ix = prev_ix;
237                prev_ix = ix;
238                point.next_ix = ix + 1;
239            }
240            points.last_mut().unwrap().next_ix = first_ix;
241        }
242    }
243
244    /// Computes the near flag for each contour.
245    ///
246    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1017>
247    fn mark_near_points(&mut self, near_limit: i32) {
248        let points = self.points.as_mut_slice();
249        for contour in &self.contours {
250            let mut prev_ix = contour.last();
251            for ix in contour.range() {
252                let point = points[ix];
253                let prev = &mut points[prev_ix];
254                // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1017>
255                let out_x = point.fx - prev.fx;
256                let out_y = point.fy - prev.fy;
257                if out_x.abs() + out_y.abs() < near_limit {
258                    prev.flags.set_marker(PointMarker::NEAR);
259                }
260                prev_ix = ix;
261            }
262        }
263    }
264
265    /// Compute directions of in and out vectors.
266    ///
267    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1064>
268    fn compute_directions(&mut self, near_limit: i32) {
269        let near_limit2 = 2 * near_limit - 1;
270        let points = self.points.as_mut_slice();
271        for contour in &self.contours {
272            // Walk backward to find the first non-near point.
273            let mut first_ix = contour.first();
274            let mut ix = first_ix;
275            let mut prev_ix = contour.prev(first_ix);
276            let mut point = points[first_ix];
277            while prev_ix != first_ix {
278                let prev = points[prev_ix];
279                let out_x = point.fx - prev.fx;
280                let out_y = point.fy - prev.fy;
281                // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1102>
282                if out_x.abs() + out_y.abs() >= near_limit2 {
283                    break;
284                }
285                point = prev;
286                ix = prev_ix;
287                prev_ix = contour.prev(prev_ix);
288            }
289            first_ix = ix;
290            // Abuse u and v fields to store deltas to the next and previous
291            // non-near points, respectively.
292            let first = &mut points[first_ix];
293            first.u = first_ix as _;
294            first.v = first_ix as _;
295            let mut next_ix = first_ix;
296            let mut ix = first_ix;
297            // Now loop over all points in the contour to compute in and
298            // out directions
299            let mut out_x = 0;
300            let mut out_y = 0;
301            loop {
302                let point_ix = next_ix;
303                next_ix = contour.next(point_ix);
304                let point = points[point_ix];
305                let next = &mut points[next_ix];
306                // Accumulate the deltas until we surpass near_limit
307                out_x += next.fx - point.fx;
308                out_y += next.fy - point.fy;
309                if out_x.abs() + out_y.abs() < near_limit {
310                    next.flags.set_marker(PointMarker::WEAK_INTERPOLATION);
311                    // The original code is a do-while loop, so make
312                    // sure we keep this condition before the continue
313                    if next_ix == first_ix {
314                        break;
315                    }
316                    continue;
317                }
318                let out_dir = Direction::new(out_x, out_y);
319                next.in_dir = out_dir;
320                next.v = ix as _;
321                let cur = &mut points[ix];
322                cur.u = next_ix as _;
323                cur.out_dir = out_dir;
324                // Adjust directions for all intermediate points
325                let mut inter_ix = contour.next(ix);
326                while inter_ix != next_ix {
327                    let point = &mut points[inter_ix];
328                    point.in_dir = out_dir;
329                    point.out_dir = out_dir;
330                    inter_ix = contour.next(inter_ix);
331                }
332                ix = next_ix;
333                points[ix].u = first_ix as _;
334                points[first_ix].v = ix as _;
335                out_x = 0;
336                out_y = 0;
337                if next_ix == first_ix {
338                    break;
339                }
340            }
341        }
342    }
343
344    /// Simplify so that we can identify local extrema more reliably.
345    ///
346    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1181>
347    fn simplify_topology(&mut self) {
348        let points = self.points.as_mut_slice();
349        for i in 0..points.len() {
350            let point = points[i];
351            if point.flags.has_marker(PointMarker::WEAK_INTERPOLATION) {
352                continue;
353            }
354            if point.in_dir == Direction::None && point.out_dir == Direction::None {
355                let u_index = point.u as usize;
356                let v_index = point.v as usize;
357                let next_u = points[u_index];
358                let prev_v = points[v_index];
359                let in_x = point.fx - prev_v.fx;
360                let in_y = point.fy - prev_v.fy;
361                let out_x = next_u.fx - point.fx;
362                let out_y = next_u.fy - point.fy;
363                if (in_x ^ out_x) >= 0 && (in_y ^ out_y) >= 0 {
364                    // Both vectors point into the same quadrant
365                    points[i].flags.set_marker(PointMarker::WEAK_INTERPOLATION);
366                    points[v_index].u = u_index as _;
367                    points[u_index].v = v_index as _;
368                }
369            }
370        }
371    }
372
373    /// Check for remaining weak points.
374    ///
375    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1226>
376    fn check_remaining_weak_points(&mut self) {
377        let points = self.points.as_mut_slice();
378        for i in 0..points.len() {
379            let point = points[i];
380            let mut make_weak = false;
381            if point.flags.has_marker(PointMarker::WEAK_INTERPOLATION) {
382                // Already weak
383                continue;
384            }
385            if !point.flags.is_on_curve() {
386                // Control points are always weak
387                make_weak = true;
388            } else if point.out_dir == point.in_dir {
389                if point.out_dir != Direction::None {
390                    // Point lies on a vertical or horizontal segment but
391                    // not at start or end
392                    make_weak = true;
393                } else {
394                    let u_index = point.u as usize;
395                    let v_index = point.v as usize;
396                    let next_u = points[u_index];
397                    let prev_v = points[v_index];
398                    if is_corner_flat(
399                        point.fx - prev_v.fx,
400                        point.fy - prev_v.fy,
401                        next_u.fx - point.fx,
402                        next_u.fy - point.fy,
403                    ) {
404                        // One of the vectors is more dominant
405                        make_weak = true;
406                        points[v_index].u = u_index as _;
407                        points[u_index].v = v_index as _;
408                    }
409                }
410            } else if point.in_dir.is_opposite(point.out_dir) {
411                // Point forms a "spike"
412                make_weak = true;
413            }
414            if make_weak {
415                points[i].flags.set_marker(PointMarker::WEAK_INTERPOLATION);
416            }
417        }
418    }
419
420    /// Computes the overall winding order of the outline.
421    ///
422    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftoutln.c#L1049>
423    fn compute_orientation(&mut self) {
424        self.orientation = None;
425        let points = self.points.as_slice();
426        if points.is_empty() {
427            return;
428        }
429        fn point_to_i64(point: &Point) -> (i64, i64) {
430            (point.fx as i64, point.fy as i64)
431        }
432        let mut area = 0i64;
433        for contour in &self.contours {
434            let last_ix = contour.last();
435            let first_ix = contour.first();
436            let (mut prev_x, mut prev_y) = point_to_i64(&points[last_ix]);
437            for point in &points[first_ix..=last_ix] {
438                let (x, y) = point_to_i64(point);
439                area += (y - prev_y) * (x + prev_x);
440                (prev_x, prev_y) = (x, y);
441            }
442        }
443        use core::cmp::Ordering;
444        self.orientation = match area.cmp(&0) {
445            Ordering::Less => Some(Orientation::CounterClockwise),
446            Ordering::Greater => Some(Orientation::Clockwise),
447            Ordering::Equal => None,
448        };
449    }
450}
451
452/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftcalc.c#L1026>
453fn is_corner_flat(in_x: i32, in_y: i32, out_x: i32, out_y: i32) -> bool {
454    let ax = in_x + out_x;
455    let ay = in_y + out_y;
456    fn hypot(x: i32, y: i32) -> i32 {
457        let x = x.abs();
458        let y = y.abs();
459        if x > y {
460            x + ((3 * y) >> 3)
461        } else {
462            y + ((3 * x) >> 3)
463        }
464    }
465    let d_in = hypot(in_x, in_y);
466    let d_out = hypot(out_x, out_y);
467    let d_hypot = hypot(ax, ay);
468    (d_in + d_out - d_hypot) < (d_hypot >> 4)
469}
470
471#[derive(Copy, Clone, Default, Debug)]
472pub(crate) struct Contour {
473    first_ix: u16,
474    last_ix: u16,
475}
476
477impl Contour {
478    pub fn first(self) -> usize {
479        self.first_ix as usize
480    }
481
482    pub fn last(self) -> usize {
483        self.last_ix as usize
484    }
485
486    pub fn next(self, index: usize) -> usize {
487        if index >= self.last_ix as usize {
488            self.first_ix as usize
489        } else {
490            index + 1
491        }
492    }
493
494    pub fn prev(self, index: usize) -> usize {
495        if index <= self.first_ix as usize {
496            self.last_ix as usize
497        } else {
498            index - 1
499        }
500    }
501
502    pub fn range(self) -> Range<usize> {
503        self.first()..self.last() + 1
504    }
505}
506
507impl UnscaledOutlineSink for Outline {
508    fn try_reserve(&mut self, additional: usize) -> Result<(), DrawError> {
509        if self.points.try_reserve(additional) {
510            Ok(())
511        } else {
512            Err(DrawError::InsufficientMemory)
513        }
514    }
515
516    fn push(&mut self, point: UnscaledPoint) -> Result<(), DrawError> {
517        let new_point = Point {
518            flags: point.flags,
519            fx: point.x as i32,
520            fy: point.y as i32,
521            ..Default::default()
522        };
523        let new_point_ix: u16 = self
524            .points
525            .len()
526            .try_into()
527            .map_err(|_| DrawError::InsufficientMemory)?;
528        if point.is_contour_start {
529            self.contours.push(Contour {
530                first_ix: new_point_ix,
531                last_ix: new_point_ix,
532            });
533        } else if let Some(last_contour) = self.contours.last_mut() {
534            last_contour.last_ix += 1;
535        } else {
536            // If our first point is not marked as contour start, just
537            // create a new contour.
538            self.contours.push(Contour {
539                first_ix: new_point_ix,
540                last_ix: new_point_ix,
541            });
542        }
543        self.points.push(new_point);
544        Ok(())
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::super::super::{pen::SvgPen, DrawSettings};
551    use super::*;
552    use crate::{prelude::Size, MetadataProvider};
553    use raw::{types::GlyphId, FontRef, TableProvider};
554
555    #[test]
556    fn direction_from_vectors() {
557        assert_eq!(Direction::new(-100, 0), Direction::Left);
558        assert_eq!(Direction::new(100, 0), Direction::Right);
559        assert_eq!(Direction::new(0, -100), Direction::Down);
560        assert_eq!(Direction::new(0, 100), Direction::Up);
561        assert_eq!(Direction::new(7, 100), Direction::Up);
562        // This triggers the too close heuristic
563        assert_eq!(Direction::new(8, 100), Direction::None);
564    }
565
566    #[test]
567    fn direction_axes() {
568        use Direction::*;
569        let hori = [Left, Right];
570        let vert = [Up, Down];
571        for h in hori {
572            for h2 in hori {
573                assert!(h.is_same_axis(h2));
574                if h != h2 {
575                    assert!(h.is_opposite(h2));
576                } else {
577                    assert!(!h.is_opposite(h2));
578                }
579            }
580            for v in vert {
581                assert!(!h.is_same_axis(v));
582                assert!(!h.is_opposite(v));
583            }
584        }
585        for v in vert {
586            for v2 in vert {
587                assert!(v.is_same_axis(v2));
588                if v != v2 {
589                    assert!(v.is_opposite(v2));
590                } else {
591                    assert!(!v.is_opposite(v2));
592                }
593            }
594        }
595    }
596
597    #[test]
598    fn fill_outline() {
599        let outline = make_outline(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, 8);
600        use Direction::*;
601        let expected = &[
602            // (x, y, in_dir, out_dir, flags)
603            (107, 0, Left, Left, 3),
604            (85, 0, Left, None, 2),
605            (55, 26, None, Up, 2),
606            (55, 71, Up, Up, 3),
607            (55, 332, Up, Up, 3),
608            (55, 360, Up, None, 2),
609            (67, 411, None, None, 2),
610            (93, 459, None, None, 2),
611            (112, 481, None, Up, 1),
612            (112, 504, Up, Right, 1),
613            (168, 504, Right, Down, 1),
614            (168, 483, Down, None, 1),
615            (153, 473, None, None, 2),
616            (126, 428, None, None, 2),
617            (109, 366, None, Down, 2),
618            (109, 332, Down, Down, 3),
619            (109, 109, Down, Right, 1),
620            (407, 109, Right, Right, 3),
621            (427, 109, Right, None, 2),
622            (446, 136, None, None, 2),
623            (453, 169, None, Up, 2),
624            (453, 178, Up, Up, 3),
625            (453, 374, Up, Up, 3),
626            (453, 432, Up, None, 2),
627            (400, 483, None, Left, 2),
628            (362, 483, Left, Left, 3),
629            (109, 483, Left, Left, 3),
630            (86, 483, Left, None, 2),
631            (62, 517, None, Up, 2),
632            (62, 555, Up, Up, 3),
633            (62, 566, Up, None, 2),
634            (64, 587, None, None, 2),
635            (71, 619, None, None, 2),
636            (76, 647, None, Right, 1),
637            (103, 647, Right, Down, 9),
638            (103, 644, Down, Down, 3),
639            (103, 619, Down, None, 2),
640            (131, 592, None, Right, 2),
641            (155, 592, Right, Right, 3),
642            (386, 592, Right, Right, 3),
643            (437, 592, Right, None, 2),
644            (489, 552, None, None, 2),
645            (507, 485, None, Down, 2),
646            (507, 443, Down, Down, 3),
647            (507, 75, Down, Down, 3),
648            (507, 40, Down, None, 2),
649            (470, 0, None, Left, 2),
650            (436, 0, Left, Left, 3),
651        ];
652        let points = outline
653            .points
654            .iter()
655            .map(|point| {
656                (
657                    point.fx,
658                    point.fy,
659                    point.in_dir,
660                    point.out_dir,
661                    point.flags.to_bits(),
662                )
663            })
664            .collect::<Vec<_>>();
665        assert_eq!(&points, expected);
666    }
667
668    #[test]
669    fn orientation() {
670        let tt_outline = make_outline(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, 8);
671        // TrueType outlines are counter clockwise
672        assert_eq!(tt_outline.orientation, Some(Orientation::CounterClockwise));
673        let ps_outline = make_outline(font_test_data::CANTARELL_VF_TRIMMED, 4);
674        // PostScript outlines are clockwise
675        assert_eq!(ps_outline.orientation, Some(Orientation::Clockwise));
676    }
677
678    fn make_outline(font_data: &[u8], glyph_id: u32) -> Outline {
679        let font = FontRef::new(font_data).unwrap();
680        let glyphs = font.outline_glyphs();
681        let glyph = glyphs.get(GlyphId::from(glyph_id)).unwrap();
682        let mut outline = Outline::default();
683        outline.fill(&glyph, Default::default()).unwrap();
684        outline
685    }
686
687    #[test]
688    fn mostly_off_curve_to_path_scan_backward() {
689        compare_path_conversion(font_test_data::MOSTLY_OFF_CURVE, PathStyle::FreeType);
690    }
691
692    #[test]
693    fn mostly_off_curve_to_path_scan_forward() {
694        compare_path_conversion(font_test_data::MOSTLY_OFF_CURVE, PathStyle::HarfBuzz);
695    }
696
697    #[test]
698    fn starting_off_curve_to_path_scan_backward() {
699        compare_path_conversion(font_test_data::STARTING_OFF_CURVE, PathStyle::FreeType);
700    }
701
702    #[test]
703    fn starting_off_curve_to_path_scan_forward() {
704        compare_path_conversion(font_test_data::STARTING_OFF_CURVE, PathStyle::HarfBuzz);
705    }
706
707    #[test]
708    fn cubic_to_path_scan_backward() {
709        compare_path_conversion(font_test_data::CUBIC_GLYF, PathStyle::FreeType);
710    }
711
712    #[test]
713    fn cubic_to_path_scan_forward() {
714        compare_path_conversion(font_test_data::CUBIC_GLYF, PathStyle::HarfBuzz);
715    }
716
717    #[test]
718    fn cff_to_path_scan_backward() {
719        compare_path_conversion(font_test_data::CANTARELL_VF_TRIMMED, PathStyle::FreeType);
720    }
721
722    #[test]
723    fn cff_to_path_scan_forward() {
724        compare_path_conversion(font_test_data::CANTARELL_VF_TRIMMED, PathStyle::HarfBuzz);
725    }
726
727    /// Ensures autohint path conversion matches the base scaler path
728    /// conversion for all glyphs in the given font with a certain
729    /// path style.
730    fn compare_path_conversion(font_data: &[u8], path_style: PathStyle) {
731        let font = FontRef::new(font_data).unwrap();
732        let glyph_count = font.maxp().unwrap().num_glyphs();
733        let glyphs = font.outline_glyphs();
734        let mut results = Vec::new();
735        // And all glyphs
736        for gid in 0..glyph_count {
737            let glyph = glyphs.get(GlyphId::from(gid)).unwrap();
738            // Unscaled, unhinted code path
739            let mut base_svg = SvgPen::default();
740            let settings = DrawSettings::unhinted(Size::unscaled(), LocationRef::default())
741                .with_path_style(path_style);
742            glyph.draw(settings, &mut base_svg).unwrap();
743            let base_svg = base_svg.to_string();
744            // Autohinter outline code path
745            let mut outline = Outline::default();
746            outline.fill(&glyph, Default::default()).unwrap();
747            // The to_path method uses the (x, y) coords which aren't filled
748            // until we scale (and we aren't doing that here) so update
749            // them with 26.6 values manually
750            for point in &mut outline.points {
751                point.x = point.fx << 6;
752                point.y = point.fy << 6;
753            }
754            let mut autohint_svg = SvgPen::default();
755            outline.to_path(path_style, &mut autohint_svg).unwrap();
756            let autohint_svg = autohint_svg.to_string();
757            if base_svg != autohint_svg {
758                results.push((gid, base_svg, autohint_svg));
759            }
760        }
761        if !results.is_empty() {
762            let report: String = results
763                .into_iter()
764                .map(|(gid, expected, got)| {
765                    format!("[glyph {gid}]\nexpected: {expected}\n     got: {got}")
766                })
767                .collect::<Vec<_>>()
768                .join("\n");
769            panic!("outline to path comparison failed:\n{report}");
770        }
771    }
772}