kurbo/
svg.rs

1// Copyright 2018 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! SVG path representation.
5
6use alloc::vec::Vec;
7use core::error::Error;
8use core::f64::consts::PI;
9use core::fmt::{self, Display, Formatter};
10#[cfg(feature = "std")]
11use std::io::{self, Write};
12
13use crate::{Arc, BezPath, ParamCurve, PathEl, PathSeg, Point, Vec2};
14
15#[cfg(not(feature = "std"))]
16use crate::common::FloatFuncs;
17
18// Note: the SVG arc logic is heavily adapted from https://github.com/nical/lyon
19
20/// A single SVG arc segment.
21#[derive(Clone, Copy, Debug)]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct SvgArc {
25    /// The arc's start point.
26    pub from: Point,
27    /// The arc's end point.
28    pub to: Point,
29    /// The arc's radii, where the vector's x-component is the radius in the
30    /// positive x direction after applying `x_rotation`.
31    pub radii: Vec2,
32    /// How much the arc is rotated, in radians.
33    pub x_rotation: f64,
34    /// Does this arc sweep through more than π radians?
35    pub large_arc: bool,
36    /// Determines if the arc should begin moving at positive angles.
37    pub sweep: bool,
38}
39
40impl BezPath {
41    /// Create a `BezPath` with segments corresponding to the sequence of
42    /// `PathSeg`s
43    pub fn from_path_segments(segments: impl Iterator<Item = PathSeg>) -> BezPath {
44        let mut path_elements = Vec::new();
45        let mut current_pos = None;
46
47        for segment in segments {
48            let start = segment.start();
49            if Some(start) != current_pos {
50                path_elements.push(PathEl::MoveTo(start));
51            }
52            path_elements.push(match segment {
53                PathSeg::Line(l) => PathEl::LineTo(l.p1),
54                PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2),
55                PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3),
56            });
57
58            current_pos = Some(segment.end());
59        }
60
61        BezPath::from_vec(path_elements)
62    }
63
64    /// Convert the path to an SVG path string representation.
65    ///
66    /// The current implementation doesn't take any special care to produce a
67    /// short string (reducing precision, using relative movement).
68    #[cfg(feature = "std")]
69    pub fn to_svg(&self) -> String {
70        let mut buffer = Vec::new();
71        self.write_to(&mut buffer).unwrap();
72        String::from_utf8(buffer).unwrap()
73    }
74
75    /// Write the SVG representation of this path to the provided buffer.
76    #[cfg(feature = "std")]
77    pub fn write_to<W: Write>(&self, mut writer: W) -> io::Result<()> {
78        for (i, el) in self.elements().iter().enumerate() {
79            if i > 0 {
80                write!(writer, " ")?;
81            }
82            match *el {
83                PathEl::MoveTo(p) => write!(writer, "M{},{}", p.x, p.y)?,
84                PathEl::LineTo(p) => write!(writer, "L{},{}", p.x, p.y)?,
85                PathEl::QuadTo(p1, p2) => write!(writer, "Q{},{} {},{}", p1.x, p1.y, p2.x, p2.y)?,
86                PathEl::CurveTo(p1, p2, p3) => write!(
87                    writer,
88                    "C{},{} {},{} {},{}",
89                    p1.x, p1.y, p2.x, p2.y, p3.x, p3.y
90                )?,
91                PathEl::ClosePath => write!(writer, "Z")?,
92            }
93        }
94
95        Ok(())
96    }
97
98    /// Try to parse a bezier path from an SVG path element.
99    ///
100    /// This is implemented on a best-effort basis, intended for cases where the
101    /// user controls the source of paths, and is not intended as a replacement
102    /// for a general, robust SVG parser.
103    pub fn from_svg(data: &str) -> Result<BezPath, SvgParseError> {
104        let mut lexer = SvgLexer::new(data);
105        let mut path = BezPath::new();
106        let mut last_cmd = 0;
107        let mut last_ctrl = None;
108        let mut first_pt = Point::ORIGIN;
109        let mut implicit_moveto = None;
110        while let Some(c) = lexer.get_cmd(last_cmd) {
111            if c != b'm' && c != b'M' {
112                if path.elements().is_empty() {
113                    return Err(SvgParseError::UninitializedPath);
114                }
115
116                if let Some(pt) = implicit_moveto.take() {
117                    path.move_to(pt);
118                }
119            }
120            match c {
121                b'm' | b'M' => {
122                    implicit_moveto = None;
123                    let pt = lexer.get_maybe_relative(c)?;
124                    path.move_to(pt);
125                    lexer.last_pt = pt;
126                    first_pt = pt;
127                    last_ctrl = Some(pt);
128                    last_cmd = c - (b'M' - b'L');
129                }
130                b'l' | b'L' => {
131                    let pt = lexer.get_maybe_relative(c)?;
132                    path.line_to(pt);
133                    lexer.last_pt = pt;
134                    last_ctrl = Some(pt);
135                    last_cmd = c;
136                }
137                b'h' | b'H' => {
138                    let mut x = lexer.get_number()?;
139                    lexer.opt_comma();
140                    if c == b'h' {
141                        x += lexer.last_pt.x;
142                    }
143                    let pt = Point::new(x, lexer.last_pt.y);
144                    path.line_to(pt);
145                    lexer.last_pt = pt;
146                    last_ctrl = Some(pt);
147                    last_cmd = c;
148                }
149                b'v' | b'V' => {
150                    let mut y = lexer.get_number()?;
151                    lexer.opt_comma();
152                    if c == b'v' {
153                        y += lexer.last_pt.y;
154                    }
155                    let pt = Point::new(lexer.last_pt.x, y);
156                    path.line_to(pt);
157                    lexer.last_pt = pt;
158                    last_ctrl = Some(pt);
159                    last_cmd = c;
160                }
161                b'q' | b'Q' => {
162                    let p1 = lexer.get_maybe_relative(c)?;
163                    let p2 = lexer.get_maybe_relative(c)?;
164                    path.quad_to(p1, p2);
165                    last_ctrl = Some(p1);
166                    lexer.last_pt = p2;
167                    last_cmd = c;
168                }
169                b't' | b'T' => {
170                    let p1 = match last_ctrl {
171                        Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(),
172                        None => lexer.last_pt,
173                    };
174                    let p2 = lexer.get_maybe_relative(c)?;
175                    path.quad_to(p1, p2);
176                    last_ctrl = Some(p1);
177                    lexer.last_pt = p2;
178                    last_cmd = c;
179                }
180                b'c' | b'C' => {
181                    let p1 = lexer.get_maybe_relative(c)?;
182                    let p2 = lexer.get_maybe_relative(c)?;
183                    let p3 = lexer.get_maybe_relative(c)?;
184                    path.curve_to(p1, p2, p3);
185                    last_ctrl = Some(p2);
186                    lexer.last_pt = p3;
187                    last_cmd = c;
188                }
189                b's' | b'S' => {
190                    let p1 = match last_ctrl {
191                        Some(ctrl) => (2.0 * lexer.last_pt.to_vec2() - ctrl.to_vec2()).to_point(),
192                        None => lexer.last_pt,
193                    };
194                    let p2 = lexer.get_maybe_relative(c)?;
195                    let p3 = lexer.get_maybe_relative(c)?;
196                    path.curve_to(p1, p2, p3);
197                    last_ctrl = Some(p2);
198                    lexer.last_pt = p3;
199                    last_cmd = c;
200                }
201                b'a' | b'A' => {
202                    let radii = lexer.get_number_pair()?;
203                    let x_rotation = lexer.get_number()?.to_radians();
204                    lexer.opt_comma();
205                    let large_arc = lexer.get_flag()?;
206                    lexer.opt_comma();
207                    let sweep = lexer.get_flag()?;
208                    lexer.opt_comma();
209                    let p = lexer.get_maybe_relative(c)?;
210                    let svg_arc = SvgArc {
211                        from: lexer.last_pt,
212                        to: p,
213                        radii: radii.to_vec2(),
214                        x_rotation,
215                        large_arc,
216                        sweep,
217                    };
218
219                    match Arc::from_svg_arc(&svg_arc) {
220                        Some(arc) => {
221                            // TODO: consider making tolerance configurable
222                            arc.to_cubic_beziers(0.1, |p1, p2, p3| {
223                                path.curve_to(p1, p2, p3);
224                            });
225                        }
226                        None => {
227                            path.line_to(p);
228                        }
229                    }
230
231                    last_ctrl = Some(p);
232                    lexer.last_pt = p;
233                    last_cmd = c;
234                }
235                b'z' | b'Z' => {
236                    path.close_path();
237                    lexer.last_pt = first_pt;
238                    implicit_moveto = Some(first_pt);
239                }
240                _ => return Err(SvgParseError::UnknownCommand(c as char)),
241            }
242        }
243        Ok(path)
244    }
245}
246
247/// An error which can be returned when parsing an SVG.
248#[derive(Debug)]
249#[non_exhaustive]
250pub enum SvgParseError {
251    /// A number was expected.
252    Wrong,
253    /// The input string ended while still expecting input.
254    UnexpectedEof,
255    /// Encountered an unknown command letter.
256    UnknownCommand(char),
257    /// Encountered a command that precedes expected 'moveto' command.
258    UninitializedPath,
259}
260
261impl Display for SvgParseError {
262    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
263        match self {
264            SvgParseError::Wrong => write!(f, "Unable to parse a number"),
265            SvgParseError::UnexpectedEof => write!(f, "Unexpected EOF"),
266            SvgParseError::UnknownCommand(letter) => write!(f, "Unknown command, \"{letter}\""),
267            SvgParseError::UninitializedPath => {
268                write!(f, "Uninitialized path (missing moveto command)")
269            }
270        }
271    }
272}
273
274impl Error for SvgParseError {}
275
276struct SvgLexer<'a> {
277    data: &'a str,
278    ix: usize,
279    pub last_pt: Point,
280}
281
282impl SvgLexer<'_> {
283    fn new(data: &str) -> SvgLexer<'_> {
284        SvgLexer {
285            data,
286            ix: 0,
287            last_pt: Point::ORIGIN,
288        }
289    }
290
291    fn skip_ws(&mut self) {
292        while let Some(&c) = self.data.as_bytes().get(self.ix) {
293            if !(c == b' ' || c == 9 || c == 10 || c == 12 || c == 13) {
294                break;
295            }
296            self.ix += 1;
297        }
298    }
299
300    fn get_cmd(&mut self, last_cmd: u8) -> Option<u8> {
301        self.skip_ws();
302        if let Some(c) = self.get_byte() {
303            if c.is_ascii_lowercase() || c.is_ascii_uppercase() {
304                return Some(c);
305            } else if last_cmd != 0 && (c == b'-' || c == b'.' || c.is_ascii_digit()) {
306                // Plausible number start
307                self.unget();
308                return Some(last_cmd);
309            } else {
310                self.unget();
311            }
312        }
313        None
314    }
315
316    fn get_byte(&mut self) -> Option<u8> {
317        self.data.as_bytes().get(self.ix).map(|&c| {
318            self.ix += 1;
319            c
320        })
321    }
322
323    fn unget(&mut self) {
324        self.ix -= 1;
325    }
326
327    fn get_number(&mut self) -> Result<f64, SvgParseError> {
328        self.skip_ws();
329        let start = self.ix;
330        let c = self.get_byte().ok_or(SvgParseError::UnexpectedEof)?;
331        if !(c == b'-' || c == b'+') {
332            self.unget();
333        }
334        let mut digit_count = 0;
335        let mut seen_period = false;
336        while let Some(c) = self.get_byte() {
337            if c.is_ascii_digit() {
338                digit_count += 1;
339            } else if c == b'.' && !seen_period {
340                seen_period = true;
341            } else {
342                self.unget();
343                break;
344            }
345        }
346        if let Some(c) = self.get_byte() {
347            if c == b'e' || c == b'E' {
348                let mut c = self.get_byte().ok_or(SvgParseError::Wrong)?;
349                if c == b'-' || c == b'+' {
350                    c = self.get_byte().ok_or(SvgParseError::Wrong)?;
351                }
352                if !c.is_ascii_digit() {
353                    return Err(SvgParseError::Wrong);
354                }
355                while let Some(c) = self.get_byte() {
356                    if !c.is_ascii_digit() {
357                        self.unget();
358                        break;
359                    }
360                }
361            } else {
362                self.unget();
363            }
364        }
365        if digit_count > 0 {
366            self.data[start..self.ix]
367                .parse()
368                .map_err(|_| SvgParseError::Wrong)
369        } else {
370            Err(SvgParseError::Wrong)
371        }
372    }
373
374    fn get_flag(&mut self) -> Result<bool, SvgParseError> {
375        self.skip_ws();
376        match self.get_byte().ok_or(SvgParseError::UnexpectedEof)? {
377            b'0' => Ok(false),
378            b'1' => Ok(true),
379            _ => Err(SvgParseError::Wrong),
380        }
381    }
382
383    fn get_number_pair(&mut self) -> Result<Point, SvgParseError> {
384        let x = self.get_number()?;
385        self.opt_comma();
386        let y = self.get_number()?;
387        self.opt_comma();
388        Ok(Point::new(x, y))
389    }
390
391    fn get_maybe_relative(&mut self, cmd: u8) -> Result<Point, SvgParseError> {
392        let pt = self.get_number_pair()?;
393        if cmd.is_ascii_lowercase() {
394            Ok(self.last_pt + pt.to_vec2())
395        } else {
396            Ok(pt)
397        }
398    }
399
400    fn opt_comma(&mut self) {
401        self.skip_ws();
402        if let Some(c) = self.get_byte() {
403            if c != b',' {
404                self.unget();
405            }
406        }
407    }
408}
409
410impl SvgArc {
411    /// Checks that arc is actually a straight line.
412    ///
413    /// In this case, it can be replaced with a `LineTo`.
414    pub fn is_straight_line(&self) -> bool {
415        self.radii.x.abs() <= 1e-5 || self.radii.y.abs() <= 1e-5 || self.from == self.to
416    }
417}
418
419impl Arc {
420    /// Creates an `Arc` from a `SvgArc`.
421    ///
422    /// Returns `None` if `arc` is actually a straight line.
423    pub fn from_svg_arc(arc: &SvgArc) -> Option<Arc> {
424        // Have to check this first, otherwise `sum_of_sq` will be 0.
425        if arc.is_straight_line() {
426            return None;
427        }
428
429        let mut rx = arc.radii.x.abs();
430        let mut ry = arc.radii.y.abs();
431
432        let xr = arc.x_rotation % (2.0 * PI);
433        let (sin_phi, cos_phi) = xr.sin_cos();
434        let hd_x = (arc.from.x - arc.to.x) * 0.5;
435        let hd_y = (arc.from.y - arc.to.y) * 0.5;
436        let hs_x = (arc.from.x + arc.to.x) * 0.5;
437        let hs_y = (arc.from.y + arc.to.y) * 0.5;
438
439        // F6.5.1
440        let p = Vec2::new(
441            cos_phi * hd_x + sin_phi * hd_y,
442            -sin_phi * hd_x + cos_phi * hd_y,
443        );
444
445        // Sanitize the radii.
446        // If rf > 1 it means the radii are too small for the arc to
447        // possibly connect the end points. In this situation we scale
448        // them up according to the formula provided by the SVG spec.
449
450        // F6.6.2
451        let rf = p.x * p.x / (rx * rx) + p.y * p.y / (ry * ry);
452        if rf > 1.0 {
453            let scale = rf.sqrt();
454            rx *= scale;
455            ry *= scale;
456        }
457
458        let rxry = rx * ry;
459        let rxpy = rx * p.y;
460        let rypx = ry * p.x;
461        let sum_of_sq = rxpy * rxpy + rypx * rypx;
462
463        debug_assert!(sum_of_sq != 0.0);
464
465        // F6.5.2
466        let sign_coe = if arc.large_arc == arc.sweep {
467            -1.0
468        } else {
469            1.0
470        };
471        let coe = sign_coe * ((rxry * rxry - sum_of_sq) / sum_of_sq).abs().sqrt();
472        let transformed_cx = coe * rxpy / ry;
473        let transformed_cy = -coe * rypx / rx;
474
475        // F6.5.3
476        let center = Point::new(
477            cos_phi * transformed_cx - sin_phi * transformed_cy + hs_x,
478            sin_phi * transformed_cx + cos_phi * transformed_cy + hs_y,
479        );
480
481        let start_v = Vec2::new((p.x - transformed_cx) / rx, (p.y - transformed_cy) / ry);
482        let end_v = Vec2::new((-p.x - transformed_cx) / rx, (-p.y - transformed_cy) / ry);
483
484        let start_angle = start_v.atan2();
485
486        let mut sweep_angle = (end_v.atan2() - start_angle) % (2.0 * PI);
487
488        if arc.sweep && sweep_angle < 0.0 {
489            sweep_angle += 2.0 * PI;
490        } else if !arc.sweep && sweep_angle > 0.0 {
491            sweep_angle -= 2.0 * PI;
492        }
493
494        Some(Arc {
495            center,
496            radii: Vec2::new(rx, ry),
497            start_angle,
498            sweep_angle,
499            x_rotation: arc.x_rotation,
500        })
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use crate::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez, Shape};
507
508    #[test]
509    fn test_parse_svg() {
510        let path = BezPath::from_svg("m10 10 100 0 0 100 -100 0z").unwrap();
511        assert_eq!(path.segments().count(), 4);
512    }
513
514    #[test]
515    fn test_parse_svg2() {
516        let path =
517            BezPath::from_svg("M3.5 8a.5.5 0 01.5-.5h8a.5.5 0 010 1H4a.5.5 0 01-.5-.5z").unwrap();
518        assert_eq!(path.segments().count(), 6);
519    }
520
521    #[test]
522    fn test_parse_svg_arc() {
523        let path = BezPath::from_svg("M 100 100 A 25 25 0 1 0 -25 25 z").unwrap();
524        assert_eq!(path.segments().count(), 3);
525    }
526
527    // Regression test for #51
528    #[test]
529    #[allow(clippy::float_cmp)]
530    fn test_parse_svg_arc_pie() {
531        let path = BezPath::from_svg("M 100 100 h 25 a 25 25 0 1 0 -25 25 z").unwrap();
532        // Approximate figures, but useful for regression testing
533        assert_eq!(path.area().round(), -1473.0);
534        assert_eq!(path.perimeter(1e-6).round(), 168.0);
535    }
536
537    #[test]
538    fn test_parse_svg_uninitialized() {
539        let path = BezPath::from_svg("L10 10 100 0 0 100");
540        assert!(path.is_err());
541    }
542
543    #[test]
544    #[allow(clippy::float_cmp)]
545    fn test_parse_scientific_notation() {
546        let path = BezPath::from_svg("M 0 0 L 1e-123 -4E+5").unwrap();
547        assert_eq!(
548            path.elements(),
549            &[
550                PathEl::MoveTo(Point { x: 0.0, y: 0.0 }),
551                PathEl::LineTo(Point {
552                    x: 1e-123,
553                    y: -4E+5
554                })
555            ]
556        );
557    }
558
559    #[test]
560    fn test_write_svg_single() {
561        let segments = [CubicBez::new(
562            Point::new(10., 10.),
563            Point::new(20., 20.),
564            Point::new(30., 30.),
565            Point::new(40., 40.),
566        )
567        .into()];
568        let path = BezPath::from_path_segments(segments.iter().cloned());
569
570        assert_eq!(path.to_svg(), "M10,10 C20,20 30,30 40,40");
571    }
572
573    #[test]
574    fn test_write_svg_two_nomove() {
575        let segments = [
576            CubicBez::new(
577                Point::new(10., 10.),
578                Point::new(20., 20.),
579                Point::new(30., 30.),
580                Point::new(40., 40.),
581            )
582            .into(),
583            CubicBez::new(
584                Point::new(40., 40.),
585                Point::new(30., 30.),
586                Point::new(20., 20.),
587                Point::new(10., 10.),
588            )
589            .into(),
590        ];
591        let path = BezPath::from_path_segments(segments.iter().cloned());
592
593        assert_eq!(
594            path.to_svg(),
595            "M10,10 C20,20 30,30 40,40 C30,30 20,20 10,10"
596        );
597    }
598
599    #[test]
600    fn test_write_svg_two_move() {
601        let segments = [
602            CubicBez::new(
603                Point::new(10., 10.),
604                Point::new(20., 20.),
605                Point::new(30., 30.),
606                Point::new(40., 40.),
607            )
608            .into(),
609            CubicBez::new(
610                Point::new(50., 50.),
611                Point::new(30., 30.),
612                Point::new(20., 20.),
613                Point::new(10., 10.),
614            )
615            .into(),
616        ];
617        let path = BezPath::from_path_segments(segments.iter().cloned());
618
619        assert_eq!(
620            path.to_svg(),
621            "M10,10 C20,20 30,30 40,40 M50,50 C30,30 20,20 10,10"
622        );
623    }
624
625    use rand::prelude::*;
626    // Suppress the unused_crate_dependencies lint for getrandom.
627    #[cfg(all(
628        target_arch = "wasm32",
629        target_vendor = "unknown",
630        target_os = "unknown"
631    ))]
632    use getrandom as _;
633
634    fn gen_random_path_sequence(rng: &mut impl Rng) -> Vec<PathSeg> {
635        const MAX_LENGTH: u32 = 10;
636
637        let mut elements = vec![];
638        let mut position = None;
639
640        let length = rng.random_range(0..MAX_LENGTH);
641        for _ in 0..length {
642            let should_follow: bool = rand::random_bool(0.5);
643            let kind = rng.random_range(0..3);
644
645            let first = position
646                .filter(|_| should_follow)
647                .unwrap_or_else(|| Point::new(rng.random(), rng.random()));
648
649            let element: PathSeg = match kind {
650                0 => Line::new(first, Point::new(rng.random(), rng.random())).into(),
651
652                1 => QuadBez::new(
653                    first,
654                    Point::new(rng.random(), rng.random()),
655                    Point::new(rng.random(), rng.random()),
656                )
657                .into(),
658
659                2 => CubicBez::new(
660                    first,
661                    Point::new(rng.random(), rng.random()),
662                    Point::new(rng.random(), rng.random()),
663                    Point::new(rng.random(), rng.random()),
664                )
665                .into(),
666
667                _ => unreachable!(),
668            };
669
670            position = Some(element.end());
671            elements.push(element);
672        }
673
674        elements
675    }
676
677    #[test]
678    fn test_serialize_deserialize() {
679        const N_TESTS: u32 = 100;
680        let mut rng = rand::rng();
681
682        for _ in 0..N_TESTS {
683            let vec = gen_random_path_sequence(&mut rng);
684            let ser = BezPath::from_path_segments(vec.iter().cloned()).to_svg();
685            let deser = BezPath::from_svg(&ser).expect("failed deserialization");
686
687            let deser_vec = deser.segments().collect::<Vec<PathSeg>>();
688
689            assert_eq!(vec, deser_vec);
690        }
691    }
692}