1use super::pen::{OutlinePen, PathStyle};
4use core::fmt;
5use raw::{
6 tables::glyf::{PointCoord, PointFlags},
7 types::Point,
8};
9
10#[derive(Clone, Debug)]
12pub enum ToPathError {
13 ContourOrder(usize),
15 ExpectedQuad(usize),
17 ExpectedQuadOrOnCurve(usize),
19 ExpectedCubic(usize),
21 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
49pub(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#[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
140pub(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 return Ok(());
159 };
160 if first_point.flags.is_off_curve_cubic() {
162 return Err(ToPathError::ExpectedQuadOrOnCurve(0));
163 }
164 let mut omit_last = false;
167 let mut trailing_points = [None; 2];
170 let start_point = if first_point.flags.is_off_curve_quad() {
172 match style {
175 PathStyle::FreeType => {
176 if last_point.flags.is_on_curve() {
177 omit_last = true;
179 last_point
180 } else {
181 last_point.midpoint(first_point)
183 }
184 }
185 PathStyle::HarfBuzz => {
186 points.next();
188 let Some((_, next_point)) = points.peek().copied() else {
190 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_point
198 } else {
199 trailing_points = [Some((0, first_point)), None];
201 first_point.midpoint(next_point)
202 }
203 }
204 }
205 } else {
206 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 #[default]
236 Empty,
237 PendingQuad(ContourPoint<C>),
239 PendingCubic(ContourPoint<C>),
241 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 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 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}