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