1use crate::media::{FillRule, SweepDirection};
2use crate::platform::IGeometryContext;
3use crate::utilities::span_helpers::parse_double;
4use crate::utilities::FormatError;
5use crate::{Point, Size};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8enum Command {
9 #[allow(dead_code)]
10 None,
11 FillRule,
12 Move,
13 Line,
14 HorizontalLine,
15 VerticalLine,
16 CubicBezierCurve,
17 QuadraticBezierCurve,
18 SmoothCubicBezierCurve,
19 SmoothQuadraticBezierCurve,
20 Arc,
21 Close,
22}
23
24fn command_from_char(c: char) -> Option<Command> {
25 Some(match c {
26 'F' => Command::FillRule,
27 'M' => Command::Move,
28 'L' => Command::Line,
29 'H' => Command::HorizontalLine,
30 'V' => Command::VerticalLine,
31 'Q' => Command::QuadraticBezierCurve,
32 'T' => Command::SmoothQuadraticBezierCurve,
33 'C' => Command::CubicBezierCurve,
34 'S' => Command::SmoothCubicBezierCurve,
35 'A' => Command::Arc,
36 'Z' => Command::Close,
37 _ => return None,
38 })
39}
40
41pub struct PathMarkupParser<'a> {
43 geometry_context: Option<&'a mut dyn IGeometryContext>,
44 current_point: Point,
45 begin_figure_point: Option<Point>,
46 previous_control_point: Option<Point>,
47 is_open: bool,
48 is_disposed: bool,
49}
50
51impl<'a> PathMarkupParser<'a> {
52 pub fn new(geometry_context: &'a mut dyn IGeometryContext) -> Self {
54 Self {
55 geometry_context: Some(geometry_context),
56 current_point: Point::default(),
57 begin_figure_point: None,
58 previous_control_point: None,
59 is_open: false,
60 is_disposed: false,
61 }
62 }
63
64 pub fn dispose(&mut self) {
66 if self.is_disposed {
67 return;
68 }
69
70 self.geometry_context = None;
71 self.is_disposed = true;
72 }
73
74 fn context(&mut self) -> &mut dyn IGeometryContext {
75 match self.geometry_context.as_deref_mut() {
76 Some(context) if !self.is_disposed => context,
77 _ => panic!("PathMarkupParser is disposed"),
78 }
79 }
80
81 fn mirror_control_point(control_point: Point, center: Point) -> Point {
82 let dir = control_point - center;
83 center + -dir
84 }
85
86 pub fn parse(&mut self, path_data: &str) -> Result<(), FormatError> {
89 let _ = self.context();
90
91 let mut span = path_data;
92 self.current_point = Point::default();
93
94 while !span.is_empty() {
95 let Some((command, relative)) = read_command(&mut span)? else {
96 break;
97 };
98
99 let mut initial_command = true;
100
101 loop {
102 if !initial_command {
103 span = read_separator(span);
104 }
105
106 match command {
107 Command::None => {}
108 Command::FillRule => self.set_fill_rule(&mut span)?,
109 Command::Move => self.add_move(&mut span, relative)?,
110 Command::Line => self.add_line(&mut span, relative)?,
111 Command::HorizontalLine => self.add_horizontal_line(&mut span, relative)?,
112 Command::VerticalLine => self.add_vertical_line(&mut span, relative)?,
113 Command::CubicBezierCurve => self.add_cubic_bezier_curve(&mut span, relative)?,
114 Command::QuadraticBezierCurve => self.add_quadratic_bezier_curve(&mut span, relative)?,
115 Command::SmoothCubicBezierCurve => self.add_smooth_cubic_bezier_curve(&mut span, relative)?,
116 Command::SmoothQuadraticBezierCurve => {
117 self.add_smooth_quadratic_bezier_curve(&mut span, relative)?
118 }
119 Command::Arc => self.add_arc(&mut span, relative)?,
120 Command::Close => self.close_figure(),
121 }
122
123 initial_command = false;
124
125 if !peek_argument(span) {
126 break;
127 }
128 }
129 }
130
131 if self.is_open {
132 self.context().end_figure(false);
133 }
134
135 Ok(())
136 }
137
138 fn create_figure(&mut self) {
139 if self.is_open {
140 self.context().end_figure(false);
141 }
142
143 let current_point = self.current_point;
144 self.context().begin_figure(current_point, true);
145
146 self.begin_figure_point = Some(current_point);
147
148 self.is_open = true;
149 }
150
151 fn set_fill_rule(&mut self, span: &mut &str) -> Result<(), FormatError> {
152 let fill_rule = match read_argument(span) {
153 Some(fill_rule) if fill_rule.len() == 1 => fill_rule,
154 _ => return Err(FormatError::new("Invalid fill rule.")),
155 };
156
157 let rule = match fill_rule {
158 "0" => FillRule::EvenOdd,
159 "1" => FillRule::NonZero,
160 _ => return Err(FormatError::new("Invalid fill rule")),
161 };
162
163 self.context().set_fill_rule(rule);
164 Ok(())
165 }
166
167 fn close_figure(&mut self) {
168 if self.is_open {
169 self.context().end_figure(true);
170 if let Some(begin_figure_point) = self.begin_figure_point.take() {
171 self.current_point = begin_figure_point;
172 }
173 }
174
175 self.previous_control_point = None;
176
177 self.is_open = false;
178 }
179
180 fn add_move(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
181 let current_point =
182 if relative { read_relative_point(span, self.current_point)? } else { read_point(span)? };
183
184 self.current_point = current_point;
185
186 self.create_figure();
187
188 while peek_argument(span) {
189 *span = read_separator(span);
190
191 self.add_line(span, relative)?;
192 }
193
194 Ok(())
195 }
196
197 fn add_line(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
198 let next = if relative { read_relative_point(span, self.current_point)? } else { read_point(span)? };
199
200 if !self.is_open {
201 self.create_figure();
202 }
203
204 self.context().line_to(next, true);
205
206 self.current_point = next;
207 Ok(())
208 }
209
210 fn add_horizontal_line(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
211 let value = read_double(span)?;
212 let next = if relative {
213 Point::new(self.current_point.x + value, self.current_point.y)
214 } else {
215 self.current_point.with_x(value)
216 };
217
218 if !self.is_open {
219 self.create_figure();
220 }
221
222 self.context().line_to(next, true);
223
224 self.current_point = next;
225 Ok(())
226 }
227
228 fn add_vertical_line(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
229 let value = read_double(span)?;
230 let next = if relative {
231 Point::new(self.current_point.x, self.current_point.y + value)
232 } else {
233 self.current_point.with_y(value)
234 };
235
236 if !self.is_open {
237 self.create_figure();
238 }
239
240 self.context().line_to(next, true);
241
242 self.current_point = next;
243 Ok(())
244 }
245
246 fn read_command_point(&self, span: &mut &str, relative: bool) -> Result<Point, FormatError> {
247 if relative {
248 read_relative_point(span, self.current_point)
249 } else {
250 read_point(span)
251 }
252 }
253
254 fn add_cubic_bezier_curve(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
255 let point1 = self.read_command_point(span, relative)?;
256
257 *span = read_separator(span);
258
259 let point2 = self.read_command_point(span, relative)?;
260
261 self.previous_control_point = Some(point2);
262
263 *span = read_separator(span);
264
265 let point3 = self.read_command_point(span, relative)?;
266
267 if !self.is_open {
268 self.create_figure();
269 }
270
271 self.context().cubic_bezier_to(point1, point2, point3, true);
272
273 self.current_point = point3;
274 Ok(())
275 }
276
277 fn add_quadratic_bezier_curve(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
278 let start = self.read_command_point(span, relative)?;
279
280 self.previous_control_point = Some(start);
281
282 *span = read_separator(span);
283
284 let end = self.read_command_point(span, relative)?;
285
286 if !self.is_open {
287 self.create_figure();
288 }
289
290 self.context().quadratic_bezier_to(start, end, true);
291
292 self.current_point = end;
293 Ok(())
294 }
295
296 fn add_smooth_cubic_bezier_curve(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
297 let point2 = self.read_command_point(span, relative)?;
298
299 *span = read_separator(span);
300
301 let end = self.read_command_point(span, relative)?;
302
303 if let Some(previous_control_point) = self.previous_control_point {
304 self.previous_control_point = Some(Self::mirror_control_point(previous_control_point, self.current_point));
305 }
306
307 if !self.is_open {
308 self.create_figure();
309 }
310
311 let point1 = self.previous_control_point.unwrap_or(self.current_point);
312 self.context().cubic_bezier_to(point1, point2, end, true);
313
314 self.previous_control_point = Some(point2);
315
316 self.current_point = end;
317 Ok(())
318 }
319
320 fn add_smooth_quadratic_bezier_curve(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
321 let end = self.read_command_point(span, relative)?;
322
323 if let Some(previous_control_point) = self.previous_control_point {
324 self.previous_control_point = Some(Self::mirror_control_point(previous_control_point, self.current_point));
325 }
326
327 if !self.is_open {
328 self.create_figure();
329 }
330
331 let control_point = self.previous_control_point.unwrap_or(self.current_point);
332 self.context().quadratic_bezier_to(control_point, end, true);
333
334 self.current_point = end;
335 Ok(())
336 }
337
338 fn add_arc(&mut self, span: &mut &str, relative: bool) -> Result<(), FormatError> {
339 let size = read_size(span)?;
340
341 *span = read_separator(span);
342
343 let rotation_angle = read_double(span)?;
344 *span = read_separator(span);
345 let is_large_arc = read_bool(span)?;
346
347 *span = read_separator(span);
348
349 let sweep_direction =
350 if read_bool(span)? { SweepDirection::Clockwise } else { SweepDirection::CounterClockwise };
351
352 *span = read_separator(span);
353
354 let end = self.read_command_point(span, relative)?;
355
356 if !self.is_open {
357 self.create_figure();
358 }
359
360 self.context().arc_to(end, size, rotation_angle, is_large_arc, sweep_direction, true);
361
362 self.current_point = end;
363
364 self.previous_control_point = None;
365 Ok(())
366 }
367}
368
369fn peek_argument(span: &str) -> bool {
370 let span = skip_whitespace(span);
371
372 match span.chars().next() {
373 Some(c) => c == ',' || c == '-' || c == '.' || c.is_ascii_digit(),
374 None => false,
375 }
376}
377
378fn read_argument<'s>(remaining: &mut &'s str) -> Option<&'s str> {
381 *remaining = skip_whitespace(remaining);
382 if remaining.is_empty() {
383 return None;
384 }
385
386 let bytes = remaining.as_bytes();
387 let mut valid = false;
388 let mut i = 0;
389 if bytes[i] == b'-' {
390 i += 1;
391 }
392 while i < bytes.len() && bytes[i].is_ascii_digit() {
393 valid = true;
394 i += 1;
395 }
396
397 if i < bytes.len() && bytes[i] == b'.' {
398 valid = false;
399 i += 1;
400 }
401 while i < bytes.len() && bytes[i].is_ascii_digit() {
402 valid = true;
403 i += 1;
404 }
405
406 if i < bytes.len() {
407 if bytes[i] == b'E' || bytes[i] == b'e' {
409 valid = false;
410 i += 1;
411 if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
412 i += 1;
413 while i < bytes.len() && bytes[i].is_ascii_digit() {
414 valid = true;
415 i += 1;
416 }
417 }
418 }
419 }
420
421 if !valid {
422 return None;
423 }
424 let (argument, rest) = remaining.split_at(i);
425 *remaining = rest;
426 Some(argument)
427}
428
429fn read_separator(span: &str) -> &str {
430 let span = skip_whitespace(span);
431 span.strip_prefix(',').unwrap_or(span)
432}
433
434fn skip_whitespace(span: &str) -> &str {
435 span.trim_start_matches(char::is_whitespace)
436}
437
438fn read_bool(span: &mut &str) -> Result<bool, FormatError> {
439 *span = skip_whitespace(span);
440
441 let mut chars = span.chars();
442 let Some(c) = chars.next() else {
443 return Err(FormatError::new("Invalid bool rule."));
444 };
445
446 *span = chars.as_str();
447
448 match c {
449 '0' => Ok(false),
450 '1' => Ok(true),
451 _ => Err(FormatError::new("Invalid bool rule")),
452 }
453}
454
455fn read_double(span: &mut &str) -> Result<f64, FormatError> {
456 let Some(double_value) = read_argument(span) else {
457 return Err(FormatError::new("Invalid double value"));
458 };
459
460 parse_double(double_value).ok_or_else(|| FormatError::invalid_input(double_value))
461}
462
463fn read_size(span: &mut &str) -> Result<Size, FormatError> {
464 let width = read_double(span)?;
465 *span = read_separator(span);
466 let height = read_double(span)?;
467 Ok(Size::new(width, height))
468}
469
470fn read_point(span: &mut &str) -> Result<Point, FormatError> {
471 let x = read_double(span)?;
472 *span = read_separator(span);
473 let y = read_double(span)?;
474 Ok(Point::new(x, y))
475}
476
477fn read_relative_point(span: &mut &str, origin: Point) -> Result<Point, FormatError> {
478 let x = read_double(span)?;
479 *span = read_separator(span);
480 let y = read_double(span)?;
481 Ok(Point::new(origin.x + x, origin.y + y))
482}
483
484fn read_command(span: &mut &str) -> Result<Option<(Command, bool)>, FormatError> {
485 *span = skip_whitespace(span);
486 let mut chars = span.chars();
487 let Some(c) = chars.next() else {
488 return Ok(None);
489 };
490
491 let Some(command) = c.to_uppercase().next().and_then(command_from_char) else {
492 return Err(FormatError::from_string(format!("Unexpected path command '{c}'.")));
493 };
494
495 let relative = c.is_lowercase();
496 *span = chars.as_str();
497 Ok(Some((command, relative)))
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::media::{ArcSegment, LineSegment, PathFigure, PathGeometry};
504 use crate::platform::PathGeometryContext;
505 use crate::Ref;
506
507 fn parse(path_data: &str) -> Result<Ref<PathGeometry>, FormatError> {
508 let path_geometry = PathGeometry::new();
509 let mut context = PathGeometryContext::new(path_geometry.clone());
510 let mut parser = PathMarkupParser::new(&mut context);
511 let result = parser.parse(path_data);
512 parser.dispose();
513 context.dispose();
514 result.map(|_| path_geometry)
515 }
516
517 fn figure(geometry: &PathGeometry, index: usize) -> Ref<PathFigure> {
518 geometry.figures().unwrap().get(index)
519 }
520
521 fn line_point(figure: &PathFigure, index: usize) -> Point {
522 figure.segments().unwrap().get(index).cast::<LineSegment>().expect("line segment").point()
523 }
524
525 #[test]
526 fn parses_move() {
527 let path_geometry = parse("M10 10").unwrap();
528 assert_eq!(Point::new(10.0, 10.0), figure(&path_geometry, 0).start_point());
529 }
530
531 #[test]
532 fn parses_line() {
533 let path_geometry = parse("M0 0L10 10").unwrap();
534 assert_eq!(Point::new(10.0, 10.0), line_point(&figure(&path_geometry, 0), 0));
535 }
536
537 #[test]
538 fn parses_close() {
539 let path_geometry = parse("M0 0L10 10z").unwrap();
540 assert!(figure(&path_geometry, 0).is_closed());
541 }
542
543 #[test]
544 fn parses_fill_mode_before_move() {
545 let path_geometry = parse("F 1M0,0").unwrap();
546 assert_eq!(FillRule::NonZero, path_geometry.fill_rule());
547 }
548
549 #[test]
550 fn parses_implicit_line_command_after_move() {
551 for path_data in ["M0 0 10 10 20 20", "M0,0 10,10 20,20", "M0,0,10,10,20,20"] {
552 let path_geometry = parse(path_data).unwrap();
553 let figure = figure(&path_geometry, 0);
554 assert_eq!(Point::new(10.0, 10.0), line_point(&figure, 0));
555 assert_eq!(Point::new(20.0, 20.0), line_point(&figure, 1));
556 }
557 }
558
559 #[test]
560 fn parses_implicit_line_command_after_relative_move() {
561 for path_data in ["m0 0 10 10 20 20", "m0,0 10,10 20,20", "m0,0,10,10,20,20"] {
562 let path_geometry = parse(path_data).unwrap();
563 let figure = figure(&path_geometry, 0);
564 assert_eq!(Point::new(10.0, 10.0), line_point(&figure, 0));
565 assert_eq!(Point::new(30.0, 30.0), line_point(&figure, 1));
566 }
567 }
568
569 #[test]
570 fn parses_scientific_notation_double() {
571 let path_geometry = parse("M -1.01725E-005 -1.01725e-005").unwrap();
572 assert_eq!(Point::new(-1.01725E-005, -1.01725E-005), figure(&path_geometry, 0).start_point());
573 }
574
575 #[test]
576 fn should_parse() {
577 for path_data in [
578 "M5.5.5 5.5.5 5.5.5",
579 concat!(
580 "F1M9.0771,11C9.1161,10.701,9.1801,10.352,9.3031,10L9.0001,10 9.0001,6.166 3.0001,9.767 3.0001,10 ",
581 "9.99999999997669E-05,10 9.99999999997669E-05,0 3.0001,0 3.0001,0.234 9.0001,3.834 9.0001,0 ",
582 "12.0001,0 12.0001,8.062C12.1861,8.043 12.3821,8.031 12.5941,8.031 15.3481,8.031 15.7961,9.826 ",
583 "15.9201,11L16.0001,16 9.0001,16 9.0001,12.562 9.0001,11z"
584 ),
585 " M0 0",
586 "F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z",
587 "M0 0L10 10z",
588 "M50 50 L100 100 L150 50",
589 "M50 50L100 100L150 50",
590 "M50,50 L100,100 L150,50",
591 "M50 50 L-10 -10 L10 50",
592 "M50 50L-10-10L10 50",
593 "M50 50 L100 100 L150 50zM50 50 L70 70 L120 50z",
594 "M 50 50 L 100 100 L 150 50",
595 "M50 50 L100 100 L150 50 H200 V100Z",
596 "M 80 200 A 100 50 45 1 0 100 50",
597 concat!(
598 "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063",
599 " 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9",
600 "062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z",
601 " M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18",
602 ".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34",
603 ".9063 10.1309 30.9062 16.6309 19.9063 Z "
604 ),
605 concat!(
606 "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 ",
607 "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 ",
608 "12.461,8.046C14.45,8.278,16,9.949,16,12"
609 ),
610 ] {
611 assert!(parse(path_data).is_ok(), "{path_data}");
612 }
613 }
614
615 #[derive(Default)]
616 struct CountingContext {
617 end_figure_calls: usize,
618 }
619
620 impl IGeometryContext for CountingContext {
621 fn arc_to(&mut self, _: Point, _: Size, _: f64, _: bool, _: SweepDirection, _: bool) {}
622 fn begin_figure(&mut self, _: Point, _: bool) {}
623 fn cubic_bezier_to(&mut self, _: Point, _: Point, _: Point, _: bool) {}
624 fn quadratic_bezier_to(&mut self, _: Point, _: Point, _: bool) {}
625 fn line_to(&mut self, _: Point, _: bool) {}
626 fn end_figure(&mut self, _: bool) {
627 self.end_figure_calls += 1;
628 }
629 fn set_fill_rule(&mut self, _: FillRule) {}
630 fn dispose(&mut self) {}
631 }
632
633 #[test]
634 fn should_always_end_figure() {
635 for path_data in
636 ["M0 0L10 10", "M0 0L10 10z", "M0 0L10 10 \n ", "M0 0L10 10z \n ", "M0 0L10 10 ", "M0 0L10 10z "]
637 {
638 let mut context = CountingContext::default();
639 PathMarkupParser::new(&mut context).parse(path_data).unwrap();
640 assert!(context.end_figure_calls >= 1, "{path_data:?}");
641 }
642 }
643
644 #[test]
645 fn parsed_geometry_to_string_should_produce_valid_value() {
646 for path_data in [
647 "M 5.5, 5 L 5.5, 5 L 5.5, 5",
648 concat!(
649 "F1 M 9.0771, 11 C 9.1161, 10.701 9.1801, 10.352 9.3031, 10 L 9.0001, 10 L 9.0001, 6.166 L 3.0001, 9.767 L 3.0001, 10 ",
650 "L 9.99999999997669E-05, 10 L 9.99999999997669E-05, 0 L 3.0001, 0 L 3.0001, 0.234 L 9.0001, 3.834 L 9.0001, 0 ",
651 "L 12.0001, 0 L 12.0001, 8.062 C 12.1861, 8.043 12.3821, 8.031 12.5941, 8.031 C 15.3481, 8.031 15.7961, 9.826 ",
652 "15.9201, 11 L 16.0001, 16 L 9.0001, 16 L 9.0001, 12.562 L 9.0001, 11Z"
653 ),
654 "F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z",
655 "M 0, 0 L 10, 10Z",
656 "M 50, 50 L 100, 100 L 150, 50",
657 "M 50, 50 L -10, -10 L 10, 50",
658 "M 50, 50 L 100, 100 L 150, 50Z M 50, 50 L 70, 70 L 120, 50Z",
659 "M 80, 200 A 100, 50 45 1 0 100, 50",
660 concat!(
661 "F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 ",
662 "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L ",
663 "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12"
664 ),
665 ] {
666 let target = PathGeometry::parse(path_data).unwrap();
667 assert_eq!(path_data, target.to_string());
668 }
669 }
670
671 #[test]
672 fn parsed_geometry_to_string_should_format_value() {
673 for (path_data, formatted_path_data) in [
674 ("M5.5.5 5.5.5 5.5.5", "M 5.5, 0.5 L 5.5, 0.5 L 5.5, 0.5"),
675 (
676 "F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z",
677 "F1 M 24, 14 A 2, 2 0 1 1 20, 14 A 2, 2 0 1 1 24, 14Z",
678 ),
679 (
680 concat!(
681 "F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 ",
682 "4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 ",
683 "12.461,8.046C14.45,8.278,16,9.949,16,12"
684 ),
685 concat!(
686 "F1 M 16, 12 C 16, 14.209 14.209, 16 12, 16 C 9.791, 16 8, 14.209 8, 12 C 8, 11.817 8.03, 11.644 8.054, 11.467 L 6.585, 10 ",
687 "L 4, 10 L 4, 6.414 L 2.5, 7.914 L 0, 5.414 L 0, 3.586 L 3.586, 0 L 4.414, 0 L 7.414, 3 L 7.586, 3 L 9, 1.586 L ",
688 "11.914, 4.5 L 10.414, 6 L 12.461, 8.046 C 14.45, 8.278 16, 9.949 16, 12"
689 ),
690 ),
691 ] {
692 let target = PathGeometry::parse(path_data).unwrap();
693 assert_eq!(formatted_path_data, target.to_string());
694 }
695 }
696
697 #[test]
698 fn throws_invalid_data_exception_on_none_defined_command() {
699 for path_data in ["0 0", "j"] {
700 assert!(parse(path_data).is_err(), "{path_data}");
701 }
702 }
703
704 #[test]
705 fn returns_error_on_malformed_arguments() {
706 for path_data in ["M", "M10", "M10 a", "F2M0 0", "F", "M0 0 A1 1 0 2 0 1 1", "M0 0 A1 1 0 1", "M1e", "M1e+", "L.", "M0 0 H", "é"]
707 {
708 assert!(parse(path_data).is_err(), "{path_data}");
709 }
710 }
711
712 #[test]
713 fn close_figure_should_move_current_point_to_create_figure_point() {
714 let path_geometry = parse("M10,10L100,100Z m10,10").unwrap();
715 assert_eq!(2, path_geometry.figures().unwrap().len());
716
717 let first = figure(&path_geometry, 0);
718 assert_eq!(Point::new(10.0, 10.0), first.start_point());
719 assert!(first.is_closed());
720 assert_eq!(Point::new(100.0, 100.0), line_point(&first, 0));
721
722 let second = figure(&path_geometry, 1);
723 assert_eq!(Point::new(20.0, 20.0), second.start_point());
724 }
725
726 #[test]
727 fn should_parse_flags_without_separator() {
728 let path_geometry = parse("a.898.898 0 01.27.188").unwrap();
729 let segments = figure(&path_geometry, 0).segments().unwrap();
730 assert_eq!(1, segments.len());
731 assert!(segments.get(0).is::<ArcSegment>());
732 }
733
734 #[test]
735 fn should_handle_start_point_after_empty_figure() {
736 let path_geometry = parse("M50,50z l -5,-5").unwrap();
737 assert_eq!(2, path_geometry.figures().unwrap().len());
738 assert_eq!(Point::new(50.0, 50.0), figure(&path_geometry, 0).start_point());
739 assert_eq!(Point::new(50.0, 50.0), figure(&path_geometry, 1).start_point());
740 }
741
742 #[test]
743 fn smooth_curves_mirror_the_previous_control_point() {
744 let path_geometry = parse("M0,0 C0,10 10,10 10,0 S20,-10 20,0 Q25,5 30,0 T40,0").unwrap();
745 let figure = figure(&path_geometry, 0);
746 let segments = figure.segments().unwrap();
747 assert_eq!(4, segments.len());
748 let smooth = segments.get(1).cast::<crate::media::BezierSegment>().unwrap();
749 assert_eq!(Point::new(10.0, -10.0), smooth.point1());
750 assert_eq!(Point::new(20.0, -10.0), smooth.point2());
751 assert_eq!(Point::new(20.0, 0.0), smooth.point3());
752 let smooth_quadratic = segments.get(3).cast::<crate::media::QuadraticBezierSegment>().unwrap();
753 assert_eq!(Point::new(35.0, -5.0), smooth_quadratic.point1());
754 assert_eq!(Point::new(40.0, 0.0), smooth_quadratic.point2());
755 }
756}