animate/path/
writer.rs

1use super::{Path, PathCommand, PathSegment, WriteBuffer, WriteOptions};
2
3struct PrevCmd {
4    cmd: PathCommand,
5    absolute: bool,
6    implicit: bool,
7}
8
9impl WriteBuffer for Path {
10    fn write_buf_opt(&self, opt: &WriteOptions, buf: &mut Vec<u8>) {
11        if self.is_empty() {
12            return;
13        }
14
15        let mut prev_cmd: Option<PrevCmd> = None;
16        let mut prev_coord_has_dot = false;
17
18        for seg in self.iter() {
19            let is_written = write_cmd(seg, &mut prev_cmd, opt, buf);
20            write_segment(seg, is_written, &mut prev_coord_has_dot, opt, buf);
21        }
22
23        if !opt.use_compact_path_notation {
24            let len = buf.len();
25            buf.truncate(len - 1);
26        }
27    }
28}
29
30fn write_cmd(
31    seg: &PathSegment,
32    prev_cmd: &mut Option<PrevCmd>,
33    opt: &WriteOptions,
34    buf: &mut Vec<u8>,
35) -> bool {
36    let mut print_cmd = true;
37    if opt.remove_duplicated_path_commands {
38        // check that previous command is the same as current
39        if let Some(ref pcmd) = *prev_cmd {
40            // MoveTo commands can't be skipped
41            if pcmd.cmd != PathCommand::MoveTo {
42                if seg.cmd() == pcmd.cmd && seg.is_absolute() == pcmd.absolute {
43                    print_cmd = false;
44                }
45            }
46        }
47    }
48
49    let mut is_implicit = false;
50    if opt.use_implicit_lineto_commands {
51        let check_implicit = || {
52            if let Some(ref pcmd) = *prev_cmd {
53                if seg.is_absolute() != pcmd.absolute {
54                    return false;
55                }
56
57                if pcmd.implicit {
58                    if seg.cmd() == PathCommand::LineTo {
59                        return true;
60                    }
61                } else if pcmd.cmd == PathCommand::MoveTo && seg.cmd() == PathCommand::LineTo {
62                    // if current segment is LineTo and previous was MoveTo
63                    return true;
64                }
65            }
66
67            false
68        };
69
70        if check_implicit() {
71            is_implicit = true;
72            print_cmd = false;
73        }
74    }
75
76    *prev_cmd = Some(PrevCmd {
77        cmd: seg.cmd(),
78        absolute: seg.is_absolute(),
79        implicit: is_implicit,
80    });
81
82    if !print_cmd {
83        // we do not update 'prev_cmd' if we do not wrote it
84        return false;
85    }
86
87    write_cmd_char(seg, buf);
88
89    if !(seg.cmd() == PathCommand::ClosePath || opt.use_compact_path_notation) {
90        buf.push(b' ');
91    }
92
93    true
94}
95
96pub fn write_cmd_char(seg: &PathSegment, buf: &mut Vec<u8>) {
97    let cmd: u8 = if seg.is_absolute() {
98        match seg.cmd() {
99            PathCommand::MoveTo => b'M',
100            PathCommand::LineTo => b'L',
101            PathCommand::HorizontalLineTo => b'H',
102            PathCommand::VerticalLineTo => b'V',
103            PathCommand::CurveTo => b'C',
104            PathCommand::SmoothCurveTo => b'S',
105            PathCommand::Quadratic => b'Q',
106            PathCommand::SmoothQuadratic => b'T',
107            PathCommand::EllipticalArc => b'A',
108            PathCommand::ClosePath => b'Z',
109        }
110    } else {
111        match seg.cmd() {
112            PathCommand::MoveTo => b'm',
113            PathCommand::LineTo => b'l',
114            PathCommand::HorizontalLineTo => b'h',
115            PathCommand::VerticalLineTo => b'v',
116            PathCommand::CurveTo => b'c',
117            PathCommand::SmoothCurveTo => b's',
118            PathCommand::Quadratic => b'q',
119            PathCommand::SmoothQuadratic => b't',
120            PathCommand::EllipticalArc => b'a',
121            PathCommand::ClosePath => b'z',
122        }
123    };
124    buf.push(cmd);
125}
126
127pub fn write_segment(
128    data: &PathSegment,
129    is_written: bool,
130    prev_coord_has_dot: &mut bool,
131    opt: &WriteOptions,
132    buf: &mut Vec<u8>,
133) {
134    match *data {
135        PathSegment::MoveTo { x, y, .. }
136        | PathSegment::LineTo { x, y, .. }
137        | PathSegment::SmoothQuadratic { x, y, .. } => {
138            write_coords(&[x, y], is_written, prev_coord_has_dot, opt, buf);
139        }
140
141        PathSegment::HorizontalLineTo { x, .. } => {
142            write_coords(&[x], is_written, prev_coord_has_dot, opt, buf);
143        }
144
145        PathSegment::VerticalLineTo { y, .. } => {
146            write_coords(&[y], is_written, prev_coord_has_dot, opt, buf);
147        }
148
149        PathSegment::CurveTo {
150            x1,
151            y1,
152            x2,
153            y2,
154            x,
155            y,
156            ..
157        } => {
158            write_coords(
159                &[x1, y1, x2, y2, x, y],
160                is_written,
161                prev_coord_has_dot,
162                opt,
163                buf,
164            );
165        }
166
167        PathSegment::SmoothCurveTo { x2, y2, x, y, .. } => {
168            write_coords(&[x2, y2, x, y], is_written, prev_coord_has_dot, opt, buf);
169        }
170
171        PathSegment::Quadratic { x1, y1, x, y, .. } => {
172            write_coords(&[x1, y1, x, y], is_written, prev_coord_has_dot, opt, buf);
173        }
174
175        PathSegment::EllipticalArc {
176            rx,
177            ry,
178            x_axis_rotation,
179            large_arc,
180            sweep,
181            x,
182            y,
183            ..
184        } => {
185            write_coords(
186                &[rx, ry, x_axis_rotation],
187                is_written,
188                prev_coord_has_dot,
189                opt,
190                buf,
191            );
192
193            if opt.use_compact_path_notation {
194                // flags must always have a space before it
195                buf.push(b' ');
196            }
197
198            write_flag(large_arc, buf);
199            if !opt.join_arc_to_flags {
200                buf.push(b' ');
201            }
202            write_flag(sweep, buf);
203            if !opt.join_arc_to_flags {
204                buf.push(b' ');
205            }
206
207            // reset, because flags can't have dots
208            *prev_coord_has_dot = false;
209
210            // 'is_explicit_cmd' is always 'true'
211            // because it's relevant only for first coordinate of the segment
212            write_coords(&[x, y], true, prev_coord_has_dot, opt, buf);
213        }
214        PathSegment::ClosePath { .. } => {
215            if !opt.use_compact_path_notation {
216                buf.push(b' ');
217            }
218        }
219    }
220}
221
222fn write_coords(
223    coords: &[f64],
224    is_explicit_cmd: bool,
225    prev_coord_has_dot: &mut bool,
226    opt: &WriteOptions,
227    buf: &mut Vec<u8>,
228) {
229    if opt.use_compact_path_notation {
230        for (i, num) in coords.iter().enumerate() {
231            let start_pos = buf.len() - 1;
232
233            num.write_buf_opt(opt, buf);
234
235            let c = buf[start_pos + 1];
236
237            let write_space = if !*prev_coord_has_dot && c == b'.' {
238                !(i == 0 && is_explicit_cmd)
239            } else if i == 0 && is_explicit_cmd {
240                false
241            } else if (c as char).is_digit(10) {
242                true
243            } else {
244                false
245            };
246
247            if write_space {
248                buf.insert(start_pos + 1, b' ');
249            }
250
251            *prev_coord_has_dot = false;
252            for c in buf.iter().skip(start_pos) {
253                if *c == b'.' {
254                    *prev_coord_has_dot = true;
255                    break;
256                }
257            }
258        }
259    } else {
260        for num in coords.iter() {
261            num.write_buf_opt(opt, buf);
262            buf.push(b' ');
263        }
264    }
265}
266
267fn write_flag(flag: bool, buf: &mut Vec<u8>) {
268    buf.push(if flag { b'1' } else { b'0' });
269}
270
271impl ::std::fmt::Display for Path {
272    #[inline]
273    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
274        write!(f, "{}", self.with_write_opt(&WriteOptions::default()))
275    }
276}
277
278impl ::std::fmt::Debug for Path {
279    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
280        // Overload Display.
281        write!(f, "{}", &self)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use std::str::FromStr;
288
289    use super::*;
290    use WriteOptions;
291
292    #[test]
293    fn write_1() {
294        let mut path = Path::new();
295        path.push(PathSegment::MoveTo {
296            abs: true,
297            x: 10.0,
298            y: 20.0,
299        });
300        path.push(PathSegment::LineTo {
301            abs: true,
302            x: 10.0,
303            y: 20.0,
304        });
305        assert_eq!(path.to_string(), "M 10 20 L 10 20");
306    }
307
308    #[test]
309    fn write_2() {
310        let path = Path::from_str("M 10 20 l 10 20").unwrap();
311        assert_eq!(path.to_string(), "M 10 20 l 10 20");
312    }
313
314    #[test]
315    fn write_3() {
316        let path = Path::from_str(
317            "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 \
318             S 130 140 150 160 Q 170 180 190 200 T 210 220 \
319             A 50 50 30 1 1 230 240 Z",
320        )
321        .unwrap();
322        assert_eq!(
323            path.to_string(),
324            "M 10 20 L 30 40 H 50 V 60 C 70 80 90 100 110 120 \
325             S 130 140 150 160 Q 170 180 190 200 T 210 220 \
326             A 50 50 30 1 1 230 240 Z"
327        );
328    }
329
330    #[test]
331    fn write_4() {
332        let path = Path::from_str(
333            "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 \
334             s 130 140 150 160 q 170 180 190 200 t 210 220 \
335             a 50 50 30 1 1 230 240 z",
336        )
337        .unwrap();
338        assert_eq!(
339            path.to_string(),
340            "m 10 20 l 30 40 h 50 v 60 c 70 80 90 100 110 120 \
341             s 130 140 150 160 q 170 180 190 200 t 210 220 \
342             a 50 50 30 1 1 230 240 z"
343        );
344    }
345
346    #[test]
347    fn write_5() {
348        let path = Path::from_str("").unwrap();
349        assert_eq!(path.to_string(), "");
350    }
351
352    macro_rules! test_write_opt {
353        ($name:ident, $in_text:expr, $out_text:expr, $flag:ident) => {
354            #[test]
355            fn $name() {
356                let path = Path::from_str($in_text).unwrap();
357
358                let mut opt = WriteOptions::default();
359                opt.$flag = true;
360
361                assert_eq!(path.with_write_opt(&opt).to_string(), $out_text);
362            }
363        };
364    }
365
366    test_write_opt!(
367        write_6,
368        "M 10 20 L 30 40 L 50 60 l 70 80",
369        "M 10 20 L 30 40 50 60 l 70 80",
370        remove_duplicated_path_commands
371    );
372
373    test_write_opt!(
374        write_7,
375        "M 10 20 30 40 50 60",
376        "M 10 20 L 30 40 50 60",
377        remove_duplicated_path_commands
378    );
379
380    test_write_opt!(
381        write_8,
382        "M 10 20 L 30 40",
383        "M10 20L30 40",
384        use_compact_path_notation
385    );
386
387    test_write_opt!(
388        write_9,
389        "M 10 20 V 30 H 40 V 50 H 60 Z",
390        "M10 20V30H40V50H60Z",
391        use_compact_path_notation
392    );
393
394    #[test]
395    fn write_10() {
396        let path = Path::from_str("M 10 -20 A 5.5 0.3 -4 1 1 0 -0.1").unwrap();
397
398        let mut opt = WriteOptions::default();
399        opt.use_compact_path_notation = true;
400        opt.join_arc_to_flags = true;
401        opt.remove_leading_zero = true;
402
403        assert_eq!(
404            path.with_write_opt(&opt).to_string(),
405            "M10-20A5.5.3-4 110-.1"
406        );
407    }
408
409    test_write_opt!(
410        write_11,
411        "M 10-10 a 1 1 0 1 1 -1 1",
412        "M10-10a1 1 0 1 1 -1 1",
413        use_compact_path_notation
414    );
415
416    test_write_opt!(
417        write_12,
418        "M 10-10 a 1 1 0 1 1 0.1 1",
419        "M10-10a1 1 0 1 1 0.1 1",
420        use_compact_path_notation
421    );
422
423    test_write_opt!(
424        write_13,
425        "M 10 20 L 30 40 L 50 60 H 10",
426        "M 10 20 30 40 50 60 H 10",
427        use_implicit_lineto_commands
428    );
429
430    // should be ignored, because of different 'absolute' values
431    test_write_opt!(
432        write_14,
433        "M 10 20 l 30 40 L 50 60",
434        "M 10 20 l 30 40 L 50 60",
435        use_implicit_lineto_commands
436    );
437
438    test_write_opt!(
439        write_15,
440        "M 10 20 L 30 40 l 50 60 L 50 60",
441        "M 10 20 30 40 l 50 60 L 50 60",
442        use_implicit_lineto_commands
443    );
444
445    test_write_opt!(
446        write_16,
447        "M 10 20 L 30 40 l 50 60",
448        "M 10 20 30 40 l 50 60",
449        use_implicit_lineto_commands
450    );
451
452    test_write_opt!(
453        write_17,
454        "M 10 20 L 30 40 L 50 60 M 10 20 L 30 40 L 50 60",
455        "M 10 20 30 40 50 60 M 10 20 30 40 50 60",
456        use_implicit_lineto_commands
457    );
458
459    #[test]
460    fn write_18() {
461        let path = Path::from_str("M 10 20 L 30 40 L 50 60 M 10 20 L 30 40 L 50 60").unwrap();
462
463        let mut opt = WriteOptions::default();
464        opt.use_implicit_lineto_commands = true;
465        opt.remove_duplicated_path_commands = true;
466
467        assert_eq!(
468            path.with_write_opt(&opt).to_string(),
469            "M 10 20 30 40 50 60 M 10 20 30 40 50 60"
470        );
471    }
472
473    #[test]
474    fn write_19() {
475        let path = Path::from_str("m10 20 A 10 10 0 1 0 0 0 A 2 2 0 1 0 2 0").unwrap();
476
477        let mut opt = WriteOptions::default();
478        opt.use_compact_path_notation = true;
479        opt.remove_duplicated_path_commands = true;
480        opt.remove_leading_zero = true;
481
482        // may generate as 'm10 20A10 10 0 1 0 0 0 2 2 0 1 0  2 0' <- two spaces
483
484        assert_eq!(
485            path.with_write_opt(&opt).to_string(),
486            "m10 20A10 10 0 1 0 0 0 2 2 0 1 0 2 0"
487        );
488    }
489
490    #[test]
491    fn write_20() {
492        let path = Path::from_str("M 0.1 0.1 L 1 0.1 2 -0.1").unwrap();
493
494        let mut opt = WriteOptions::default();
495        opt.use_compact_path_notation = true;
496        opt.remove_duplicated_path_commands = true;
497        opt.remove_leading_zero = true;
498
499        assert_eq!(path.with_write_opt(&opt).to_string(), "M.1.1L1 .1 2-.1");
500    }
501
502    test_write_opt!(
503        write_21,
504        "M 10 20 M 30 40 M 50 60 L 30 40",
505        "M 10 20 M 30 40 M 50 60 L 30 40",
506        remove_duplicated_path_commands
507    );
508}