1 | use crate::{Error, Stream}; |
2 | |
3 | /// Representation of a path segment. |
4 | /// |
5 | /// If you want to change the segment type (for example MoveTo to LineTo) |
6 | /// you should create a new segment. |
7 | /// But you still can change points or make segment relative or absolute. |
8 | #[allow (missing_docs)] |
9 | #[derive (Clone, Copy, PartialEq, Debug)] |
10 | pub enum PathSegment { |
11 | MoveTo { |
12 | abs: bool, |
13 | x: f64, |
14 | y: f64, |
15 | }, |
16 | LineTo { |
17 | abs: bool, |
18 | x: f64, |
19 | y: f64, |
20 | }, |
21 | HorizontalLineTo { |
22 | abs: bool, |
23 | x: f64, |
24 | }, |
25 | VerticalLineTo { |
26 | abs: bool, |
27 | y: f64, |
28 | }, |
29 | CurveTo { |
30 | abs: bool, |
31 | x1: f64, |
32 | y1: f64, |
33 | x2: f64, |
34 | y2: f64, |
35 | x: f64, |
36 | y: f64, |
37 | }, |
38 | SmoothCurveTo { |
39 | abs: bool, |
40 | x2: f64, |
41 | y2: f64, |
42 | x: f64, |
43 | y: f64, |
44 | }, |
45 | Quadratic { |
46 | abs: bool, |
47 | x1: f64, |
48 | y1: f64, |
49 | x: f64, |
50 | y: f64, |
51 | }, |
52 | SmoothQuadratic { |
53 | abs: bool, |
54 | x: f64, |
55 | y: f64, |
56 | }, |
57 | EllipticalArc { |
58 | abs: bool, |
59 | rx: f64, |
60 | ry: f64, |
61 | x_axis_rotation: f64, |
62 | large_arc: bool, |
63 | sweep: bool, |
64 | x: f64, |
65 | y: f64, |
66 | }, |
67 | ClosePath { |
68 | abs: bool, |
69 | }, |
70 | } |
71 | |
72 | /// A pull-based [path data] parser. |
73 | /// |
74 | /// # Errors |
75 | /// |
76 | /// - Most of the `Error` types can occur. |
77 | /// |
78 | /// # Notes |
79 | /// |
80 | /// The library does not support implicit commands, so they will be converted to an explicit one. |
81 | /// It mostly affects an implicit MoveTo, which will be converted, according to the spec, |
82 | /// into explicit LineTo. |
83 | /// |
84 | /// Example: `M 10 20 30 40 50 60` -> `M 10 20 L 30 40 L 50 60` |
85 | /// |
86 | /// # Examples |
87 | /// |
88 | /// ``` |
89 | /// use svgtypes::{PathParser, PathSegment}; |
90 | /// |
91 | /// let mut segments = Vec::new(); |
92 | /// for segment in PathParser::from("M10-20l30.1.5.1-20z" ) { |
93 | /// segments.push(segment.unwrap()); |
94 | /// } |
95 | /// |
96 | /// assert_eq!(segments, &[ |
97 | /// PathSegment::MoveTo { abs: true, x: 10.0, y: -20.0 }, |
98 | /// PathSegment::LineTo { abs: false, x: 30.1, y: 0.5 }, |
99 | /// PathSegment::LineTo { abs: false, x: 0.1, y: -20.0 }, |
100 | /// PathSegment::ClosePath { abs: false }, |
101 | /// ]); |
102 | /// ``` |
103 | /// |
104 | /// [path data]: https://www.w3.org/TR/SVG2/paths.html#PathData |
105 | #[derive (Clone, Copy, PartialEq, Eq, Debug)] |
106 | pub struct PathParser<'a> { |
107 | stream: Stream<'a>, |
108 | prev_cmd: Option<u8>, |
109 | } |
110 | |
111 | impl<'a> From<&'a str> for PathParser<'a> { |
112 | #[inline ] |
113 | fn from(v: &'a str) -> Self { |
114 | PathParser { |
115 | stream: Stream::from(v), |
116 | prev_cmd: None, |
117 | } |
118 | } |
119 | } |
120 | |
121 | impl<'a> Iterator for PathParser<'a> { |
122 | type Item = Result<PathSegment, Error>; |
123 | |
124 | #[inline ] |
125 | fn next(&mut self) -> Option<Self::Item> { |
126 | let s: &mut Stream<'_> = &mut self.stream; |
127 | |
128 | s.skip_spaces(); |
129 | |
130 | if s.at_end() { |
131 | return None; |
132 | } |
133 | |
134 | let res: Result = next_impl(s, &mut self.prev_cmd); |
135 | if res.is_err() { |
136 | s.jump_to_end(); |
137 | } |
138 | |
139 | Some(res) |
140 | } |
141 | } |
142 | |
143 | fn next_impl(s: &mut Stream, prev_cmd: &mut Option<u8>) -> Result<PathSegment, Error> { |
144 | let start = s.pos(); |
145 | |
146 | let has_prev_cmd = prev_cmd.is_some(); |
147 | let first_char = s.curr_byte_unchecked(); |
148 | |
149 | if !has_prev_cmd && !is_cmd(first_char) { |
150 | return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); |
151 | } |
152 | |
153 | if !has_prev_cmd && !matches!(first_char, b'M' | b'm' ) { |
154 | // The first segment must be a MoveTo. |
155 | return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); |
156 | } |
157 | |
158 | // TODO: simplify |
159 | let is_implicit_move_to; |
160 | let cmd: u8; |
161 | if is_cmd(first_char) { |
162 | is_implicit_move_to = false; |
163 | cmd = first_char; |
164 | s.advance(1); |
165 | } else if is_number_start(first_char) && has_prev_cmd { |
166 | // unwrap is safe, because we checked 'has_prev_cmd' |
167 | let p_cmd = prev_cmd.unwrap(); |
168 | |
169 | if p_cmd == b'Z' || p_cmd == b'z' { |
170 | // ClosePath cannot be followed by a number. |
171 | return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); |
172 | } |
173 | |
174 | if p_cmd == b'M' || p_cmd == b'm' { |
175 | // 'If a moveto is followed by multiple pairs of coordinates, |
176 | // the subsequent pairs are treated as implicit lineto commands.' |
177 | // So we parse them as LineTo. |
178 | is_implicit_move_to = true; |
179 | cmd = if is_absolute(p_cmd) { b'L' } else { b'l' }; |
180 | } else { |
181 | is_implicit_move_to = false; |
182 | cmd = p_cmd; |
183 | } |
184 | } else { |
185 | return Err(Error::UnexpectedData(s.calc_char_pos_at(start))); |
186 | } |
187 | |
188 | let cmdl = to_relative(cmd); |
189 | let absolute = is_absolute(cmd); |
190 | let token = match cmdl { |
191 | b'm' => PathSegment::MoveTo { |
192 | abs: absolute, |
193 | x: s.parse_list_number()?, |
194 | y: s.parse_list_number()?, |
195 | }, |
196 | b'l' => PathSegment::LineTo { |
197 | abs: absolute, |
198 | x: s.parse_list_number()?, |
199 | y: s.parse_list_number()?, |
200 | }, |
201 | b'h' => PathSegment::HorizontalLineTo { |
202 | abs: absolute, |
203 | x: s.parse_list_number()?, |
204 | }, |
205 | b'v' => PathSegment::VerticalLineTo { |
206 | abs: absolute, |
207 | y: s.parse_list_number()?, |
208 | }, |
209 | b'c' => PathSegment::CurveTo { |
210 | abs: absolute, |
211 | x1: s.parse_list_number()?, |
212 | y1: s.parse_list_number()?, |
213 | x2: s.parse_list_number()?, |
214 | y2: s.parse_list_number()?, |
215 | x: s.parse_list_number()?, |
216 | y: s.parse_list_number()?, |
217 | }, |
218 | b's' => PathSegment::SmoothCurveTo { |
219 | abs: absolute, |
220 | x2: s.parse_list_number()?, |
221 | y2: s.parse_list_number()?, |
222 | x: s.parse_list_number()?, |
223 | y: s.parse_list_number()?, |
224 | }, |
225 | b'q' => PathSegment::Quadratic { |
226 | abs: absolute, |
227 | x1: s.parse_list_number()?, |
228 | y1: s.parse_list_number()?, |
229 | x: s.parse_list_number()?, |
230 | y: s.parse_list_number()?, |
231 | }, |
232 | b't' => PathSegment::SmoothQuadratic { |
233 | abs: absolute, |
234 | x: s.parse_list_number()?, |
235 | y: s.parse_list_number()?, |
236 | }, |
237 | b'a' => { |
238 | // TODO: radius cannot be negative |
239 | PathSegment::EllipticalArc { |
240 | abs: absolute, |
241 | rx: s.parse_list_number()?, |
242 | ry: s.parse_list_number()?, |
243 | x_axis_rotation: s.parse_list_number()?, |
244 | large_arc: parse_flag(s)?, |
245 | sweep: parse_flag(s)?, |
246 | x: s.parse_list_number()?, |
247 | y: s.parse_list_number()?, |
248 | } |
249 | } |
250 | b'z' => PathSegment::ClosePath { abs: absolute }, |
251 | _ => unreachable!(), |
252 | }; |
253 | |
254 | *prev_cmd = Some(if is_implicit_move_to { |
255 | if absolute { |
256 | b'M' |
257 | } else { |
258 | b'm' |
259 | } |
260 | } else { |
261 | cmd |
262 | }); |
263 | |
264 | Ok(token) |
265 | } |
266 | |
267 | /// Returns `true` if the selected char is the command. |
268 | #[rustfmt::skip] |
269 | #[inline ] |
270 | fn is_cmd(c: u8) -> bool { |
271 | matches!(c, |
272 | b'M' | b'm' |
273 | | b'Z' | b'z' |
274 | | b'L' | b'l' |
275 | | b'H' | b'h' |
276 | | b'V' | b'v' |
277 | | b'C' | b'c' |
278 | | b'S' | b's' |
279 | | b'Q' | b'q' |
280 | | b'T' | b't' |
281 | | b'A' | b'a' ) |
282 | } |
283 | |
284 | /// Returns `true` if the selected char is the absolute command. |
285 | #[inline ] |
286 | fn is_absolute(c: u8) -> bool { |
287 | debug_assert!(is_cmd(c)); |
288 | matches!( |
289 | c, |
290 | b'M' | b'Z' | b'L' | b'H' | b'V' | b'C' | b'S' | b'Q' | b'T' | b'A' |
291 | ) |
292 | } |
293 | |
294 | /// Converts the selected command char into the relative command char. |
295 | #[inline ] |
296 | fn to_relative(c: u8) -> u8 { |
297 | debug_assert!(is_cmd(c)); |
298 | match c { |
299 | b'M' => b'm' , |
300 | b'Z' => b'z' , |
301 | b'L' => b'l' , |
302 | b'H' => b'h' , |
303 | b'V' => b'v' , |
304 | b'C' => b'c' , |
305 | b'S' => b's' , |
306 | b'Q' => b'q' , |
307 | b'T' => b't' , |
308 | b'A' => b'a' , |
309 | _ => c, |
310 | } |
311 | } |
312 | |
313 | #[inline ] |
314 | fn is_number_start(c: u8) -> bool { |
315 | matches!(c, b'0' ..=b'9' | b'.' | b'-' | b'+' ) |
316 | } |
317 | |
318 | // By the SVG spec 'large-arc' and 'sweep' must contain only one char |
319 | // and can be written without any separators, e.g.: 10 20 30 01 10 20. |
320 | fn parse_flag(s: &mut Stream) -> Result<bool, Error> { |
321 | s.skip_spaces(); |
322 | |
323 | let c: u8 = s.curr_byte()?; |
324 | match c { |
325 | b'0' | b'1' => { |
326 | s.advance(1); |
327 | if s.is_curr_byte_eq(b',' ) { |
328 | s.advance(1); |
329 | } |
330 | s.skip_spaces(); |
331 | |
332 | Ok(c == b'1' ) |
333 | } |
334 | _ => Err(Error::UnexpectedData(s.calc_char_pos_at(byte_pos:s.pos()))), |
335 | } |
336 | } |
337 | |
338 | #[rustfmt::skip] |
339 | #[cfg (test)] |
340 | mod tests { |
341 | use super::*; |
342 | |
343 | macro_rules! test { |
344 | ($name:ident, $text:expr, $( $seg:expr ),*) => ( |
345 | #[test] |
346 | fn $name() { |
347 | let mut s = PathParser::from($text); |
348 | $( |
349 | assert_eq!(s.next().unwrap().unwrap(), $seg); |
350 | )* |
351 | |
352 | if let Some(res) = s.next() { |
353 | assert!(res.is_err()); |
354 | } |
355 | } |
356 | ) |
357 | } |
358 | |
359 | test !(null, "" , ); |
360 | test !(not_a_path, "q" , ); |
361 | test !(not_a_move_to, "L 20 30" , ); |
362 | test !(stop_on_err_1, "M 10 20 L 30 40 L 50" , |
363 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
364 | PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 } |
365 | ); |
366 | |
367 | test !(move_to_1, "M 10 20" , PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }); |
368 | test !(move_to_2, "m 10 20" , PathSegment::MoveTo { abs: false, x: 10.0, y: 20.0 }); |
369 | test !(move_to_3, "M 10 20 30 40 50 60" , |
370 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
371 | PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, |
372 | PathSegment::LineTo { abs: true, x: 50.0, y: 60.0 } |
373 | ); |
374 | test !(move_to_4, "M 10 20 30 40 50 60 M 70 80 90 100 110 120" , |
375 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
376 | PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, |
377 | PathSegment::LineTo { abs: true, x: 50.0, y: 60.0 }, |
378 | PathSegment::MoveTo { abs: true, x: 70.0, y: 80.0 }, |
379 | PathSegment::LineTo { abs: true, x: 90.0, y: 100.0 }, |
380 | PathSegment::LineTo { abs: true, x: 110.0, y: 120.0 } |
381 | ); |
382 | |
383 | test !(arc_to_1, "M 10 20 A 5 5 30 1 1 20 20" , |
384 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
385 | PathSegment::EllipticalArc { |
386 | abs: true, |
387 | rx: 5.0, ry: 5.0, |
388 | x_axis_rotation: 30.0, |
389 | large_arc: true, sweep: true, |
390 | x: 20.0, y: 20.0 |
391 | } |
392 | ); |
393 | |
394 | test !(arc_to_2, "M 10 20 a 5 5 30 0 0 20 20" , |
395 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
396 | PathSegment::EllipticalArc { |
397 | abs: false, |
398 | rx: 5.0, ry: 5.0, |
399 | x_axis_rotation: 30.0, |
400 | large_arc: false, sweep: false, |
401 | x: 20.0, y: 20.0 |
402 | } |
403 | ); |
404 | |
405 | test !(arc_to_10, "M10-20A5.5.3-4 010-.1" , |
406 | PathSegment::MoveTo { abs: true, x: 10.0, y: -20.0 }, |
407 | PathSegment::EllipticalArc { |
408 | abs: true, |
409 | rx: 5.5, ry: 0.3, |
410 | x_axis_rotation: -4.0, |
411 | large_arc: false, sweep: true, |
412 | x: 0.0, y: -0.1 |
413 | } |
414 | ); |
415 | |
416 | test !(separator_1, "M 10 20 L 5 15 C 10 20 30 40 50 60" , |
417 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
418 | PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, |
419 | PathSegment::CurveTo { |
420 | abs: true, |
421 | x1: 10.0, y1: 20.0, |
422 | x2: 30.0, y2: 40.0, |
423 | x: 50.0, y: 60.0, |
424 | } |
425 | ); |
426 | |
427 | test !(separator_2, "M 10, 20 L 5, 15 C 10, 20 30, 40 50, 60" , |
428 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
429 | PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, |
430 | PathSegment::CurveTo { |
431 | abs: true, |
432 | x1: 10.0, y1: 20.0, |
433 | x2: 30.0, y2: 40.0, |
434 | x: 50.0, y: 60.0, |
435 | } |
436 | ); |
437 | |
438 | test !(separator_3, "M 10,20 L 5,15 C 10,20 30,40 50,60" , |
439 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
440 | PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, |
441 | PathSegment::CurveTo { |
442 | abs: true, |
443 | x1: 10.0, y1: 20.0, |
444 | x2: 30.0, y2: 40.0, |
445 | x: 50.0, y: 60.0, |
446 | } |
447 | ); |
448 | |
449 | test !(separator_4, "M10, 20 L5, 15 C10, 20 30 40 50 60" , |
450 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
451 | PathSegment::LineTo { abs: true, x: 5.0, y: 15.0 }, |
452 | PathSegment::CurveTo { |
453 | abs: true, |
454 | x1: 10.0, y1: 20.0, |
455 | x2: 30.0, y2: 40.0, |
456 | x: 50.0, y: 60.0, |
457 | } |
458 | ); |
459 | |
460 | test !(separator_5, "M10 20V30H40V50H60Z" , |
461 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
462 | PathSegment::VerticalLineTo { abs: true, y: 30.0 }, |
463 | PathSegment::HorizontalLineTo { abs: true, x: 40.0 }, |
464 | PathSegment::VerticalLineTo { abs: true, y: 50.0 }, |
465 | PathSegment::HorizontalLineTo { abs: true, x: 60.0 }, |
466 | PathSegment::ClosePath { abs: true } |
467 | ); |
468 | |
469 | test !(all_segments_1, "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 S 130 140 150 160 |
470 | Q 170 180 190 200 T 210 220 A 50 50 30 1 1 230 240 Z" , |
471 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
472 | PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, |
473 | PathSegment::HorizontalLineTo { abs: true, x: 50.0 }, |
474 | PathSegment::VerticalLineTo { abs: true, y: 60.0 }, |
475 | PathSegment::CurveTo { |
476 | abs: true, |
477 | x1: 70.0, y1: 80.0, |
478 | x2: 90.0, y2: 100.0, |
479 | x: 110.0, y: 120.0, |
480 | }, |
481 | PathSegment::SmoothCurveTo { |
482 | abs: true, |
483 | x2: 130.0, y2: 140.0, |
484 | x: 150.0, y: 160.0, |
485 | }, |
486 | PathSegment::Quadratic { |
487 | abs: true, |
488 | x1: 170.0, y1: 180.0, |
489 | x: 190.0, y: 200.0, |
490 | }, |
491 | PathSegment::SmoothQuadratic { abs: true, x: 210.0, y: 220.0 }, |
492 | PathSegment::EllipticalArc { |
493 | abs: true, |
494 | rx: 50.0, ry: 50.0, |
495 | x_axis_rotation: 30.0, |
496 | large_arc: true, sweep: true, |
497 | x: 230.0, y: 240.0 |
498 | }, |
499 | PathSegment::ClosePath { abs: true } |
500 | ); |
501 | |
502 | test !(all_segments_2, "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 s 130 140 150 160 |
503 | q 170 180 190 200 t 210 220 a 50 50 30 1 1 230 240 z" , |
504 | PathSegment::MoveTo { abs: false, x: 10.0, y: 20.0 }, |
505 | PathSegment::LineTo { abs: false, x: 30.0, y: 40.0 }, |
506 | PathSegment::HorizontalLineTo { abs: false, x: 50.0 }, |
507 | PathSegment::VerticalLineTo { abs: false, y: 60.0 }, |
508 | PathSegment::CurveTo { |
509 | abs: false, |
510 | x1: 70.0, y1: 80.0, |
511 | x2: 90.0, y2: 100.0, |
512 | x: 110.0, y: 120.0, |
513 | }, |
514 | PathSegment::SmoothCurveTo { |
515 | abs: false, |
516 | x2: 130.0, y2: 140.0, |
517 | x: 150.0, y: 160.0, |
518 | }, |
519 | PathSegment::Quadratic { |
520 | abs: false, |
521 | x1: 170.0, y1: 180.0, |
522 | x: 190.0, y: 200.0, |
523 | }, |
524 | PathSegment::SmoothQuadratic { abs: false, x: 210.0, y: 220.0 }, |
525 | PathSegment::EllipticalArc { |
526 | abs: false, |
527 | rx: 50.0, ry: 50.0, |
528 | x_axis_rotation: 30.0, |
529 | large_arc: true, sweep: true, |
530 | x: 230.0, y: 240.0 |
531 | }, |
532 | PathSegment::ClosePath { abs: false } |
533 | ); |
534 | |
535 | test !(close_path_1, "M10 20 L 30 40 ZM 100 200 L 300 400" , |
536 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
537 | PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, |
538 | PathSegment::ClosePath { abs: true }, |
539 | PathSegment::MoveTo { abs: true, x: 100.0, y: 200.0 }, |
540 | PathSegment::LineTo { abs: true, x: 300.0, y: 400.0 } |
541 | ); |
542 | |
543 | test !(close_path_2, "M10 20 L 30 40 zM 100 200 L 300 400" , |
544 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
545 | PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, |
546 | PathSegment::ClosePath { abs: false }, |
547 | PathSegment::MoveTo { abs: true, x: 100.0, y: 200.0 }, |
548 | PathSegment::LineTo { abs: true, x: 300.0, y: 400.0 } |
549 | ); |
550 | |
551 | test !(close_path_3, "M10 20 L 30 40 Z Z Z" , |
552 | PathSegment::MoveTo { abs: true, x: 10.0, y: 20.0 }, |
553 | PathSegment::LineTo { abs: true, x: 30.0, y: 40.0 }, |
554 | PathSegment::ClosePath { abs: true }, |
555 | PathSegment::ClosePath { abs: true }, |
556 | PathSegment::ClosePath { abs: true } |
557 | ); |
558 | |
559 | // first token should be EndOfStream |
560 | test !(invalid_1, "M \t." , ); |
561 | |
562 | // ClosePath can't be followed by a number |
563 | test !(invalid_2, "M 0 0 Z 2" , |
564 | PathSegment::MoveTo { abs: true, x: 0.0, y: 0.0 }, |
565 | PathSegment::ClosePath { abs: true } |
566 | ); |
567 | |
568 | // ClosePath can be followed by any command |
569 | test !(invalid_3, "M 0 0 Z H 10" , |
570 | PathSegment::MoveTo { abs: true, x: 0.0, y: 0.0 }, |
571 | PathSegment::ClosePath { abs: true }, |
572 | PathSegment::HorizontalLineTo { abs: true, x: 10.0 } |
573 | ); |
574 | } |
575 | |
576 | /// Representation of a simple path segment. |
577 | #[allow (missing_docs)] |
578 | #[derive (Clone, Copy, PartialEq, Debug)] |
579 | pub enum SimplePathSegment { |
580 | MoveTo { |
581 | x: f64, |
582 | y: f64, |
583 | }, |
584 | LineTo { |
585 | x: f64, |
586 | y: f64, |
587 | }, |
588 | CurveTo { |
589 | x1: f64, |
590 | y1: f64, |
591 | x2: f64, |
592 | y2: f64, |
593 | x: f64, |
594 | y: f64, |
595 | }, |
596 | Quadratic { |
597 | x1: f64, |
598 | y1: f64, |
599 | x: f64, |
600 | y: f64, |
601 | }, |
602 | ClosePath, |
603 | } |
604 | |
605 | /// A simplifying Path Data parser. |
606 | /// |
607 | /// A more high-level Path Data parser on top of [`PathParser`] that provides: |
608 | /// |
609 | /// - Relative to absolute segment coordinates conversion |
610 | /// - ArcTo to CurveTos conversion |
611 | /// - SmoothCurveTo and SmoothQuadratic conversion |
612 | /// - HorizontalLineTo and VerticalLineTo to LineTo conversion |
613 | /// - Implicit MoveTo after ClosePath handling |
614 | /// |
615 | /// In the end, only absolute MoveTo, LineTo, CurveTo, Quadratic and ClosePath segments |
616 | /// will be produced. |
617 | #[derive (Clone, Debug)] |
618 | pub struct SimplifyingPathParser<'a> { |
619 | parser: PathParser<'a>, |
620 | |
621 | // Previous MoveTo coordinates. |
622 | prev_mx: f64, |
623 | prev_my: f64, |
624 | |
625 | // Previous coordinates. |
626 | prev_x: f64, |
627 | prev_y: f64, |
628 | |
629 | prev_segment: SimplePathSegment, |
630 | |
631 | buffer: Vec<SimplePathSegment>, |
632 | } |
633 | |
634 | impl<'a> From<&'a str> for SimplifyingPathParser<'a> { |
635 | #[inline ] |
636 | fn from(v: &'a str) -> Self { |
637 | SimplifyingPathParser { |
638 | parser: PathParser::from(v), |
639 | prev_mx: 0.0, |
640 | prev_my: 0.0, |
641 | prev_x: 0.0, |
642 | prev_y: 0.0, |
643 | prev_segment: SimplePathSegment::MoveTo { x: 0.0, y: 0.0 }, |
644 | buffer: Vec::new(), |
645 | } |
646 | } |
647 | } |
648 | |
649 | impl<'a> Iterator for SimplifyingPathParser<'a> { |
650 | type Item = Result<SimplePathSegment, Error>; |
651 | |
652 | fn next(&mut self) -> Option<Self::Item> { |
653 | if !self.buffer.is_empty() { |
654 | return Some(Ok(self.buffer.remove(0))); |
655 | } |
656 | |
657 | let segment = match self.parser.next()? { |
658 | Ok(v) => v, |
659 | Err(e) => return Some(Err(e)), |
660 | }; |
661 | |
662 | // If a ClosePath segment is followed by any command other than MoveTo or ClosePath |
663 | // then MoveTo is implicit. |
664 | if let SimplePathSegment::ClosePath = self.prev_segment { |
665 | match segment { |
666 | PathSegment::MoveTo { .. } | PathSegment::ClosePath { .. } => {} |
667 | _ => { |
668 | let new_seg = SimplePathSegment::MoveTo { |
669 | x: self.prev_mx, |
670 | y: self.prev_my, |
671 | }; |
672 | self.buffer.push(new_seg); |
673 | self.prev_segment = new_seg; |
674 | } |
675 | } |
676 | } |
677 | |
678 | match segment { |
679 | PathSegment::MoveTo { abs, mut x, mut y } => { |
680 | if !abs { |
681 | // When we get 'm'(relative) segment, which is not first segment - then it's |
682 | // relative to a previous 'M'(absolute) segment, not to the first segment. |
683 | if let SimplePathSegment::ClosePath = self.prev_segment { |
684 | x += self.prev_mx; |
685 | y += self.prev_my; |
686 | } else { |
687 | x += self.prev_x; |
688 | y += self.prev_y; |
689 | } |
690 | } |
691 | |
692 | self.buffer.push(SimplePathSegment::MoveTo { x, y }); |
693 | } |
694 | PathSegment::LineTo { abs, mut x, mut y } => { |
695 | if !abs { |
696 | x += self.prev_x; |
697 | y += self.prev_y; |
698 | } |
699 | |
700 | self.buffer.push(SimplePathSegment::LineTo { x, y }); |
701 | } |
702 | PathSegment::HorizontalLineTo { abs, mut x } => { |
703 | if !abs { |
704 | x += self.prev_x; |
705 | } |
706 | |
707 | self.buffer |
708 | .push(SimplePathSegment::LineTo { x, y: self.prev_y }); |
709 | } |
710 | PathSegment::VerticalLineTo { abs, mut y } => { |
711 | if !abs { |
712 | y += self.prev_y; |
713 | } |
714 | |
715 | self.buffer |
716 | .push(SimplePathSegment::LineTo { x: self.prev_x, y }); |
717 | } |
718 | PathSegment::CurveTo { |
719 | abs, |
720 | mut x1, |
721 | mut y1, |
722 | mut x2, |
723 | mut y2, |
724 | mut x, |
725 | mut y, |
726 | } => { |
727 | if !abs { |
728 | x1 += self.prev_x; |
729 | y1 += self.prev_y; |
730 | x2 += self.prev_x; |
731 | y2 += self.prev_y; |
732 | x += self.prev_x; |
733 | y += self.prev_y; |
734 | } |
735 | |
736 | self.buffer.push(SimplePathSegment::CurveTo { |
737 | x1, |
738 | y1, |
739 | x2, |
740 | y2, |
741 | x, |
742 | y, |
743 | }); |
744 | } |
745 | PathSegment::SmoothCurveTo { |
746 | abs, |
747 | mut x2, |
748 | mut y2, |
749 | mut x, |
750 | mut y, |
751 | } => { |
752 | // 'The first control point is assumed to be the reflection of the second control |
753 | // point on the previous command relative to the current point. |
754 | // (If there is no previous command or if the previous command |
755 | // was not an C, c, S or s, assume the first control point is |
756 | // coincident with the current point.)' |
757 | let (x1, y1) = match self.prev_segment { |
758 | SimplePathSegment::CurveTo { x2, y2, x, y, .. } => (x * 2.0 - x2, y * 2.0 - y2), |
759 | _ => (self.prev_x, self.prev_y), |
760 | }; |
761 | |
762 | if !abs { |
763 | x2 += self.prev_x; |
764 | y2 += self.prev_y; |
765 | x += self.prev_x; |
766 | y += self.prev_y; |
767 | } |
768 | |
769 | self.buffer.push(SimplePathSegment::CurveTo { |
770 | x1, |
771 | y1, |
772 | x2, |
773 | y2, |
774 | x, |
775 | y, |
776 | }); |
777 | } |
778 | PathSegment::Quadratic { |
779 | abs, |
780 | mut x1, |
781 | mut y1, |
782 | mut x, |
783 | mut y, |
784 | } => { |
785 | if !abs { |
786 | x1 += self.prev_x; |
787 | y1 += self.prev_y; |
788 | x += self.prev_x; |
789 | y += self.prev_y; |
790 | } |
791 | |
792 | self.buffer |
793 | .push(SimplePathSegment::Quadratic { x1, y1, x, y }); |
794 | } |
795 | PathSegment::SmoothQuadratic { abs, mut x, mut y } => { |
796 | // 'The control point is assumed to be the reflection of |
797 | // the control point on the previous command relative to |
798 | // the current point. (If there is no previous command or |
799 | // if the previous command was not a Q, q, T or t, assume |
800 | // the control point is coincident with the current point.)' |
801 | let (x1, y1) = match self.prev_segment { |
802 | SimplePathSegment::Quadratic { x1, y1, x, y, .. } => { |
803 | (x * 2.0 - x1, y * 2.0 - y1) |
804 | } |
805 | _ => (self.prev_x, self.prev_y), |
806 | }; |
807 | |
808 | if !abs { |
809 | x += self.prev_x; |
810 | y += self.prev_y; |
811 | } |
812 | |
813 | self.buffer |
814 | .push(SimplePathSegment::Quadratic { x1, y1, x, y }); |
815 | } |
816 | PathSegment::EllipticalArc { |
817 | abs, |
818 | rx, |
819 | ry, |
820 | x_axis_rotation, |
821 | large_arc, |
822 | sweep, |
823 | mut x, |
824 | mut y, |
825 | } => { |
826 | if !abs { |
827 | x += self.prev_x; |
828 | y += self.prev_y; |
829 | } |
830 | |
831 | let svg_arc = kurbo::SvgArc { |
832 | from: kurbo::Point::new(self.prev_x, self.prev_y), |
833 | to: kurbo::Point::new(x, y), |
834 | radii: kurbo::Vec2::new(rx, ry), |
835 | x_rotation: x_axis_rotation.to_radians(), |
836 | large_arc, |
837 | sweep, |
838 | }; |
839 | |
840 | match kurbo::Arc::from_svg_arc(&svg_arc) { |
841 | Some(arc) => { |
842 | arc.to_cubic_beziers(0.1, |p1, p2, p| { |
843 | self.buffer.push(SimplePathSegment::CurveTo { |
844 | x1: p1.x, |
845 | y1: p1.y, |
846 | x2: p2.x, |
847 | y2: p2.y, |
848 | x: p.x, |
849 | y: p.y, |
850 | }); |
851 | }); |
852 | } |
853 | None => { |
854 | self.buffer.push(SimplePathSegment::LineTo { x, y }); |
855 | } |
856 | } |
857 | } |
858 | PathSegment::ClosePath { .. } => { |
859 | if let SimplePathSegment::ClosePath = self.prev_segment { |
860 | // Do not add sequential ClosePath segments. |
861 | // Otherwise it will break markers rendering. |
862 | } else { |
863 | self.buffer.push(SimplePathSegment::ClosePath); |
864 | } |
865 | } |
866 | } |
867 | |
868 | // Remember last position. |
869 | if let Some(new_segment) = self.buffer.last() { |
870 | self.prev_segment = *new_segment; |
871 | |
872 | match *new_segment { |
873 | SimplePathSegment::MoveTo { x, y } => { |
874 | self.prev_x = x; |
875 | self.prev_y = y; |
876 | self.prev_mx = self.prev_x; |
877 | self.prev_my = self.prev_y; |
878 | } |
879 | SimplePathSegment::LineTo { x, y } => { |
880 | self.prev_x = x; |
881 | self.prev_y = y; |
882 | } |
883 | SimplePathSegment::CurveTo { x, y, .. } => { |
884 | self.prev_x = x; |
885 | self.prev_y = y; |
886 | } |
887 | SimplePathSegment::Quadratic { x, y, .. } => { |
888 | self.prev_x = x; |
889 | self.prev_y = y; |
890 | } |
891 | SimplePathSegment::ClosePath => { |
892 | // ClosePath moves us to the last MoveTo coordinate. |
893 | self.prev_x = self.prev_mx; |
894 | self.prev_y = self.prev_my; |
895 | } |
896 | } |
897 | } |
898 | |
899 | if self.buffer.is_empty() { |
900 | return self.next(); |
901 | } |
902 | |
903 | Some(Ok(self.buffer.remove(0))) |
904 | } |
905 | } |
906 | |
907 | #[rustfmt::skip] |
908 | #[cfg (test)] |
909 | mod simple_tests { |
910 | use super::*; |
911 | |
912 | macro_rules! test { |
913 | ($name:ident, $text:expr, $( $seg:expr ),*) => ( |
914 | #[test] |
915 | fn $name() { |
916 | let mut s = SimplifyingPathParser::from($text); |
917 | $( |
918 | assert_eq!(s.next().unwrap().unwrap(), $seg); |
919 | )* |
920 | |
921 | if let Some(res) = s.next() { |
922 | assert!(res.is_err()); |
923 | } |
924 | } |
925 | ) |
926 | } |
927 | |
928 | test !(ignore_duplicated_close_paths, "M 10 20 L 30 40 Z Z Z Z" , |
929 | SimplePathSegment::MoveTo { x: 10.0, y: 20.0 }, |
930 | SimplePathSegment::LineTo { x: 30.0, y: 40.0 }, |
931 | SimplePathSegment::ClosePath |
932 | ); |
933 | |
934 | test !(relative_move_to, "m 30 40 110 120 -20 -130" , |
935 | SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, |
936 | SimplePathSegment::LineTo { x: 140.0, y: 160.0 }, |
937 | SimplePathSegment::LineTo { x: 120.0, y: 30.0 } |
938 | ); |
939 | |
940 | test !(smooth_curve_to_after_move_to, "M 30 40 S 171 45 180 155" , |
941 | SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, |
942 | SimplePathSegment::CurveTo { x1: 30.0, y1: 40.0, x2: 171.0, y2: 45.0, x: 180.0, y: 155.0 } |
943 | ); |
944 | |
945 | test !(smooth_curve_to_after_curve_to, "M 30 40 C 16 137 171 45 100 90 S 171 45 180 155" , |
946 | SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, |
947 | SimplePathSegment::CurveTo { x1: 16.0, y1: 137.0, x2: 171.0, y2: 45.0, x: 100.0, y: 90.0 }, |
948 | SimplePathSegment::CurveTo { x1: 29.0, y1: 135.0, x2: 171.0, y2: 45.0, x: 180.0, y: 155.0 } |
949 | ); |
950 | |
951 | test !(smooth_quadratic_after_move_to, "M 30 40 T 180 155" , |
952 | SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, |
953 | SimplePathSegment::Quadratic { x1: 30.0, y1: 40.0, x: 180.0, y: 155.0 } |
954 | ); |
955 | |
956 | test !(smooth_quadratic_after_quadratic, "M 30 40 Q 171 45 100 90 T 160 180" , |
957 | SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, |
958 | SimplePathSegment::Quadratic { x1: 171.0, y1: 45.0, x: 100.0, y: 90.0 }, |
959 | SimplePathSegment::Quadratic { x1: 29.0, y1: 135.0, x: 160.0, y: 180.0 } |
960 | ); |
961 | |
962 | test !(relative_smooth_quadratic_after_quadratic, "M 30 40 Q 171 45 100 90 t 60 80" , |
963 | SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, |
964 | SimplePathSegment::Quadratic { x1: 171.0, y1: 45.0, x: 100.0, y: 90.0 }, |
965 | SimplePathSegment::Quadratic { x1: 29.0, y1: 135.0, x: 160.0, y: 170.0 } |
966 | ); |
967 | |
968 | test !(relative_smooth_quadratic_after_relative_quadratic, "M 30 40 q 171 45 50 40 t 60 80" , |
969 | SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }, |
970 | SimplePathSegment::Quadratic { x1: 201.0, y1: 85.0, x: 80.0, y: 80.0 }, |
971 | SimplePathSegment::Quadratic { x1: -41.0, y1: 75.0, x: 140.0, y: 160.0 } |
972 | ); |
973 | |
974 | test !(smooth_quadratic_after_smooth_quadratic, "M 30 30 T 40 140 T 170 30" , |
975 | SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, |
976 | SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, |
977 | SimplePathSegment::Quadratic { x1: 50.0, y1: 250.0, x: 170.0, y: 30.0 } |
978 | ); |
979 | |
980 | test !(smooth_quadratic_after_relative_smooth_quadratic, "M 30 30 T 40 140 t 100 -30" , |
981 | SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, |
982 | SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, |
983 | SimplePathSegment::Quadratic { x1: 50.0, y1: 250.0, x: 140.0, y: 110.0 } |
984 | ); |
985 | |
986 | test !(smooth_quadratic_after_relative_quadratic, "M 30 30 T 40 140 q 30 100 120 -30" , |
987 | SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, |
988 | SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, |
989 | SimplePathSegment::Quadratic { x1: 70.0, y1: 240.0, x: 160.0, y: 110.0 } |
990 | ); |
991 | |
992 | test !(smooth_quadratic_after_relative_smooth_curve_to, "M 30 30 T 40 170 s 90 -20 90 -90" , |
993 | SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, |
994 | SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 170.0 }, |
995 | SimplePathSegment::CurveTo { x1: 40.0, y1: 170.0, x2: 130.0, y2: 150.0, x: 130.0, y: 80.0 } |
996 | ); |
997 | |
998 | test !(quadratic_after_smooth_quadratic, "M 30 30 T 40 140 Q 80 180 170 30" , |
999 | SimplePathSegment::MoveTo { x: 30.0, y: 30.0 }, |
1000 | SimplePathSegment::Quadratic { x1: 30.0, y1: 30.0, x: 40.0, y: 140.0 }, |
1001 | SimplePathSegment::Quadratic { x1: 80.0, y1: 180.0, x: 170.0, y: 30.0 } |
1002 | ); |
1003 | |
1004 | test !(implicit_move_to_after_close_path, "M 10 20 L 30 40 Z L 50 60" , |
1005 | SimplePathSegment::MoveTo { x: 10.0, y: 20.0 }, |
1006 | SimplePathSegment::LineTo { x: 30.0, y: 40.0 }, |
1007 | SimplePathSegment::ClosePath, |
1008 | SimplePathSegment::MoveTo { x: 10.0, y: 20.0 }, |
1009 | SimplePathSegment::LineTo { x: 50.0, y: 60.0 } |
1010 | ); |
1011 | |
1012 | #[test ] |
1013 | fn arc_to() { |
1014 | let mut s = SimplifyingPathParser::from("M 30 40 A 40 30 20 1 1 150 100" ); |
1015 | assert_eq!(s.next().unwrap().unwrap(), SimplePathSegment::MoveTo { x: 30.0, y: 40.0 }); |
1016 | let curve1 = s.next().unwrap().unwrap(); |
1017 | let curve2 = s.next().unwrap().unwrap(); |
1018 | if let Some(res) = s.next() { |
1019 | assert!(res.is_err()); |
1020 | } |
1021 | |
1022 | if let SimplePathSegment::CurveTo { x1, y1, x2, y2, x, y } = curve1 { |
1023 | assert_eq!(x1.round(), 45.0); |
1024 | assert_eq!(y1.round(), 16.0); |
1025 | assert_eq!(x2.round(), 84.0); |
1026 | assert_eq!(y2.round(), 10.0); |
1027 | assert_eq!(x.round(), 117.0); |
1028 | assert_eq!(y.round(), 27.0); |
1029 | } else { |
1030 | panic!("invalid type" ); |
1031 | } |
1032 | |
1033 | if let SimplePathSegment::CurveTo { x1, y1, x2, y2, x, y } = curve2 { |
1034 | assert_eq!(x1.round(), 150.0); |
1035 | assert_eq!(y1.round(), 43.0); |
1036 | assert_eq!(x2.round(), 165.0); |
1037 | assert_eq!(y2.round(), 76.0); |
1038 | assert_eq!(x.round(), 150.0); |
1039 | assert_eq!(y.round(), 100.0); |
1040 | } else { |
1041 | panic!("invalid type" ); |
1042 | } |
1043 | } |
1044 | } |
1045 | |