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