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 = 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#[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#[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 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 let mut omit_last = false;
185 let start_point = if first_point.flags.is_off_curve_quad() {
187 if last_point.flags.is_on_curve() {
188 omit_last = true;
190 last_point
191 } else {
192 last_point.midpoint(first_point)
194 }
195 } else {
196 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 let mut trailing_points = [None; 2];
229 let start_point = if first_point.flags.is_off_curve_quad() {
231 points.next();
233 let Some((_, next_point)) = points.peek().copied() else {
235 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_point
243 } else {
244 trailing_points = [Some((0, first_point)), None];
246 first_point.midpoint(next_point)
247 }
248 } else {
249 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 #[default]
270 Empty,
271 PendingQuad(ContourPoint<C>),
273 PendingCubic(ContourPoint<C>),
275 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 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 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}