1use crate::types::Instruction;
4use thiserror::Error;
5
6#[derive(Error, Debug, PartialEq)]
7pub enum PathParseError {
8 #[error("Invalid SVG command '{0}' at index {1}")]
9 InvalidCommand(char, usize),
10 #[error("Unexpected end of path string")]
11 UnexpectedEnd,
12 #[error("Failed to parse number '{0}'")]
13 ParseNumberError(String),
14}
15
16pub fn parse_path(d: &str) -> Result<Vec<Instruction>, PathParseError> {
18 let mut instructions = Vec::new();
19 let tokens = tokenize(d)?;
20 let mut idx = 0;
21
22 let mut current_x = 0.0;
23 let mut current_y = 0.0;
24 let mut start_x = 0.0;
25 let mut start_y = 0.0;
26 let mut last_control_point: Option<(f64, f64)> = None;
27 let mut last_cmd: Option<char> = None;
28
29 while idx < tokens.len() {
30 match tokens[idx].as_str() {
31 "M" => {
32 idx += 1;
33 let x = parse_num(&tokens, &mut idx)?;
34 let y = parse_num(&tokens, &mut idx)?;
35 current_x = x;
36 current_y = y;
37 start_x = x;
38 start_y = y;
39 instructions.push(Instruction::MoveTo { x, y });
40 last_cmd = None;
41 last_control_point = None;
42
43 while idx < tokens.len() && is_num_token(&tokens[idx]) {
44 let x = parse_num(&tokens, &mut idx)?;
45 let y = parse_num(&tokens, &mut idx)?;
46 current_x = x;
47 current_y = y;
48 instructions.push(Instruction::LineTo { x, y });
49 }
50 }
51 "m" => {
52 idx += 1;
53 let dx = parse_num(&tokens, &mut idx)?;
54 let dy = parse_num(&tokens, &mut idx)?;
55 current_x += dx;
56 current_y += dy;
57 start_x = current_x;
58 start_y = current_y;
59 instructions.push(Instruction::MoveTo {
60 x: current_x,
61 y: current_y,
62 });
63 last_cmd = None;
64 last_control_point = None;
65
66 while idx < tokens.len() && is_num_token(&tokens[idx]) {
67 let dx = parse_num(&tokens, &mut idx)?;
68 let dy = parse_num(&tokens, &mut idx)?;
69 current_x += dx;
70 current_y += dy;
71 instructions.push(Instruction::LineTo {
72 x: current_x,
73 y: current_y,
74 });
75 }
76 }
77 "L" => {
78 idx += 1;
79 last_cmd = None;
80 last_control_point = None;
81 while idx < tokens.len() && is_num_token(&tokens[idx]) {
82 let x = parse_num(&tokens, &mut idx)?;
83 let y = parse_num(&tokens, &mut idx)?;
84 current_x = x;
85 current_y = y;
86 instructions.push(Instruction::LineTo { x, y });
87 }
88 }
89 "l" => {
90 idx += 1;
91 last_cmd = None;
92 last_control_point = None;
93 while idx < tokens.len() && is_num_token(&tokens[idx]) {
94 let dx = parse_num(&tokens, &mut idx)?;
95 let dy = parse_num(&tokens, &mut idx)?;
96 current_x += dx;
97 current_y += dy;
98 instructions.push(Instruction::LineTo {
99 x: current_x,
100 y: current_y,
101 });
102 }
103 }
104 "H" => {
105 idx += 1;
106 last_cmd = None;
107 last_control_point = None;
108 while idx < tokens.len() && is_num_token(&tokens[idx]) {
109 let x = parse_num(&tokens, &mut idx)?;
110 current_x = x;
111 instructions.push(Instruction::LineTo {
112 x: current_x,
113 y: current_y,
114 });
115 }
116 }
117 "h" => {
118 idx += 1;
119 last_cmd = None;
120 last_control_point = None;
121 while idx < tokens.len() && is_num_token(&tokens[idx]) {
122 let dx = parse_num(&tokens, &mut idx)?;
123 current_x += dx;
124 instructions.push(Instruction::LineTo {
125 x: current_x,
126 y: current_y,
127 });
128 }
129 }
130 "V" => {
131 idx += 1;
132 last_cmd = None;
133 last_control_point = None;
134 while idx < tokens.len() && is_num_token(&tokens[idx]) {
135 let y = parse_num(&tokens, &mut idx)?;
136 current_y = y;
137 instructions.push(Instruction::LineTo {
138 x: current_x,
139 y: current_y,
140 });
141 }
142 }
143 "v" => {
144 idx += 1;
145 last_cmd = None;
146 last_control_point = None;
147 while idx < tokens.len() && is_num_token(&tokens[idx]) {
148 let dy = parse_num(&tokens, &mut idx)?;
149 current_y += dy;
150 instructions.push(Instruction::LineTo {
151 x: current_x,
152 y: current_y,
153 });
154 }
155 }
156 "C" => {
157 idx += 1;
158 while idx < tokens.len() && is_num_token(&tokens[idx]) {
159 let x1 = parse_num(&tokens, &mut idx)?;
160 let y1 = parse_num(&tokens, &mut idx)?;
161 let x2 = parse_num(&tokens, &mut idx)?;
162 let y2 = parse_num(&tokens, &mut idx)?;
163 let x = parse_num(&tokens, &mut idx)?;
164 let y = parse_num(&tokens, &mut idx)?;
165 current_x = x;
166 current_y = y;
167 last_control_point = Some((x2, y2));
168 last_cmd = Some('C');
169 instructions.push(Instruction::CubicCurveTo {
170 x1,
171 y1,
172 x2,
173 y2,
174 x,
175 y,
176 });
177 }
178 }
179 "c" => {
180 idx += 1;
181 while idx < tokens.len() && is_num_token(&tokens[idx]) {
182 let dx1 = parse_num(&tokens, &mut idx)?;
183 let dy1 = parse_num(&tokens, &mut idx)?;
184 let dx2 = parse_num(&tokens, &mut idx)?;
185 let dy2 = parse_num(&tokens, &mut idx)?;
186 let dx = parse_num(&tokens, &mut idx)?;
187 let dy = parse_num(&tokens, &mut idx)?;
188 let x1 = current_x + dx1;
189 let y1 = current_y + dy1;
190 let x2 = current_x + dx2;
191 let y2 = current_y + dy2;
192 let x = current_x + dx;
193 let y = current_y + dy;
194 current_x = x;
195 current_y = y;
196 last_control_point = Some((x2, y2));
197 last_cmd = Some('C');
198 instructions.push(Instruction::CubicCurveTo {
199 x1,
200 y1,
201 x2,
202 y2,
203 x,
204 y,
205 });
206 }
207 }
208 "S" => {
209 idx += 1;
210 while idx < tokens.len() && is_num_token(&tokens[idx]) {
211 let x2 = parse_num(&tokens, &mut idx)?;
212 let y2 = parse_num(&tokens, &mut idx)?;
213 let x = parse_num(&tokens, &mut idx)?;
214 let y = parse_num(&tokens, &mut idx)?;
215 let (x1, y1) = match (last_cmd, last_control_point) {
216 (Some('C'), Some((px2, py2))) => {
217 (2.0 * current_x - px2, 2.0 * current_y - py2)
218 }
219 _ => (current_x, current_y),
220 };
221 current_x = x;
222 current_y = y;
223 last_control_point = Some((x2, y2));
224 last_cmd = Some('C');
225 instructions.push(Instruction::CubicCurveTo {
226 x1,
227 y1,
228 x2,
229 y2,
230 x,
231 y,
232 });
233 }
234 }
235 "s" => {
236 idx += 1;
237 while idx < tokens.len() && is_num_token(&tokens[idx]) {
238 let dx2 = parse_num(&tokens, &mut idx)?;
239 let dy2 = parse_num(&tokens, &mut idx)?;
240 let dx = parse_num(&tokens, &mut idx)?;
241 let dy = parse_num(&tokens, &mut idx)?;
242 let (x1, y1) = match (last_cmd, last_control_point) {
243 (Some('C'), Some((px2, py2))) => {
244 (2.0 * current_x - px2, 2.0 * current_y - py2)
245 }
246 _ => (current_x, current_y),
247 };
248 let x2 = current_x + dx2;
249 let y2 = current_y + dy2;
250 let x = current_x + dx;
251 let y = current_y + dy;
252 current_x = x;
253 current_y = y;
254 last_control_point = Some((x2, y2));
255 last_cmd = Some('C');
256 instructions.push(Instruction::CubicCurveTo {
257 x1,
258 y1,
259 x2,
260 y2,
261 x,
262 y,
263 });
264 }
265 }
266 "Q" => {
267 idx += 1;
268 while idx < tokens.len() && is_num_token(&tokens[idx]) {
269 let x1 = parse_num(&tokens, &mut idx)?;
270 let y1 = parse_num(&tokens, &mut idx)?;
271 let x = parse_num(&tokens, &mut idx)?;
272 let y = parse_num(&tokens, &mut idx)?;
273 current_x = x;
274 current_y = y;
275 last_control_point = Some((x1, y1));
276 last_cmd = Some('Q');
277 instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
278 }
279 }
280 "q" => {
281 idx += 1;
282 while idx < tokens.len() && is_num_token(&tokens[idx]) {
283 let dx1 = parse_num(&tokens, &mut idx)?;
284 let dy1 = parse_num(&tokens, &mut idx)?;
285 let dx = parse_num(&tokens, &mut idx)?;
286 let dy = parse_num(&tokens, &mut idx)?;
287 let x1 = current_x + dx1;
288 let y1 = current_y + dy1;
289 let x = current_x + dx;
290 let y = current_y + dy;
291 current_x = x;
292 current_y = y;
293 last_control_point = Some((x1, y1));
294 last_cmd = Some('Q');
295 instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
296 }
297 }
298 "T" => {
299 idx += 1;
300 while idx < tokens.len() && is_num_token(&tokens[idx]) {
301 let x = parse_num(&tokens, &mut idx)?;
302 let y = parse_num(&tokens, &mut idx)?;
303 let (x1, y1) = match (last_cmd, last_control_point) {
304 (Some('Q'), Some((px1, py1))) => {
305 (2.0 * current_x - px1, 2.0 * current_y - py1)
306 }
307 _ => (current_x, current_y),
308 };
309 current_x = x;
310 current_y = y;
311 last_control_point = Some((x1, y1));
312 last_cmd = Some('Q');
313 instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
314 }
315 }
316 "t" => {
317 idx += 1;
318 while idx < tokens.len() && is_num_token(&tokens[idx]) {
319 let dx = parse_num(&tokens, &mut idx)?;
320 let dy = parse_num(&tokens, &mut idx)?;
321 let (x1, y1) = match (last_cmd, last_control_point) {
322 (Some('Q'), Some((px1, py1))) => {
323 (2.0 * current_x - px1, 2.0 * current_y - py1)
324 }
325 _ => (current_x, current_y),
326 };
327 let x = current_x + dx;
328 let y = current_y + dy;
329 current_x = x;
330 current_y = y;
331 last_control_point = Some((x1, y1));
332 last_cmd = Some('Q');
333 instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
334 }
335 }
336 "A" => {
337 idx += 1;
338 last_cmd = None;
339 last_control_point = None;
340 while idx < tokens.len() && is_num_token(&tokens[idx]) {
341 let rx = parse_num(&tokens, &mut idx)?;
342 let ry = parse_num(&tokens, &mut idx)?;
343 let x_axis_rotation = parse_num(&tokens, &mut idx)?;
344 let large_arc_flag = parse_num(&tokens, &mut idx)? != 0.0;
345 let sweep_flag = parse_num(&tokens, &mut idx)? != 0.0;
346 let x = parse_num(&tokens, &mut idx)?;
347 let y = parse_num(&tokens, &mut idx)?;
348 current_x = x;
349 current_y = y;
350 instructions.push(Instruction::ArcTo {
351 rx: rx.abs(),
352 ry: ry.abs(),
353 x_axis_rotation,
354 large_arc_flag,
355 sweep_flag,
356 x,
357 y,
358 });
359 }
360 }
361 "a" => {
362 idx += 1;
363 last_cmd = None;
364 last_control_point = None;
365 while idx < tokens.len() && is_num_token(&tokens[idx]) {
366 let rx = parse_num(&tokens, &mut idx)?;
367 let ry = parse_num(&tokens, &mut idx)?;
368 let x_axis_rotation = parse_num(&tokens, &mut idx)?;
369 let large_arc_flag = parse_num(&tokens, &mut idx)? != 0.0;
370 let sweep_flag = parse_num(&tokens, &mut idx)? != 0.0;
371 let dx = parse_num(&tokens, &mut idx)?;
372 let dy = parse_num(&tokens, &mut idx)?;
373 let x = current_x + dx;
374 let y = current_y + dy;
375 current_x = x;
376 current_y = y;
377 instructions.push(Instruction::ArcTo {
378 rx: rx.abs(),
379 ry: ry.abs(),
380 x_axis_rotation,
381 large_arc_flag,
382 sweep_flag,
383 x,
384 y,
385 });
386 }
387 }
388 "Z" | "z" => {
389 idx += 1;
390 current_x = start_x;
391 current_y = start_y;
392 last_cmd = None;
393 last_control_point = None;
394 instructions.push(Instruction::ClosePath);
395 }
396 other => {
397 return Err(PathParseError::ParseNumberError(other.to_string()));
398 }
399 }
400 }
401
402 Ok(instructions)
403}
404
405fn is_svg_cmd(c: char) -> bool {
406 matches!(
407 c,
408 'M' | 'm'
409 | 'L'
410 | 'l'
411 | 'H'
412 | 'h'
413 | 'V'
414 | 'v'
415 | 'C'
416 | 'c'
417 | 'S'
418 | 's'
419 | 'Q'
420 | 'q'
421 | 'T'
422 | 't'
423 | 'A'
424 | 'a'
425 | 'Z'
426 | 'z'
427 )
428}
429
430fn is_num_token(s: &str) -> bool {
431 s.parse::<f64>().is_ok()
432}
433
434fn tokenize(d: &str) -> Result<Vec<String>, PathParseError> {
435 let mut tokens = Vec::new();
436 let mut current_token = String::new();
437
438 let chars: Vec<char> = d.chars().collect();
439 let mut i = 0;
440
441 while i < chars.len() {
442 let c = chars[i];
443 if is_svg_cmd(c) {
444 if !current_token.trim().is_empty() {
445 tokens.push(current_token.trim().to_string());
446 current_token = String::new();
447 }
448 tokens.push(c.to_string());
449 i += 1;
450 } else if c == ',' || c.is_whitespace() {
451 if !current_token.trim().is_empty() {
452 tokens.push(current_token.trim().to_string());
453 current_token = String::new();
454 }
455 i += 1;
456 } else if ((c == '-' || c == '+')
457 && !current_token.is_empty()
458 && !current_token.ends_with('e')
459 && !current_token.ends_with('E'))
460 || (c == '.' && current_token.contains('.'))
461 {
462 tokens.push(current_token.trim().to_string());
463 current_token = String::new();
464 current_token.push(c);
465 i += 1;
466 } else {
467 current_token.push(c);
468 i += 1;
469 }
470 }
471
472 if !current_token.trim().is_empty() {
473 tokens.push(current_token.trim().to_string());
474 }
475
476 Ok(tokens)
477}
478
479fn parse_num(tokens: &[String], idx: &mut usize) -> Result<f64, PathParseError> {
480 if *idx >= tokens.len() {
481 return Err(PathParseError::UnexpectedEnd);
482 }
483 let val = tokens[*idx]
484 .parse::<f64>()
485 .map_err(|_| PathParseError::ParseNumberError(tokens[*idx].clone()))?;
486 *idx += 1;
487 Ok(val)
488}
489
490pub fn serialize_instructions(instructions: &[Instruction]) -> String {
492 let mut out = Vec::new();
493
494 for inst in instructions {
495 match inst {
496 Instruction::MoveTo { x, y } => out.push(format!("M {x:.4} {y:.4}")),
497 Instruction::LineTo { x, y } => out.push(format!("L {x:.4} {y:.4}")),
498 Instruction::CubicCurveTo {
499 x1,
500 y1,
501 x2,
502 y2,
503 x,
504 y,
505 } => out.push(format!("C {x1:.4} {y1:.4} {x2:.4} {y2:.4} {x:.4} {y:.4}")),
506 Instruction::QuadCurveTo { x1, y1, x, y } => {
507 out.push(format!("Q {x1:.4} {y1:.4} {x:.4} {y:.4}"))
508 }
509 Instruction::ArcTo {
510 rx,
511 ry,
512 x_axis_rotation,
513 large_arc_flag,
514 sweep_flag,
515 x,
516 y,
517 } => out.push(format!(
518 "A {rx:.4} {ry:.4} {x_axis_rotation:.4} {} {} {x:.4} {y:.4}",
519 if *large_arc_flag { 1 } else { 0 },
520 if *sweep_flag { 1 } else { 0 }
521 )),
522 Instruction::ClosePath => out.push("Z".to_string()),
523 }
524 }
525
526 out.join(" ")
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_parse_and_serialize_line() {
535 let d = "M 10 20 L 100 200 Z";
536 let insts = parse_path(d).unwrap();
537 assert_eq!(insts.len(), 3);
538 assert_eq!(insts[0], Instruction::MoveTo { x: 10.0, y: 20.0 });
539 assert_eq!(insts[1], Instruction::LineTo { x: 100.0, y: 200.0 });
540 assert_eq!(insts[2], Instruction::ClosePath);
541
542 let res = serialize_instructions(&insts);
543 assert_eq!(res, "M 10.0000 20.0000 L 100.0000 200.0000 Z");
544 }
545
546 #[test]
547 fn test_parse_arc() {
548 let d = "M 100 100 L 100 0 A 100 100 0 0 1 200 100 Z";
549 let insts = parse_path(d).unwrap();
550 assert_eq!(insts.len(), 4);
551 assert_eq!(insts[0], Instruction::MoveTo { x: 100.0, y: 100.0 });
552 assert_eq!(insts[1], Instruction::LineTo { x: 100.0, y: 0.0 });
553 assert_eq!(
554 insts[2],
555 Instruction::ArcTo {
556 rx: 100.0,
557 ry: 100.0,
558 x_axis_rotation: 0.0,
559 large_arc_flag: false,
560 sweep_flag: true,
561 x: 200.0,
562 y: 100.0,
563 }
564 );
565 assert_eq!(insts[3], Instruction::ClosePath);
566
567 let res = serialize_instructions(&insts);
568 assert_eq!(
569 res,
570 "M 100.0000 100.0000 L 100.0000 0.0000 A 100.0000 100.0000 0.0000 0 1 200.0000 100.0000 Z"
571 );
572 }
573
574 #[test]
575 fn test_parse_relative_and_hv() {
576 let d = "m 10 20 h 30 v 40 l -10 -10 z";
577 let insts = parse_path(d).unwrap();
578 assert_eq!(insts.len(), 5);
579 assert_eq!(insts[0], Instruction::MoveTo { x: 10.0, y: 20.0 });
580 assert_eq!(insts[1], Instruction::LineTo { x: 40.0, y: 20.0 });
581 assert_eq!(insts[2], Instruction::LineTo { x: 40.0, y: 60.0 });
582 assert_eq!(insts[3], Instruction::LineTo { x: 30.0, y: 50.0 });
583 assert_eq!(insts[4], Instruction::ClosePath);
584 }
585
586 #[test]
587 fn test_parse_chained_beziers_and_smooth() {
588 let d = "M 0 0 C 10 20 30 40 50 60 S 70 80 90 100 Q 110 120 130 140 T 150 160";
589 let insts = parse_path(d).unwrap();
590 assert_eq!(insts.len(), 5);
591 assert_eq!(insts[0], Instruction::MoveTo { x: 0.0, y: 0.0 });
592 assert_eq!(
593 insts[1],
594 Instruction::CubicCurveTo {
595 x1: 10.0,
596 y1: 20.0,
597 x2: 30.0,
598 y2: 40.0,
599 x: 50.0,
600 y: 60.0,
601 }
602 );
603 assert_eq!(
605 insts[2],
606 Instruction::CubicCurveTo {
607 x1: 70.0,
608 y1: 80.0,
609 x2: 70.0,
610 y2: 80.0,
611 x: 90.0,
612 y: 100.0,
613 }
614 );
615 assert_eq!(
616 insts[3],
617 Instruction::QuadCurveTo {
618 x1: 110.0,
619 y1: 120.0,
620 x: 130.0,
621 y: 140.0,
622 }
623 );
624 assert_eq!(
626 insts[4],
627 Instruction::QuadCurveTo {
628 x1: 150.0,
629 y1: 160.0,
630 x: 150.0,
631 y: 160.0,
632 }
633 );
634 }
635
636 #[test]
637 fn test_parse_exponential_and_compact_coordinates() {
638 let d = "M1e1-2e1L.5.25";
639 let insts = parse_path(d).unwrap();
640 assert_eq!(insts.len(), 2);
641 assert_eq!(insts[0], Instruction::MoveTo { x: 10.0, y: -20.0 });
642 assert_eq!(insts[1], Instruction::LineTo { x: 0.5, y: 0.25 });
643 }
644
645 #[test]
646 fn test_parse_error_invalid_command() {
647 let res = parse_path("X 10 20");
648 assert!(res.is_err());
649 }
650}