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 = if contour_ix > 0 {
69            contours[contour_ix - 1] as usize + 1
70        } else {
71            Default::default()
72        };
73        let end_ix = contours[contour_ix] as usize;
74        if end_ix < start_ix || end_ix >= points.len() {
75            return Err(ToPathError::ContourOrder(contour_ix));
76        }
77        let points = &points[start_ix..=end_ix];
78        if points.is_empty() {
79            continue;
80        }
81        let flags = flags
82            .get(start_ix..=end_ix)
83            .ok_or(ToPathError::PointFlagMismatch {
84                num_points: points.len(),
85                num_flags: flags.len(),
86            })?;
87        let [first_point, last_point] = [
88            (points.first(), flags.first()),
89            (points.last(), flags.last()),
90        ]
91        .map(|(point, flags)| {
92            let point = point.unwrap();
93            ContourPoint {
94                x: point.x,
95                y: point.y,
96                flags: *flags.unwrap(),
97            }
98        });
99        contour_to_path(
100            points.iter().zip(flags).map(|(point, flags)| ContourPoint {
101                x: point.x,
102                y: point.y,
103                flags: *flags,
104            }),
105            first_point,
106            last_point,
107            path_style,
108            pen,
109        )
110        .map_err(|e| match &e {
111            ToPathError::ExpectedCubic(ix) => ToPathError::ExpectedCubic(ix + start_ix),
112            ToPathError::ExpectedQuad(ix) => ToPathError::ExpectedQuad(ix + start_ix),
113            ToPathError::ExpectedQuadOrOnCurve(ix) => {
114                ToPathError::ExpectedQuadOrOnCurve(ix + start_ix)
115            }
116            _ => e,
117        })?
118    }
119    Ok(())
120}
121
122/// Combination of point coordinates and flags.
123#[derive(Copy, Clone, Default, Debug)]
124pub(crate) struct ContourPoint<T> {
125    pub x: T,
126    pub y: T,
127    pub flags: PointFlags,
128}
129
130impl<T> ContourPoint<T>
131where
132    T: PointCoord,
133{
134    fn point_f32(&self) -> Point<f32> {
135        Point::new(self.x.to_f32(), self.y.to_f32())
136    }
137
138    fn midpoint(&self, other: Self) -> ContourPoint<T> {
139        let (x, y) = (self.x.midpoint(other.x), self.y.midpoint(other.y));
140        Self {
141            x,
142            y,
143            flags: other.flags,
144        }
145    }
146}
147
148/// Generates a path from an iterator of contour points.
149///
150/// Note that this requires the last point of the contour to be passed
151/// separately to support FreeType style path conversion when the contour
152/// begins with an off curve point. The points iterator should still
153/// yield the last point as well.
154///
155/// This is more general than [`to_path`] and exists to support cases (such as
156/// autohinting) where the source outline data is in a different format.
157#[inline(always)]
158pub(crate) fn contour_to_path<C: PointCoord>(
159    points: impl ExactSizeIterator<Item = ContourPoint<C>>,
160    first_point: ContourPoint<C>,
161    last_point: ContourPoint<C>,
162    style: PathStyle,
163    pen: &mut impl OutlinePen,
164) -> Result<(), ToPathError> {
165    // We don't accept an off curve cubic as the first point
166    if first_point.flags.is_off_curve_cubic() {
167        return Err(ToPathError::ExpectedQuadOrOnCurve(0));
168    }
169    match style {
170        PathStyle::FreeType => contour_to_path_freetype(points, first_point, last_point, pen),
171        PathStyle::HarfBuzz => contour_to_path_harfbuzz(points, first_point, pen),
172    }
173}
174
175fn contour_to_path_freetype<C: PointCoord>(
176    points: impl ExactSizeIterator<Item = ContourPoint<C>>,
177    first_point: ContourPoint<C>,
178    last_point: ContourPoint<C>,
179    pen: &mut impl OutlinePen,
180) -> Result<(), ToPathError> {
181    let mut points = points.enumerate();
182    // For FreeType style, we may need to omit the last point if we find the
183    // first on curve there
184    let mut omit_last = false;
185    // Find our starting point
186    let start_point = if first_point.flags.is_off_curve_quad() {
187        if last_point.flags.is_on_curve() {
188            // The last point is an on curve, so let's start there
189            omit_last = true;
190            last_point
191        } else {
192            // It's also an off curve, so take implicit midpoint
193            last_point.midpoint(first_point)
194        }
195    } else {
196        // We're starting with an on curve, so consume the point
197        points.next();
198        first_point
199    };
200    let point = start_point.point_f32();
201    pen.move_to(point.x, point.y);
202    let mut state = PendingState::default();
203    if omit_last {
204        let end_ix = points.len() - 1;
205        for (ix, point) in points {
206            if ix == end_ix {
207                break;
208            }
209            state.emit(ix, point, pen)?;
210        }
211    } else {
212        for (ix, point) in points {
213            state.emit(ix, point, pen)?;
214        }
215    }
216    state.finish(0, start_point, pen)?;
217    Ok(())
218}
219
220fn contour_to_path_harfbuzz<C: PointCoord>(
221    points: impl ExactSizeIterator<Item = ContourPoint<C>>,
222    first_point: ContourPoint<C>,
223    pen: &mut impl OutlinePen,
224) -> Result<(), ToPathError> {
225    let mut points = points.enumerate().peekable();
226    // For HarfBuzz style, may skip up to two points in finding the start, so
227    // process these at the end
228    let mut trailing_points = [None; 2];
229    // Find our starting point
230    let start_point = if first_point.flags.is_off_curve_quad() {
231        // Always consume the first point
232        points.next();
233        // Then check the next point
234        let Some((_, next_point)) = points.peek().copied() else {
235            // This is a single point contour
236            return Ok(());
237        };
238        if next_point.flags.is_on_curve() {
239            points.next();
240            trailing_points = [Some((0, first_point)), Some((1, next_point))];
241            // Next is on curve, so let's start there
242            next_point
243        } else {
244            // It's also an off curve, so take the implicit midpoint
245            trailing_points = [Some((0, first_point)), None];
246            first_point.midpoint(next_point)
247        }
248    } else {
249        // We're starting with an on curve, so consume the point
250        points.next();
251        first_point
252    };
253    let point = start_point.point_f32();
254    pen.move_to(point.x, point.y);
255    let mut state = PendingState::default();
256    for (ix, point) in points {
257        state.emit(ix, point, pen)?;
258    }
259    for (ix, point) in trailing_points.iter().filter_map(|x| *x) {
260        state.emit(ix, point, pen)?;
261    }
262    state.finish(0, start_point, pen)?;
263    Ok(())
264}
265
266#[derive(Copy, Clone, Default)]
267enum PendingState<C> {
268    /// No pending points.
269    #[default]
270    Empty,
271    /// Pending off-curve quad point.
272    PendingQuad(ContourPoint<C>),
273    /// Single pending off-curve cubic point.
274    PendingCubic(ContourPoint<C>),
275    /// Two pending off-curve cubic points.
276    TwoPendingCubics(ContourPoint<C>, ContourPoint<C>),
277}
278
279impl<C> PendingState<C>
280where
281    C: PointCoord,
282{
283    #[inline(always)]
284    fn emit(
285        &mut self,
286        ix: usize,
287        point: ContourPoint<C>,
288        pen: &mut impl OutlinePen,
289    ) -> Result<(), ToPathError> {
290        let flags = point.flags;
291        match *self {
292            Self::Empty => {
293                if flags.is_off_curve_quad() {
294                    *self = Self::PendingQuad(point);
295                } else if flags.is_off_curve_cubic() {
296                    *self = Self::PendingCubic(point);
297                } else {
298                    let p = point.point_f32();
299                    pen.line_to(p.x, p.y);
300                }
301            }
302            Self::PendingQuad(quad) => {
303                if flags.is_off_curve_quad() {
304                    let c0 = quad.point_f32();
305                    let p = quad.midpoint(point).point_f32();
306                    pen.quad_to(c0.x, c0.y, p.x, p.y);
307                    *self = Self::PendingQuad(point);
308                } else if flags.is_off_curve_cubic() {
309                    return Err(ToPathError::ExpectedQuadOrOnCurve(ix));
310                } else {
311                    let c0 = quad.point_f32();
312                    let p = point.point_f32();
313                    pen.quad_to(c0.x, c0.y, p.x, p.y);
314                    *self = Self::Empty;
315                }
316            }
317            Self::PendingCubic(cubic) => {
318                if flags.is_off_curve_cubic() {
319                    *self = Self::TwoPendingCubics(cubic, point);
320                } else {
321                    return Err(ToPathError::ExpectedCubic(ix));
322                }
323            }
324            Self::TwoPendingCubics(cubic0, cubic1) => {
325                if flags.is_off_curve_quad() {
326                    return Err(ToPathError::ExpectedCubic(ix));
327                } else if flags.is_off_curve_cubic() {
328                    let c0 = cubic0.point_f32();
329                    let c1 = cubic1.point_f32();
330                    let p = cubic1.midpoint(point).point_f32();
331                    pen.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
332                    *self = Self::PendingCubic(point);
333                } else {
334                    let c0 = cubic0.point_f32();
335                    let c1 = cubic1.point_f32();
336                    let p = point.point_f32();
337                    pen.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
338                    *self = Self::Empty;
339                }
340            }
341        }
342        Ok(())
343    }
344
345    fn finish(
346        mut self,
347        start_ix: usize,
348        mut start_point: ContourPoint<C>,
349        pen: &mut impl OutlinePen,
350    ) -> Result<(), ToPathError> {
351        match self {
352            Self::Empty => {}
353            _ => {
354                // We always want to end with an explicit on-curve
355                start_point.flags = PointFlags::on_curve();
356                self.emit(start_ix, start_point, pen)?;
357            }
358        }
359        pen.close();
360        Ok(())
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::{super::pen::SvgPen, *};
367    use raw::types::F26Dot6;
368
369    fn assert_off_curve_path_to_svg(expected: &str, path_style: PathStyle, all_off_curve: bool) {
370        fn pt(x: i32, y: i32) -> Point<F26Dot6> {
371            Point::new(x, y).map(F26Dot6::from_bits)
372        }
373        let mut flags = [PointFlags::off_curve_quad(); 4];
374        if !all_off_curve {
375            flags[1] = PointFlags::on_curve();
376        }
377        let contours = [3];
378        // This test is meant to prevent a bug where the first move-to was computed improperly
379        // for a contour consisting of all off curve points.
380        // In this case, the start of the path should be the midpoint between the first and last points.
381        // For this test case (in 26.6 fixed point): [(640, 128) + (128, 128)] / 2 = (384, 128)
382        // which becomes (6.0, 2.0) when converted to floating point.
383        let points = [pt(640, 128), pt(256, 64), pt(640, 64), pt(128, 128)];
384        let mut pen = SvgPen::with_precision(1);
385        to_path(&points, &flags, &contours, path_style, &mut pen).unwrap();
386        assert_eq!(pen.as_ref(), expected);
387    }
388
389    #[test]
390    fn all_off_curve_to_path_scan_backward() {
391        assert_off_curve_path_to_svg(
392            "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",
393            PathStyle::FreeType,
394            true,
395        );
396    }
397
398    #[test]
399    fn all_off_curve_to_path_scan_forward() {
400        assert_off_curve_path_to_svg(
401            "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",
402            PathStyle::HarfBuzz,
403            true,
404        );
405    }
406
407    #[test]
408    fn start_off_curve_to_path_scan_backward() {
409        assert_off_curve_path_to_svg(
410            "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",
411            PathStyle::FreeType,
412            false,
413        );
414    }
415
416    #[test]
417    fn start_off_curve_to_path_scan_forward() {
418        assert_off_curve_path_to_svg(
419            "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",
420            PathStyle::HarfBuzz,
421            false,
422        );
423    }
424}