Skip to main content

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