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
25 while idx < tokens.len() {
26 match tokens[idx].as_str() {
27 "M" => {
28 idx += 1;
29 let x = parse_num(&tokens, &mut idx)?;
30 let y = parse_num(&tokens, &mut idx)?;
31 current_x = x;
32 current_y = y;
33 instructions.push(Instruction::MoveTo { x, y });
34 }
35 "m" => {
36 idx += 1;
37 let dx = parse_num(&tokens, &mut idx)?;
38 let dy = parse_num(&tokens, &mut idx)?;
39 current_x += dx;
40 current_y += dy;
41 instructions.push(Instruction::MoveTo {
42 x: current_x,
43 y: current_y,
44 });
45 }
46 "L" => {
47 idx += 1;
48 let x = parse_num(&tokens, &mut idx)?;
49 let y = parse_num(&tokens, &mut idx)?;
50 current_x = x;
51 current_y = y;
52 instructions.push(Instruction::LineTo { x, y });
53 }
54 "l" => {
55 idx += 1;
56 let dx = parse_num(&tokens, &mut idx)?;
57 let dy = parse_num(&tokens, &mut idx)?;
58 current_x += dx;
59 current_y += dy;
60 instructions.push(Instruction::LineTo {
61 x: current_x,
62 y: current_y,
63 });
64 }
65 "H" => {
66 idx += 1;
67 let x = parse_num(&tokens, &mut idx)?;
68 current_x = x;
69 instructions.push(Instruction::LineTo {
70 x: current_x,
71 y: current_y,
72 });
73 }
74 "h" => {
75 idx += 1;
76 let dx = parse_num(&tokens, &mut idx)?;
77 current_x += dx;
78 instructions.push(Instruction::LineTo {
79 x: current_x,
80 y: current_y,
81 });
82 }
83 "V" => {
84 idx += 1;
85 let y = parse_num(&tokens, &mut idx)?;
86 current_y = y;
87 instructions.push(Instruction::LineTo {
88 x: current_x,
89 y: current_y,
90 });
91 }
92 "v" => {
93 idx += 1;
94 let dy = parse_num(&tokens, &mut idx)?;
95 current_y += dy;
96 instructions.push(Instruction::LineTo {
97 x: current_x,
98 y: current_y,
99 });
100 }
101 "C" => {
102 idx += 1;
103 let x1 = parse_num(&tokens, &mut idx)?;
104 let y1 = parse_num(&tokens, &mut idx)?;
105 let x2 = parse_num(&tokens, &mut idx)?;
106 let y2 = parse_num(&tokens, &mut idx)?;
107 let x = parse_num(&tokens, &mut idx)?;
108 let y = parse_num(&tokens, &mut idx)?;
109 current_x = x;
110 current_y = y;
111 instructions.push(Instruction::CubicCurveTo {
112 x1,
113 y1,
114 x2,
115 y2,
116 x,
117 y,
118 });
119 }
120 "c" => {
121 idx += 1;
122 let dx1 = parse_num(&tokens, &mut idx)?;
123 let dy1 = parse_num(&tokens, &mut idx)?;
124 let dx2 = parse_num(&tokens, &mut idx)?;
125 let dy2 = parse_num(&tokens, &mut idx)?;
126 let dx = parse_num(&tokens, &mut idx)?;
127 let dy = parse_num(&tokens, &mut idx)?;
128 let x1 = current_x + dx1;
129 let y1 = current_y + dy1;
130 let x2 = current_x + dx2;
131 let y2 = current_y + dy2;
132 let x = current_x + dx;
133 let y = current_y + dy;
134 current_x = x;
135 current_y = y;
136 instructions.push(Instruction::CubicCurveTo {
137 x1,
138 y1,
139 x2,
140 y2,
141 x,
142 y,
143 });
144 }
145 "Q" => {
146 idx += 1;
147 let x1 = parse_num(&tokens, &mut idx)?;
148 let y1 = parse_num(&tokens, &mut idx)?;
149 let x = parse_num(&tokens, &mut idx)?;
150 let y = parse_num(&tokens, &mut idx)?;
151 current_x = x;
152 current_y = y;
153 instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
154 }
155 "q" => {
156 idx += 1;
157 let dx1 = parse_num(&tokens, &mut idx)?;
158 let dy1 = parse_num(&tokens, &mut idx)?;
159 let dx = parse_num(&tokens, &mut idx)?;
160 let dy = parse_num(&tokens, &mut idx)?;
161 let x1 = current_x + dx1;
162 let y1 = current_y + dy1;
163 let x = current_x + dx;
164 let y = current_y + dy;
165 current_x = x;
166 current_y = y;
167 instructions.push(Instruction::QuadCurveTo { x1, y1, x, y });
168 }
169 "Z" | "z" => {
170 idx += 1;
171 instructions.push(Instruction::ClosePath);
172 }
173 other => {
174 return Err(PathParseError::ParseNumberError(other.to_string()));
175 }
176 }
177 }
178
179 Ok(instructions)
180}
181
182fn tokenize(d: &str) -> Result<Vec<String>, PathParseError> {
183 let mut tokens = Vec::new();
184 let mut current_token = String::new();
185
186 let chars: Vec<char> = d.chars().collect();
187 let mut i = 0;
188
189 while i < chars.len() {
190 let c = chars[i];
191 if c.is_alphabetic() {
192 if !current_token.trim().is_empty() {
193 tokens.push(current_token.trim().to_string());
194 current_token = String::new();
195 }
196 tokens.push(c.to_string());
197 i += 1;
198 } else if c == ',' || c.is_whitespace() {
199 if !current_token.trim().is_empty() {
200 tokens.push(current_token.trim().to_string());
201 current_token = String::new();
202 }
203 i += 1;
204 } else if c == '-'
205 && !current_token.is_empty()
206 && !current_token.ends_with('e')
207 && !current_token.ends_with('E')
208 {
209 tokens.push(current_token.trim().to_string());
210 current_token = String::new();
211 current_token.push(c);
212 i += 1;
213 } else {
214 current_token.push(c);
215 i += 1;
216 }
217 }
218
219 if !current_token.trim().is_empty() {
220 tokens.push(current_token.trim().to_string());
221 }
222
223 Ok(tokens)
224}
225
226fn parse_num(tokens: &[String], idx: &mut usize) -> Result<f64, PathParseError> {
227 if *idx >= tokens.len() {
228 return Err(PathParseError::UnexpectedEnd);
229 }
230 let val = tokens[*idx]
231 .parse::<f64>()
232 .map_err(|_| PathParseError::ParseNumberError(tokens[*idx].clone()))?;
233 *idx += 1;
234 Ok(val)
235}
236
237pub fn serialize_instructions(instructions: &[Instruction]) -> String {
239 let mut out = Vec::new();
240
241 for inst in instructions {
242 match inst {
243 Instruction::MoveTo { x, y } => out.push(format!("M {x:.4} {y:.4}")),
244 Instruction::LineTo { x, y } => out.push(format!("L {x:.4} {y:.4}")),
245 Instruction::CubicCurveTo {
246 x1,
247 y1,
248 x2,
249 y2,
250 x,
251 y,
252 } => out.push(format!("C {x1:.4} {y1:.4} {x2:.4} {y2:.4} {x:.4} {y:.4}")),
253 Instruction::QuadCurveTo { x1, y1, x, y } => {
254 out.push(format!("Q {x1:.4} {y1:.4} {x:.4} {y:.4}"))
255 }
256 Instruction::ClosePath => out.push("Z".to_string()),
257 }
258 }
259
260 out.join(" ")
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_parse_and_serialize_line() {
269 let d = "M 10 20 L 100 200 Z";
270 let insts = parse_path(d).unwrap();
271 assert_eq!(insts.len(), 3);
272 assert_eq!(insts[0], Instruction::MoveTo { x: 10.0, y: 20.0 });
273 assert_eq!(insts[1], Instruction::LineTo { x: 100.0, y: 200.0 });
274 assert_eq!(insts[2], Instruction::ClosePath);
275
276 let res = serialize_instructions(&insts);
277 assert_eq!(res, "M 10.0000 20.0000 L 100.0000 200.0000 Z");
278 }
279}