skrifa/outline/
path.rs

1//! TrueType style outline to path conversion.
2
3use super::pen::{OutlinePen, PathStyle};
4use core::fmt;
5use raw::{
6    tables::glyf::{PointCoord, PointFlags},
7    types::Point,
8};
9
10/// Errors that can occur when converting an outline to a path.
11#[derive(Clone, Debug)]
12pub enum ToPathError {
13    /// Contour end point at this index was less than its preceding end point.
14    ContourOrder(usize),
15    /// Expected a quadratic off-curve point at this index.
16    ExpectedQuad(usize),
17    /// Expected a quadratic off-curve or on-curve point at this index.
18    ExpectedQuadOrOnCurve(usize),
19    /// Expected a cubic off-curve point at this index.
20    ExpectedCubic(usize),
21    /// Expected number of points to == number of flags
22    PointFlagMismatch { num_points: usize, num_flags: usize },
23}
24
25impl fmt::Display for ToPathError {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        match self {
28            Self::ContourOrder(ix) => write!(
29                f,
30                "Contour end point at index {ix} was less than preceding end point"
31            ),
32            Self::ExpectedQuad(ix) => write!(f, "Expected quadatic off-curve point at index {ix}"),
33            Self::ExpectedQuadOrOnCurve(ix) => write!(
34                f,
35                "Expected quadatic off-curve or on-curve point at index {ix}"
36            ),
37            Self::ExpectedCubic(ix) => write!(f, "Expected cubic off-curve point at index {ix}"),
38            Self::PointFlagMismatch {
39                num_points,
40                num_flags,
41            } => write!(
42                f,
43                "Number of points ({num_points}) and flags ({num_flags}) must match"
44            ),
45        }
46    }
47}
48
49/// Converts a `glyf` outline described by points, flags and contour end points
50/// to a sequence of path elements and invokes the appropriate callback on the
51/// given pen for each.
52///
53/// The input points can have any coordinate type that implements
54/// [`PointCoord`]. Output points are always generated in `f32`.
55///
56/// This is roughly equivalent to [`FT_Outline_Decompose`](https://freetype.org/freetype2/docs/reference/ft2-outline_processing.html#ft_outline_decompose).
57///
58/// See [`contour_to_path`] for a more general function that takes an iterator
59/// if your outline data is in a different format.
60pub(crate) fn to_path<C: PointCoord>(
61    points: &[Point<C>],
62    flags: &[PointFlags],
63    contours: &[u16],
64    path_style: PathStyle,
65    pen: &mut impl OutlinePen,
66) -> Result<(), ToPathError> {
67    for contour_ix in 0..contours.len() {
68        let start_ix = (contour_ix > 0)
69            .then(|| contours[contour_ix - 1] as usize + 1)
70            .unwrap_or_default();
71        let end_ix = contours[contour_ix] as usize;
72        if end_ix < start_ix || end_ix >= points.len() {
73            return Err(ToPathError::ContourOrder(contour_ix));
74        }
75        let points = &points[start_ix..=end_ix];
76        if points.is_empty() {
77            continue;
78        }
79        let flags = flags
80            .get(start_ix..=end_ix)
81            .ok_or(ToPathError::PointFlagMismatch {
82                num_points: points.len(),
83                num_flags: flags.len(),
84            })?;
85        let last_point = points.last().unwrap();
86        let last_flags = flags.last().unwrap();
87        let last_point = ContourPoint {
88            x: last_point.x,
89            y: last_point.y,
90            flags: *last_flags,
91        };
92        contour_to_path(
93            points.iter().zip(flags).map(|(point, flags)| ContourPoint {
94                x: point.x,
95                y: point.y,
96                flags: *flags,
97            }),
98            last_point,
99            path_style,
100            pen,
101        )
102        .map_err(|e| match &e {
103            ToPathError::ExpectedCubic(ix) => ToPathError::ExpectedCubic(ix + start_ix),
104            ToPathError::ExpectedQuad(ix) => ToPathError::ExpectedQuad(ix + start_ix),
105            ToPathError::ExpectedQuadOrOnCurve(ix) => {
106                ToPathError::ExpectedQuadOrOnCurve(ix + start_ix)
107            }
108            _ => e,
109        })?
110    }
111    Ok(())
112}
113
114/// Combination of point coordinates and flags.
115#[derive(Copy, Clone, Default, Debug)]
116pub(crate) struct ContourPoint<T> {
117    pub x: T,
118    pub y: T,
119    pub flags: PointFlags,
120}
121
122impl<T> ContourPoint<T>
123where
124    T: PointCoord,
125{
126    fn point_f32(&self) -> Point<f32> {
127        Point::new(self.x.to_f32(), self.y.to_f32())
128    }
129
130    fn midpoint(&self, other: Self) -> ContourPoint<T> {
131        let (x, y) = (self.x.midpoint(other.x), self.y.midpoint(other.y));
132        Self {
133            x,
134            y,
135            flags: other.flags,
136        }
137    }
138}
139
140/// Generates a path from an iterator of contour points.
141///
142/// Note that this requires the last point of the contour to be passed
143/// separately to support FreeType style path conversion when the contour
144/// begins with an off curve point. The points iterator should still
145/// yield the last point as well.
146///
147/// This is more general than [`to_path`] and exists to support cases (such as
148/// autohinting) where the source outline data is in a different format.
149pub(crate) fn contour_to_path<C: PointCoord>(
150    points: impl Iterator<Item = ContourPoint<C>>,
151    last_point: ContourPoint<C>,
152    style: PathStyle,
153    pen: &mut impl OutlinePen,
154) -> Result<(), ToPathError> {
155    let mut points = points.enumerate().peekable();
156    let Some((_, first_point)) = points.peek().copied() else {
157        // This is an empty contour
158        return Ok(());
159    };
160    // We don't accept an off curve cubic as the first point
161    if first_point.flags.is_off_curve_cubic() {
162        return Err(ToPathError::ExpectedQuadOrOnCurve(0));
163    }
164    // For FreeType style, we may need to omit the last point if we find the
165    // first on curve there
166    let mut omit_last = false;
167    // For HarfBuzz style, may skip up to two points in finding the start, so
168    // process these at the end
169    let mut trailing_points = [None; 2];
170    // Find our starting point
171    let start_point = if first_point.flags.is_off_curve_quad() {
172        // We're starting with an off curve, so select our first move based on
173        // the path style
174        match style {
175            PathStyle::FreeType => {
176                if last_point.flags.is_on_curve() {
177                    // The last point is an on curve, so let's start there
178                    omit_last = true;
179                    last_point
180                } else {
181                    // It's also an off curve, so take implicit midpoint
182                    last_point.midpoint(first_point)
183                }
184            }
185            PathStyle::HarfBuzz => {
186                // Always consume the first point
187                points.next();
188                // Then check the next point
189                let Some((_, next_point)) = points.peek().copied() else {
190                    // This is a single point contour
191                    return Ok(());
192                };
193                if next_point.flags.is_on_curve() {
194                    points.next();
195                    trailing_points = [Some((0, first_point)), Some((1, next_point))];
196                    // Next is on curve, so let's start there
197                    next_point
198                } else {
199                    // It's also an off curve, so take the implicit midpoint
200                    trailing_points = [Some((0, first_point)), None];
201                    first_point.midpoint(next_point)
202                }
203            }
204        }
205    } else {
206        // We're starting with an on curve, so consume the point
207        points.next();
208        first_point
209    };
210    let point = start_point.point_f32();
211    pen.move_to(point.x, point.y);
212    let mut state = PendingState::default();
213    if omit_last {
214        while let Some((ix, point)) = points.next() {
215            if points.peek().is_none() {
216                break;
217            }
218            state.emit(ix, point, pen)?;
219        }
220    } else {
221        for (ix, point) in points {
222            state.emit(ix, point, pen)?;
223        }
224    }
225    for (ix, point) in trailing_points.iter().filter_map(|x| *x) {
226        state.emit(ix, point, pen)?;
227    }
228    state.finish(0, start_point, pen)?;
229    Ok(())
230}
231
232#[derive(Copy, Clone, Default)]
233enum PendingState<C> {
234    /// No pending points.
235    #[default]
236    Empty,
237    /// Pending off-curve quad point.
238    PendingQuad(ContourPoint<C>),
239    /// Single pending off-curve cubic point.
240    PendingCubic(ContourPoint<C>),
241    /// Two pending off-curve cubic points.
242    TwoPendingCubics(ContourPoint<C>, ContourPoint<C>),
243}
244
245impl<C> PendingState<C>
246where
247    C: PointCoord,
248{
249    #[inline(always)]
250    fn emit(
251        &mut self,
252        ix: usize,
253        point: ContourPoint<C>,
254        pen: &mut impl OutlinePen,
255    ) -> Result<(), ToPathError> {
256        let flags = point.flags;
257        match *self {
258            Self::Empty => {
259                if flags.is_off_curve_quad() {
260                    *self = Self::PendingQuad(point);
261                } else if flags.is_off_curve_cubic() {
262                    *self = Self::PendingCubic(point);
263                } else {
264                    let p = point.point_f32();
265                    pen.line_to(p.x, p.y);
266                }
267            }
268            Self::PendingQuad(quad) => {
269                if flags.is_off_curve_quad() {
270                    let c0 = quad.point_f32();
271                    let p = quad.midpoint(point).point_f32();
272                    pen.quad_to(c0.x, c0.y, p.x, p.y);
273                    *self = Self::PendingQuad(point);
274                } else if flags.is_off_curve_cubic() {
275                    return Err(ToPathError::ExpectedQuadOrOnCurve(ix));
276                } else {
277                    let c0 = quad.point_f32();
278                    let p = point.point_f32();
279                    pen.quad_to(c0.x, c0.y, p.x, p.y);
280                    *self = Self::Empty;
281                }
282            }
283            Self::PendingCubic(cubic) => {
284                if flags.is_off_curve_cubic() {
285                    *self = Self::TwoPendingCubics(cubic, point);
286                } else {
287                    return Err(ToPathError::ExpectedCubic(ix));
288                }
289            }
290            Self::TwoPendingCubics(cubic0, cubic1) => {
291                if flags.is_off_curve_quad() {
292                    return Err(ToPathError::ExpectedCubic(ix));
293                } else if flags.is_off_curve_cubic() {
294                    let c0 = cubic0.point_f32();
295                    let c1 = cubic1.point_f32();
296                    let p = cubic1.midpoint(point).point_f32();
297                    pen.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
298                    *self = Self::PendingCubic(point);
299                } else {
300                    let c0 = cubic0.point_f32();
301                    let c1 = cubic1.point_f32();
302                    let p = point.point_f32();
303                    pen.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
304                    *self = Self::Empty;
305                }
306            }
307        }
308        Ok(())
309    }
310
311    fn finish(
312        mut self,
313        start_ix: usize,
314        mut start_point: ContourPoint<C>,
315        pen: &mut impl OutlinePen,
316    ) -> Result<(), ToPathError> {
317        match self {
318            Self::Empty => {}
319            _ => {
320                // We always want to end with an explicit on-curve
321                start_point.flags = PointFlags::on_curve();
322                self.emit(start_ix, start_point, pen)?;
323            }
324        }
325        pen.close();
326        Ok(())
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::{super::pen::SvgPen, *};
333    use raw::types::F26Dot6;
334
335    fn assert_off_curve_path_to_svg(expected: &str, path_style: PathStyle, all_off_curve: bool) {
336        fn pt(x: i32, y: i32) -> Point<F26Dot6> {
337            Point::new(x, y).map(F26Dot6::from_bits)
338        }
339        let mut flags = [PointFlags::off_curve_quad(); 4];
340        if !all_off_curve {
341            flags[1] = PointFlags::on_curve();
342        }
343        let contours = [3];
344        // This test is meant to prevent a bug where the first move-to was computed improperly
345        // for a contour consisting of all off curve points.
346        // In this case, the start of the path should be the midpoint between the first and last points.
347        // For this test case (in 26.6 fixed point): [(640, 128) + (128, 128)] / 2 = (384, 128)
348        // which becomes (6.0, 2.0) when converted to floating point.
349        let points = [pt(640, 128), pt(256, 64), pt(640, 64), pt(128, 128)];
350        let mut pen = SvgPen::with_precision(1);
351        to_path(&points, &flags, &contours, path_style, &mut pen).unwrap();
352        assert_eq!(pen.as_ref(), expected);
353    }
354
355    #[test]
356    fn all_off_curve_to_path_scan_backward() {
357        assert_off_curve_path_to_svg(
358            "M6.0,2.0 Q10.0,2.0 7.0,1.5 Q4.0,1.0 7.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Z",
359            PathStyle::FreeType,
360            true,
361        );
362    }
363
364    #[test]
365    fn all_off_curve_to_path_scan_forward() {
366        assert_off_curve_path_to_svg(
367            "M7.0,1.5 Q4.0,1.0 7.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Q10.0,2.0 7.0,1.5 Z",
368            PathStyle::HarfBuzz,
369            true,
370        );
371    }
372
373    #[test]
374    fn start_off_curve_to_path_scan_backward() {
375        assert_off_curve_path_to_svg(
376            "M6.0,2.0 Q10.0,2.0 4.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Z",
377            PathStyle::FreeType,
378            false,
379        );
380    }
381
382    #[test]
383    fn start_off_curve_to_path_scan_forward() {
384        assert_off_curve_path_to_svg(
385            "M4.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Q10.0,2.0 4.0,1.0 Z",
386            PathStyle::HarfBuzz,
387            false,
388        );
389    }
390}