Skip to main content

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