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