1use std::f64::consts::PI;
43use std::fmt;
44
45pub type Contour = Vec<[f64; 2]>;
47
48const ARC_STEP: f64 = PI / 24.0;
50const CUBIC_STEPS: usize = 16;
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum IconPathError {
56 UnsupportedCommand(char),
58 MissingArgument(char),
60 BadNumber(String),
62 NoCurrentPoint,
64 UnexpectedNumber,
69}
70
71impl fmt::Display for IconPathError {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 IconPathError::UnsupportedCommand(c) => {
75 write!(f, "unsupported SVG path command '{c}'")
76 }
77 IconPathError::MissingArgument(c) => {
78 write!(f, "SVG path command '{c}' is missing arguments")
79 }
80 IconPathError::BadNumber(s) => write!(f, "'{s}' is not a number"),
81 IconPathError::NoCurrentPoint => {
82 write!(
83 f,
84 "SVG path starts with a drawing command before any moveto"
85 )
86 }
87 IconPathError::UnexpectedNumber => {
88 write!(
89 f,
90 "SVG path has a number after a closepath, which takes no arguments"
91 )
92 }
93 }
94 }
95}
96
97impl std::error::Error for IconPathError {}
98
99pub fn parse_path(d: &str) -> Result<Vec<Contour>, IconPathError> {
105 let mut scan = Scanner::new(d);
106 let mut contours: Vec<Contour> = Vec::new();
107 let mut current: Contour = Vec::new();
108 let mut pos = [0.0f64, 0.0];
109 let mut start = [0.0f64, 0.0];
110 let mut cmd: Option<char> = None;
111
112 loop {
113 scan.skip_separators();
114 let next = match scan.peek() {
115 None => break,
116 Some(c) if c.is_ascii_alphabetic() => {
117 scan.bump();
118 cmd = Some(c);
119 c
120 }
121 Some(_) => match cmd {
124 Some('M') => 'L',
125 Some('m') => 'l',
126 Some('Z') | Some('z') => return Err(IconPathError::UnexpectedNumber),
130 Some(c) => c,
131 None => return Err(IconPathError::NoCurrentPoint),
132 },
133 };
134
135 match next {
136 'M' | 'm' => {
137 if current.len() > 1 {
138 contours.push(std::mem::take(&mut current));
139 } else {
140 current.clear();
141 }
142 let x = scan.number(next)?;
143 let y = scan.number(next)?;
144 pos = if next == 'M' {
145 [x, y]
146 } else {
147 [pos[0] + x, pos[1] + y]
148 };
149 start = pos;
150 current.push(pos);
151 }
152 'L' | 'l' => {
153 let x = scan.number(next)?;
154 let y = scan.number(next)?;
155 pos = if next == 'L' {
156 [x, y]
157 } else {
158 [pos[0] + x, pos[1] + y]
159 };
160 push_point(&mut current, pos)?;
161 }
162 'H' | 'h' => {
163 let x = scan.number(next)?;
164 pos = [if next == 'H' { x } else { pos[0] + x }, pos[1]];
165 push_point(&mut current, pos)?;
166 }
167 'V' | 'v' => {
168 let y = scan.number(next)?;
169 pos = [pos[0], if next == 'V' { y } else { pos[1] + y }];
170 push_point(&mut current, pos)?;
171 }
172 'C' | 'c' => {
173 let mut p = [[0.0f64; 2]; 3];
174 for slot in p.iter_mut() {
175 let x = scan.number(next)?;
176 let y = scan.number(next)?;
177 *slot = if next == 'C' {
178 [x, y]
179 } else {
180 [pos[0] + x, pos[1] + y]
181 };
182 }
183 if current.is_empty() {
184 return Err(IconPathError::NoCurrentPoint);
185 }
186 flatten_cubic(&mut current, pos, p[0], p[1], p[2]);
187 pos = p[2];
188 }
189 'A' | 'a' => {
190 let rx = scan.number(next)?;
191 let ry = scan.number(next)?;
192 let rot = scan.number(next)?;
193 let large = scan.number(next)? != 0.0;
194 let sweep = scan.number(next)? != 0.0;
195 let x = scan.number(next)?;
196 let y = scan.number(next)?;
197 let end = if next == 'A' {
198 [x, y]
199 } else {
200 [pos[0] + x, pos[1] + y]
201 };
202 if current.is_empty() {
203 return Err(IconPathError::NoCurrentPoint);
204 }
205 flatten_arc(&mut current, pos, end, rx, ry, rot, large, sweep);
206 pos = end;
207 }
208 'Z' | 'z' => {
209 if current.len() > 1 {
210 contours.push(std::mem::take(&mut current));
211 } else {
212 current.clear();
213 }
214 pos = start;
215 current.push(pos);
216 }
217 other => return Err(IconPathError::UnsupportedCommand(other)),
218 }
219 }
220
221 if current.len() > 1 {
222 contours.push(current);
223 }
224 Ok(contours)
225}
226
227fn push_point(current: &mut Contour, p: [f64; 2]) -> Result<(), IconPathError> {
228 if current.is_empty() {
229 return Err(IconPathError::NoCurrentPoint);
230 }
231 current.push(p);
232 Ok(())
233}
234
235fn flatten_cubic(out: &mut Contour, p0: [f64; 2], p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) {
236 for i in 1..=CUBIC_STEPS {
237 let t = i as f64 / CUBIC_STEPS as f64;
238 let u = 1.0 - t;
239 let x = u * u * u * p0[0]
240 + 3.0 * u * u * t * p1[0]
241 + 3.0 * u * t * t * p2[0]
242 + t * t * t * p3[0];
243 let y = u * u * u * p0[1]
244 + 3.0 * u * u * t * p1[1]
245 + 3.0 * u * t * t * p2[1]
246 + t * t * t * p3[1];
247 out.push([x, y]);
248 }
249}
250
251#[allow(clippy::too_many_arguments)]
257fn flatten_arc(
258 out: &mut Contour,
259 from: [f64; 2],
260 to: [f64; 2],
261 rx: f64,
262 ry: f64,
263 x_rot_deg: f64,
264 large: bool,
265 sweep: bool,
266) {
267 let (mut rx, mut ry) = (rx.abs(), ry.abs());
268 if (from[0] - to[0]).abs() + (from[1] - to[1]).abs() < 1e-12 {
272 return;
273 }
274 if rx < 1e-12 || ry < 1e-12 {
276 out.push(to);
277 return;
278 }
279 let phi = x_rot_deg.to_radians();
280 let (cos_p, sin_p) = (phi.cos(), phi.sin());
281
282 let dx2 = (from[0] - to[0]) * 0.5;
283 let dy2 = (from[1] - to[1]) * 0.5;
284 let x1p = cos_p * dx2 + sin_p * dy2;
285 let y1p = -sin_p * dx2 + cos_p * dy2;
286
287 let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
289 if lambda > 1.0 {
290 let s = lambda.sqrt();
291 rx *= s;
292 ry *= s;
293 }
294
295 let num = (rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p).max(0.0);
296 let den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
297 let coef = if den <= 0.0 {
298 0.0
299 } else {
300 let c = (num / den).sqrt();
301 if large == sweep {
302 -c
303 } else {
304 c
305 }
306 };
307 let cxp = coef * rx * y1p / ry;
308 let cyp = -coef * ry * x1p / rx;
309 let cx = cos_p * cxp - sin_p * cyp + (from[0] + to[0]) * 0.5;
310 let cy = sin_p * cxp + cos_p * cyp + (from[1] + to[1]) * 0.5;
311
312 let theta = angle(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry);
313 let mut delta = angle(
314 (x1p - cxp) / rx,
315 (y1p - cyp) / ry,
316 (-x1p - cxp) / rx,
317 (-y1p - cyp) / ry,
318 );
319 if !sweep && delta > 0.0 {
320 delta -= 2.0 * PI;
321 } else if sweep && delta < 0.0 {
322 delta += 2.0 * PI;
323 }
324
325 let steps = ((delta.abs() / ARC_STEP).ceil() as usize).max(1);
326 for i in 1..=steps {
327 let t = theta + delta * (i as f64 / steps as f64);
328 let (ct, st) = (t.cos(), t.sin());
329 out.push([
330 cx + cos_p * rx * ct - sin_p * ry * st,
331 cy + sin_p * rx * ct + cos_p * ry * st,
332 ]);
333 }
334 if let Some(last) = out.last_mut() {
338 *last = to;
339 }
340}
341
342fn angle(ux: f64, uy: f64, vx: f64, vy: f64) -> f64 {
344 let dot = ux * vx + uy * vy;
345 let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
346 if len <= 0.0 {
347 return 0.0;
348 }
349 let mut a = (dot / len).clamp(-1.0, 1.0).acos();
350 if ux * vy - uy * vx < 0.0 {
351 a = -a;
352 }
353 a
354}
355
356struct Scanner<'a> {
360 bytes: &'a [u8],
361 i: usize,
362}
363
364impl<'a> Scanner<'a> {
365 fn new(s: &'a str) -> Self {
366 Self {
367 bytes: s.as_bytes(),
368 i: 0,
369 }
370 }
371
372 fn peek(&self) -> Option<char> {
373 self.bytes.get(self.i).map(|b| *b as char)
374 }
375
376 fn bump(&mut self) {
377 self.i += 1;
378 }
379
380 fn skip_separators(&mut self) {
381 while let Some(c) = self.peek() {
382 if c.is_ascii_whitespace() || c == ',' {
383 self.bump();
384 } else {
385 break;
386 }
387 }
388 }
389
390 fn number(&mut self, cmd: char) -> Result<f64, IconPathError> {
392 self.skip_separators();
393 let start = self.i;
394 if matches!(self.peek(), Some('+') | Some('-')) {
395 self.bump();
396 }
397 let mut seen_digit = false;
398 let mut seen_dot = false;
399 while let Some(c) = self.peek() {
400 match c {
401 '0'..='9' => {
402 seen_digit = true;
403 self.bump();
404 }
405 '.' if !seen_dot => {
406 seen_dot = true;
407 self.bump();
408 }
409 'e' | 'E' if seen_digit => {
410 self.bump();
411 if matches!(self.peek(), Some('+') | Some('-')) {
412 self.bump();
413 }
414 }
415 _ => break,
416 }
417 }
418 if !seen_digit {
419 self.i = start;
420 return Err(IconPathError::MissingArgument(cmd));
421 }
422 let text = std::str::from_utf8(&self.bytes[start..self.i])
423 .map_err(|_| IconPathError::BadNumber(String::from("<non-utf8>")))?;
424 text.parse::<f64>()
425 .map_err(|_| IconPathError::BadNumber(text.to_string()))
426 }
427}