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
49impl core::error::Error for ToPathError {}
50
51pub(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#[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#[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 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 let mut omit_last = false;
187 let start_point = if first_point.flags.is_off_curve_quad() {
189 if last_point.flags.is_on_curve() {
190 omit_last = true;
192 last_point
193 } else {
194 last_point.midpoint(first_point)
196 }
197 } else {
198 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 let mut trailing_points = [None; 2];
231 let start_point = if first_point.flags.is_off_curve_quad() {
233 points.next();
235 let Some((_, next_point)) = points.peek().copied() else {
237 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_point
245 } else {
246 trailing_points = [Some((0, first_point)), None];
248 first_point.midpoint(next_point)
249 }
250 } else {
251 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 #[default]
272 Empty,
273 PendingQuad(ContourPoint<C>),
275 PendingCubic(ContourPoint<C>),
277 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 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 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}