Skip to main content

skrifa/outline/autohint/
outline.rs

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