Skip to main content

ferroui_base/media/
path_markup_parser.rs

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
41/// Parses a path markup string.
42pub 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    /// Creates a parser that draws into `geometry_context`.
53    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    /// Releases the geometry context. Using the parser afterwards panics.
65    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    /// Parses the specified path data and writes the result to the geometry
87    /// context of this instance.
88    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
378/// Reads one numeric argument off the front of `remaining`. All inspected
379/// characters are ASCII, so the byte offsets are character boundaries.
380fn 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        // scientific notation
408        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}