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