svg_path_parser/
lib.rs

1//! Simple SVG path parsing.
2//! Parses a SVG path string (the thing that goes in 'd') into sets of points representing continuous lines.
3//! 
4//! ## Usage
5//! ```rust
6//! let paths = svg_path_parser::parse(&path).collect::<Vec<(bool, Vec<(f64, f64)>)>>();
7//! ```
8//! 
9//! Each `(bool, Vec<(f64, f64)>)` represents a continuous line, 
10//! with the `bool` indicating if the line is closed.
11//! A closed line has the ending point connect to the starting point.
12
13#![warn(missing_docs)]
14
15use std::iter::Peekable;
16use std::str::Chars;
17
18mod curves;
19mod elements;
20mod utils;
21
22use elements::{ PathElementCommand, PathElementLabel, PreviousElementCommand };
23
24/// Parse a SVG path string.
25/// 
26/// Returns an iterator over 
27pub fn parse<'a>(path:&'a str) -> PathParser<'a> {
28    PathParser::new(path, 64)
29}
30
31/// Parse a SVG path string with custom resolution.
32/// 
33/// Refer to 
34/// Resolution refers to how many line segments a curve is broken down into.
35/// Higher numbers give smoother curves, but also more points to work with, which depending on what you're planning further down may not be worth it.
36/// The default resolution is 64.
37/// 
38/// # Usage
39/// ```rust
40///     let paths = svg_path_parser::parse(&path).collect::<Vec<(bool, Vec<(f64, f64)>)>>();
41/// ```
42pub fn parse_with_resolution<'a>(path:&'a str, resolution:u64) -> PathParser<'a> {
43    PathParser::new(path, resolution)
44}
45
46/// Iterator over continuous lines.
47pub struct PathParser<'a> {
48    data: Peekable<Chars<'a>>,
49    current_command: Option<PathElementCommand>,
50    previous_command: Option<PreviousElementCommand>,
51    cursor: (f64, f64),
52    paths: Vec<Vec<(f64, f64)>>,
53    hard_ended: bool,
54    resolution: u64,
55}
56impl<'a> PathParser<'a> {
57    fn new(data:&'a str, resolution:u64) -> Self {
58        Self {
59            data: data.chars().peekable(),
60            current_command: None,
61            previous_command: None,
62            cursor: (0.0, 0.0),
63            paths: Vec::new(),
64            hard_ended: false,
65            resolution,
66        }
67    }
68
69    // Keep advancing until the next character is not a separator
70    fn discard_separators(&mut self) {
71        while let Some(&ch) = self.data.peek() {
72            if !utils::is_separator(ch) { // I can't put it up there :(
73                break;
74            }
75
76            let _ = self.data.next();
77        }
78    }
79
80    fn get_float(&mut self) -> Option<f64> {
81        self.discard_separators();
82
83        let mut s = String::new();
84        let mut decimal_count = 0;
85        while let Some(&ch) = self.data.peek() {
86            if ch == '+' || ch == '-' && s.is_empty() {
87                s.push(self.data.next()?);
88            } else if ch == '.' && decimal_count == 0 {
89                s.push(self.data.next()?);
90                decimal_count = 1;  
91            } else if ch.is_digit(10) {
92                s.push(self.data.next()?);
93            } else {
94                break;
95            }
96        }
97        
98        s.parse::<f64>().ok()
99    }
100
101    fn get_point(&mut self, relative:bool) -> Option<(f64, f64)> {
102        let x = self.get_float()?;
103        let y = self.get_float()?;
104
105        Some(match relative {
106            true => utils::add_point(self.cursor, (x, y)),
107            false => (x, y),
108        })
109    }
110
111    fn get_bool(&mut self) -> Option<bool> {
112        let n = self.get_float()?; // I can't use floats in match arms
113        if n == 1.0 {
114            Some(true)
115        } else if n == 0.0 {
116            Some(false)
117        } else {
118            None
119        }
120    }
121
122    // Returns: (ended, result)
123    fn get_path(&mut self) -> Option<(bool, Vec<(f64, f64)>)> {
124        let mut ended = false;
125        let mut hard_ended = false;
126        while !ended && !hard_ended {
127            (ended, hard_ended) = match self.advance() {
128                Some(ended) => (ended, false),
129                None => (false, true),
130            };
131        }
132
133        self.hard_ended = hard_ended;
134        self.paths.pop().map(|paths| (ended, paths))
135    }
136
137    fn advance(&mut self) -> Option<bool> {
138        self.discard_separators();
139        let elem = match utils::is_number_part(*self.data.peek()?) {
140            true => self.current_command?.updated(),
141            false => PathElementCommand::from_ch(self.data.next()?)?,
142        };
143
144        // Update memory
145        self.current_command = Some(elem);
146
147        // Parse command
148        let command = match elem.label() {
149            PathElementLabel::Move => self.handle_move(elem.relative()),
150            PathElementLabel::Line => self.handle_line(elem.relative()),
151            PathElementLabel::Horizontal => self.handle_horizontal(elem.relative()),
152            PathElementLabel::Vertical => self.handle_vertical(elem.relative()),
153            PathElementLabel::CubicBezier => self.handle_cubic_bezier(elem.relative()),
154            PathElementLabel::SmoothCubicBezier => self.handle_smooth_cubic_bezier(elem.relative()),
155            PathElementLabel::QuadraticBezier => self.handle_quadratic_bezier(elem.relative()),
156            PathElementLabel::SmoothQuadraticBezier => self.handle_smooth_quadratic_bezier(elem.relative()),
157            PathElementLabel::Arc => self.handle_arc(elem.relative()),
158            PathElementLabel::End => self.handle_end(),
159        }?;
160
161        self.previous_command = Some(command);
162        Some(self.previous_command == Some(PreviousElementCommand::End)) // Avoid more allocations
163    }
164
165    fn handle_move(&mut self, relative:bool) -> Option<PreviousElementCommand> {
166        self.cursor = self.get_point(relative)?;
167        self.paths.push(vec![self.cursor]);
168
169        Some(PreviousElementCommand::NotCurve)
170    }
171
172    fn update_paths(&mut self, end:(f64, f64)) {
173        // Make sure there's an active path
174        if self.paths.len() == 0 {
175            self.paths.push(vec![self.cursor]);
176        }
177
178        // Make sure that the last point is pointing to the cursor
179        let n = self.paths.len() - 1;
180        let n2 = self.paths[n].len();
181        if n2 > 0 && self.paths[n][n2 - 1] != self.cursor {
182            self.paths[n].push(self.cursor);
183        }
184
185        // Update cursor
186        self.cursor = end;
187    }
188
189    fn insert_points(&mut self, mut points:Vec<(f64, f64)>) {
190        let end = match points.len() {
191            0 => self.cursor,
192            _ => points[points.len() - 1],
193        };
194
195        self.update_paths(end);
196        let n = self.paths.len() - 1;
197
198        self.paths[n].append(&mut points);        
199        // Don't return anything as these are used with curves
200    }
201
202    fn insert_line(&mut self, end:(f64, f64)) -> Option<PreviousElementCommand> {
203        self.update_paths(end);
204
205        let n = self.paths.len() - 1;
206        self.paths[n].push(end);
207
208        Some(PreviousElementCommand::NotCurve)
209    }
210
211    fn handle_line(&mut self, relative:bool) -> Option<PreviousElementCommand> {
212        let end = self.get_point(relative)?;
213        self.insert_line(end)
214    }
215
216    fn handle_horizontal(&mut self, relative:bool) -> Option<PreviousElementCommand> {
217        let y = self.cursor.1;
218        let x = match relative {
219            true => self.cursor.0 + self.get_float()?,
220            false => self.get_float()?,
221        };
222
223        self.insert_line((x, y))
224    }
225
226    fn handle_vertical(&mut self, relative:bool) -> Option<PreviousElementCommand> {
227        let x = self.cursor.0;
228        let y = match relative {
229            true => self.cursor.1 + self.get_float()?,
230            false => self.get_float()?,
231        };
232
233        self.insert_line((x, y))
234    }
235
236    fn handle_end(&mut self) -> Option<PreviousElementCommand> {
237        if self.paths.len() > 0 && self.paths[0].len() > 0 {
238            self.cursor = self.paths[0][0];
239        }
240
241        Some(PreviousElementCommand::End)
242    }
243
244    fn handle_cubic_bezier(&mut self, relative:bool) -> Option<PreviousElementCommand> {
245        let p1 = self.get_point(relative)?;
246        let p2 = self.get_point(relative)?;
247        let end = self.get_point(relative)?;
248
249        let points = curves::compute_cubic_bezier(self.cursor, p1, p2, end, self.resolution);
250        self.insert_points(points);
251
252        Some(PreviousElementCommand::CubicBezier(p2))
253    }
254
255    fn handle_smooth_cubic_bezier(&mut self, relative:bool) -> Option<PreviousElementCommand> {
256        let p2 = self.get_point(relative)?;
257        let end = self.get_point(relative)?;
258
259        let p1 = match self.previous_command {
260            Some(PreviousElementCommand::CubicBezier(p1)) => utils::reflect_point(p1, self.cursor),
261            _ => self.cursor,
262        };
263
264        let points = curves::compute_cubic_bezier(self.cursor, p1, p2, end, self.resolution);
265        self.insert_points(points);
266
267        Some(PreviousElementCommand::CubicBezier(p2))
268    }
269
270    fn handle_quadratic_bezier(&mut self, relative:bool) -> Option<PreviousElementCommand> {
271        let p1 = self.get_point(relative)?;
272        let end = self.get_point(relative)?;
273
274        let points = curves::compute_quadratic_bezier(self.cursor, p1, end, self.resolution);
275        self.insert_points(points);
276
277        Some(PreviousElementCommand::QuadraticBezier(p1))
278    }
279
280    fn handle_smooth_quadratic_bezier(&mut self, relative:bool) -> Option<PreviousElementCommand> {
281        let end = self.get_point(relative)?;
282        let p1 = match self.previous_command {
283            Some(PreviousElementCommand::QuadraticBezier(p1)) => utils::reflect_point(p1, self.cursor),
284            _ => self.cursor,
285        };
286
287        let points = curves::compute_quadratic_bezier(self.cursor, p1, end, self.resolution);
288        self.insert_points(points);
289
290        Some(PreviousElementCommand::QuadraticBezier(p1))
291    }
292
293    fn handle_arc(&mut self, relative:bool) -> Option<PreviousElementCommand> {
294        let r = self.get_point(relative)?;
295        let rotation = self.get_float()?;
296        let large = self.get_bool()?;
297        let sweep = self.get_bool()?;
298        let end = self.get_point(relative)?;
299
300        let points = curves::compute_arc(self.cursor, r, rotation, large, sweep, end, self.resolution);
301        self.insert_points(points);
302
303        Some(PreviousElementCommand::NotCurve)
304    }
305}
306
307impl Iterator for PathParser<'_> {
308    type Item = (bool, Vec<(f64, f64)>);
309    // The default SVG behaviour is to ignore everything after an error is encountered
310    fn next(&mut self) -> Option<Self::Item> {
311        if self.hard_ended {
312            self.paths.pop().map(|paths| (false, paths))
313        } else {
314            self.get_path()
315        }
316    }
317}