1use quick_xml::events::Event;
8use quick_xml::Reader;
9
10#[derive(Debug, Clone, Copy)]
12pub struct ViewBox {
13 pub min_x: f64,
14 pub min_y: f64,
15 pub width: f64,
16 pub height: f64,
17}
18
19#[derive(Debug, Clone)]
21pub enum SvgCommand {
22 MoveTo(f64, f64),
23 LineTo(f64, f64),
24 CurveTo(f64, f64, f64, f64, f64, f64),
25 ClosePath,
26 SetFill(f64, f64, f64),
27 SetFillNone,
28 SetStroke(f64, f64, f64),
29 SetStrokeNone,
30 SetStrokeWidth(f64),
31 Fill,
32 Stroke,
33 FillAndStroke,
34 SetLineCap(u32),
35 SetLineJoin(u32),
36 SaveState,
37 RestoreState,
38}
39
40pub fn parse_view_box(s: &str) -> Option<ViewBox> {
42 let parts: Vec<f64> = s
43 .split_whitespace()
44 .filter_map(|p| p.parse::<f64>().ok())
45 .collect();
46 if parts.len() == 4 {
47 Some(ViewBox {
48 min_x: parts[0],
49 min_y: parts[1],
50 width: parts[2],
51 height: parts[3],
52 })
53 } else {
54 None
55 }
56}
57
58pub fn parse_svg(
60 content: &str,
61 _view_box: ViewBox,
62 _target_width: f64,
63 _target_height: f64,
64) -> Vec<SvgCommand> {
65 let mut commands = Vec::new();
66 let mut reader = Reader::from_str(content);
67
68 let mut fill_stack: Vec<Option<(f64, f64, f64)>> = vec![Some((0.0, 0.0, 0.0))];
69 let mut stroke_stack: Vec<Option<(f64, f64, f64)>> = vec![None];
70 let mut stroke_width_stack: Vec<f64> = vec![1.0];
71
72 let mut buf = Vec::new();
73
74 loop {
75 let event = reader.read_event_into(&mut buf);
76 let (e_ref, is_start) = match &event {
77 Ok(Event::Start(e)) => (Some(e), true),
78 Ok(Event::Empty(e)) => (Some(e), false),
79 Ok(Event::End(e)) => {
80 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
81 if tag_name == "g" {
82 fill_stack.pop();
83 stroke_stack.pop();
84 stroke_width_stack.pop();
85 commands.push(SvgCommand::RestoreState);
86 }
87 buf.clear();
88 continue;
89 }
90 Ok(Event::Eof) => break,
91 Err(_) => break,
92 _ => {
93 buf.clear();
94 continue;
95 }
96 };
97 if let Some(e) = e_ref {
98 let tag_name = String::from_utf8_lossy(e.name().as_ref()).to_string();
99
100 let fill = get_attr(e, "fill");
102 let stroke = get_attr(e, "stroke");
103 let sw = get_attr(e, "stroke-width");
104
105 let current_fill = if let Some(ref f) = fill {
106 if f == "none" {
107 None
108 } else {
109 parse_svg_color(f).or(*fill_stack.last().unwrap_or(&Some((0.0, 0.0, 0.0))))
110 }
111 } else {
112 *fill_stack.last().unwrap_or(&Some((0.0, 0.0, 0.0)))
113 };
114
115 let current_stroke = if let Some(ref s) = stroke {
116 if s == "none" {
117 None
118 } else {
119 parse_svg_color(s).or(*stroke_stack.last().unwrap_or(&None))
120 }
121 } else {
122 *stroke_stack.last().unwrap_or(&None)
123 };
124
125 let current_sw = sw
126 .as_deref()
127 .and_then(|s| s.parse::<f64>().ok())
128 .unwrap_or(*stroke_width_stack.last().unwrap_or(&1.0));
129
130 match tag_name.as_str() {
131 "g" if is_start => {
132 commands.push(SvgCommand::SaveState);
133 fill_stack.push(current_fill);
134 stroke_stack.push(current_stroke);
135 stroke_width_stack.push(current_sw);
136 }
137 "rect" => {
138 let x = get_attr_f64(e, "x").unwrap_or(0.0);
139 let y = get_attr_f64(e, "y").unwrap_or(0.0);
140 let w = get_attr_f64(e, "width").unwrap_or(0.0);
141 let h = get_attr_f64(e, "height").unwrap_or(0.0);
142
143 emit_shape(
144 &mut commands,
145 current_fill,
146 current_stroke,
147 current_sw,
148 || {
149 vec![
150 SvgCommand::MoveTo(x, y),
151 SvgCommand::LineTo(x + w, y),
152 SvgCommand::LineTo(x + w, y + h),
153 SvgCommand::LineTo(x, y + h),
154 SvgCommand::ClosePath,
155 ]
156 },
157 );
158 }
159 "circle" => {
160 let cx = get_attr_f64(e, "cx").unwrap_or(0.0);
161 let cy = get_attr_f64(e, "cy").unwrap_or(0.0);
162 let r = get_attr_f64(e, "r").unwrap_or(0.0);
163
164 emit_shape(
165 &mut commands,
166 current_fill,
167 current_stroke,
168 current_sw,
169 || ellipse_commands(cx, cy, r, r),
170 );
171 }
172 "ellipse" => {
173 let cx = get_attr_f64(e, "cx").unwrap_or(0.0);
174 let cy = get_attr_f64(e, "cy").unwrap_or(0.0);
175 let rx = get_attr_f64(e, "rx").unwrap_or(0.0);
176 let ry = get_attr_f64(e, "ry").unwrap_or(0.0);
177
178 emit_shape(
179 &mut commands,
180 current_fill,
181 current_stroke,
182 current_sw,
183 || ellipse_commands(cx, cy, rx, ry),
184 );
185 }
186 "line" => {
187 let x1 = get_attr_f64(e, "x1").unwrap_or(0.0);
188 let y1 = get_attr_f64(e, "y1").unwrap_or(0.0);
189 let x2 = get_attr_f64(e, "x2").unwrap_or(0.0);
190 let y2 = get_attr_f64(e, "y2").unwrap_or(0.0);
191
192 emit_shape(&mut commands, None, current_stroke, current_sw, || {
194 vec![SvgCommand::MoveTo(x1, y1), SvgCommand::LineTo(x2, y2)]
195 });
196 }
197 "polyline" | "polygon" => {
198 let points_str = get_attr(e, "points").unwrap_or_default();
199 let points = parse_points(&points_str);
200 if !points.is_empty() {
201 let close = tag_name == "polygon";
202 emit_shape(
203 &mut commands,
204 current_fill,
205 current_stroke,
206 current_sw,
207 || {
208 let mut cmds = Vec::new();
209 cmds.push(SvgCommand::MoveTo(points[0].0, points[0].1));
210 for &(px, py) in &points[1..] {
211 cmds.push(SvgCommand::LineTo(px, py));
212 }
213 if close {
214 cmds.push(SvgCommand::ClosePath);
215 }
216 cmds
217 },
218 );
219 }
220 }
221 "path" => {
222 let d = get_attr(e, "d").unwrap_or_default();
223 let path_cmds = parse_path_d(&d);
224 if !path_cmds.is_empty() {
225 emit_shape(
226 &mut commands,
227 current_fill,
228 current_stroke,
229 current_sw,
230 || path_cmds.clone(),
231 );
232 }
233 }
234 _ => {}
235 }
236 }
237 buf.clear();
238 }
239
240 commands
241}
242
243fn emit_shape(
244 commands: &mut Vec<SvgCommand>,
245 fill: Option<(f64, f64, f64)>,
246 stroke: Option<(f64, f64, f64)>,
247 stroke_width: f64,
248 path_fn: impl FnOnce() -> Vec<SvgCommand>,
249) {
250 let has_fill = fill.is_some();
251 let has_stroke = stroke.is_some();
252
253 if !has_fill && !has_stroke {
254 return;
255 }
256
257 commands.push(SvgCommand::SaveState);
258
259 if let Some((r, g, b)) = fill {
260 commands.push(SvgCommand::SetFill(r, g, b));
261 }
262 if let Some((r, g, b)) = stroke {
263 commands.push(SvgCommand::SetStroke(r, g, b));
264 commands.push(SvgCommand::SetStrokeWidth(stroke_width));
265 }
266
267 commands.extend(path_fn());
268
269 match (has_fill, has_stroke) {
270 (true, true) => commands.push(SvgCommand::FillAndStroke),
271 (true, false) => commands.push(SvgCommand::Fill),
272 (false, true) => commands.push(SvgCommand::Stroke),
273 _ => {}
274 }
275
276 commands.push(SvgCommand::RestoreState);
277}
278
279pub fn ellipse_commands(cx: f64, cy: f64, rx: f64, ry: f64) -> Vec<SvgCommand> {
281 let k: f64 = 0.5522847498;
282 let kx = rx * k;
283 let ky = ry * k;
284
285 vec![
286 SvgCommand::MoveTo(cx + rx, cy),
287 SvgCommand::CurveTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry),
288 SvgCommand::CurveTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy),
289 SvgCommand::CurveTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry),
290 SvgCommand::CurveTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy),
291 SvgCommand::ClosePath,
292 ]
293}
294
295#[allow(clippy::too_many_arguments)]
298fn svg_arc_to_curves(
299 x1: f64,
300 y1: f64,
301 mut rx: f64,
302 mut ry: f64,
303 x_rotation_deg: f64,
304 large_arc: bool,
305 sweep: bool,
306 x2: f64,
307 y2: f64,
308) -> Vec<SvgCommand> {
309 if (x1 - x2).abs() < 1e-10 && (y1 - y2).abs() < 1e-10 {
311 return vec![];
312 }
313 if rx.abs() < 1e-10 || ry.abs() < 1e-10 {
315 return vec![SvgCommand::LineTo(x2, y2)];
316 }
317
318 rx = rx.abs();
319 ry = ry.abs();
320
321 let phi = x_rotation_deg.to_radians();
322 let cos_phi = phi.cos();
323 let sin_phi = phi.sin();
324
325 let dx = (x1 - x2) / 2.0;
327 let dy = (y1 - y2) / 2.0;
328 let x1p = cos_phi * dx + sin_phi * dy;
329 let y1p = -sin_phi * dx + cos_phi * dy;
330
331 let x1p2 = x1p * x1p;
333 let y1p2 = y1p * y1p;
334 let rx2 = rx * rx;
335 let ry2 = ry * ry;
336 let lambda = x1p2 / rx2 + y1p2 / ry2;
337 if lambda > 1.0 {
338 let lambda_sqrt = lambda.sqrt();
339 rx *= lambda_sqrt;
340 ry *= lambda_sqrt;
341 }
342
343 let rx2 = rx * rx;
344 let ry2 = ry * ry;
345
346 let num = (rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2).max(0.0);
348 let den = rx2 * y1p2 + ry2 * x1p2;
349 let sq = if den.abs() < 1e-10 {
350 0.0
351 } else {
352 (num / den).sqrt()
353 };
354 let sign = if large_arc == sweep { -1.0 } else { 1.0 };
355 let cxp = sign * sq * (rx * y1p / ry);
356 let cyp = sign * sq * -(ry * x1p / rx);
357
358 let cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2.0;
360 let cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2.0;
361
362 let theta1 = angle_between(1.0, 0.0, (x1p - cxp) / rx, (y1p - cyp) / ry);
364 let mut dtheta = angle_between(
365 (x1p - cxp) / rx,
366 (y1p - cyp) / ry,
367 (-x1p - cxp) / rx,
368 (-y1p - cyp) / ry,
369 );
370
371 if !sweep && dtheta > 0.0 {
372 dtheta -= std::f64::consts::TAU;
373 } else if sweep && dtheta < 0.0 {
374 dtheta += std::f64::consts::TAU;
375 }
376
377 let n_segs = (dtheta.abs() / (std::f64::consts::FRAC_PI_2)).ceil() as usize;
379 let n_segs = n_segs.max(1);
380 let d_per_seg = dtheta / n_segs as f64;
381
382 let mut commands = Vec::new();
383 let mut theta = theta1;
384
385 for _ in 0..n_segs {
386 let t1 = theta;
387 let t2 = theta + d_per_seg;
388
389 let alpha = (d_per_seg / 4.0).tan() * 4.0 / 3.0;
391
392 let cos_t1 = t1.cos();
393 let sin_t1 = t1.sin();
394 let cos_t2 = t2.cos();
395 let sin_t2 = t2.sin();
396
397 let ep1x = cos_t1 - alpha * sin_t1;
399 let ep1y = sin_t1 + alpha * cos_t1;
400 let ep2x = cos_t2 + alpha * sin_t2;
401 let ep2y = sin_t2 - alpha * cos_t2;
402
403 let cp1x = cos_phi * rx * ep1x - sin_phi * ry * ep1y + cx;
405 let cp1y = sin_phi * rx * ep1x + cos_phi * ry * ep1y + cy;
406 let cp2x = cos_phi * rx * ep2x - sin_phi * ry * ep2y + cx;
407 let cp2y = sin_phi * rx * ep2x + cos_phi * ry * ep2y + cy;
408 let ex = cos_phi * rx * cos_t2 - sin_phi * ry * sin_t2 + cx;
409 let ey = sin_phi * rx * cos_t2 + cos_phi * ry * sin_t2 + cy;
410
411 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, ex, ey));
412
413 theta = t2;
414 }
415
416 commands
417}
418
419fn angle_between(ux: f64, uy: f64, vx: f64, vy: f64) -> f64 {
421 let dot = ux * vx + uy * vy;
422 let len = (ux * ux + uy * uy).sqrt() * (vx * vx + vy * vy).sqrt();
423 if len.abs() < 1e-10 {
424 return 0.0;
425 }
426 let cos_val = (dot / len).clamp(-1.0, 1.0);
427 let angle = cos_val.acos();
428 if ux * vy - uy * vx < 0.0 {
429 -angle
430 } else {
431 angle
432 }
433}
434
435fn parse_path_d(d: &str) -> Vec<SvgCommand> {
437 let mut commands = Vec::new();
438 let mut cur_x = 0.0f64;
439 let mut cur_y = 0.0f64;
440 let mut start_x = 0.0f64;
441 let mut start_y = 0.0f64;
442
443 let tokens = tokenize_path(d);
444 let mut i = 0;
445
446 while i < tokens.len() {
447 match tokens[i].as_str() {
448 "M" => {
449 if i + 2 < tokens.len() {
450 cur_x = tokens[i + 1].parse().unwrap_or(0.0);
451 cur_y = tokens[i + 2].parse().unwrap_or(0.0);
452 start_x = cur_x;
453 start_y = cur_y;
454 commands.push(SvgCommand::MoveTo(cur_x, cur_y));
455 i += 3;
456 while i + 1 < tokens.len() && is_number(&tokens[i]) {
458 cur_x = tokens[i].parse().unwrap_or(0.0);
459 cur_y = tokens[i + 1].parse().unwrap_or(0.0);
460 commands.push(SvgCommand::LineTo(cur_x, cur_y));
461 i += 2;
462 }
463 } else {
464 i += 1;
465 }
466 }
467 "m" => {
468 if i + 2 < tokens.len() {
469 cur_x += tokens[i + 1].parse::<f64>().unwrap_or(0.0);
470 cur_y += tokens[i + 2].parse::<f64>().unwrap_or(0.0);
471 start_x = cur_x;
472 start_y = cur_y;
473 commands.push(SvgCommand::MoveTo(cur_x, cur_y));
474 i += 3;
475 while i + 1 < tokens.len() && is_number(&tokens[i]) {
476 cur_x += tokens[i].parse::<f64>().unwrap_or(0.0);
477 cur_y += tokens[i + 1].parse::<f64>().unwrap_or(0.0);
478 commands.push(SvgCommand::LineTo(cur_x, cur_y));
479 i += 2;
480 }
481 } else {
482 i += 1;
483 }
484 }
485 "L" => {
486 i += 1;
487 while i + 1 < tokens.len() && is_number(&tokens[i]) {
488 cur_x = tokens[i].parse().unwrap_or(0.0);
489 cur_y = tokens[i + 1].parse().unwrap_or(0.0);
490 commands.push(SvgCommand::LineTo(cur_x, cur_y));
491 i += 2;
492 }
493 }
494 "l" => {
495 i += 1;
496 while i + 1 < tokens.len() && is_number(&tokens[i]) {
497 cur_x += tokens[i].parse::<f64>().unwrap_or(0.0);
498 cur_y += tokens[i + 1].parse::<f64>().unwrap_or(0.0);
499 commands.push(SvgCommand::LineTo(cur_x, cur_y));
500 i += 2;
501 }
502 }
503 "H" => {
504 i += 1;
505 while i < tokens.len() && is_number(&tokens[i]) {
506 cur_x = tokens[i].parse().unwrap_or(0.0);
507 commands.push(SvgCommand::LineTo(cur_x, cur_y));
508 i += 1;
509 }
510 }
511 "h" => {
512 i += 1;
513 while i < tokens.len() && is_number(&tokens[i]) {
514 cur_x += tokens[i].parse::<f64>().unwrap_or(0.0);
515 commands.push(SvgCommand::LineTo(cur_x, cur_y));
516 i += 1;
517 }
518 }
519 "V" => {
520 i += 1;
521 while i < tokens.len() && is_number(&tokens[i]) {
522 cur_y = tokens[i].parse().unwrap_or(0.0);
523 commands.push(SvgCommand::LineTo(cur_x, cur_y));
524 i += 1;
525 }
526 }
527 "v" => {
528 i += 1;
529 while i < tokens.len() && is_number(&tokens[i]) {
530 cur_y += tokens[i].parse::<f64>().unwrap_or(0.0);
531 commands.push(SvgCommand::LineTo(cur_x, cur_y));
532 i += 1;
533 }
534 }
535 "C" => {
536 i += 1;
537 while i + 5 < tokens.len() && is_number(&tokens[i]) {
538 let x1 = tokens[i].parse().unwrap_or(0.0);
539 let y1 = tokens[i + 1].parse().unwrap_or(0.0);
540 let x2 = tokens[i + 2].parse().unwrap_or(0.0);
541 let y2 = tokens[i + 3].parse().unwrap_or(0.0);
542 cur_x = tokens[i + 4].parse().unwrap_or(0.0);
543 cur_y = tokens[i + 5].parse().unwrap_or(0.0);
544 commands.push(SvgCommand::CurveTo(x1, y1, x2, y2, cur_x, cur_y));
545 i += 6;
546 }
547 }
548 "c" => {
549 i += 1;
550 while i + 5 < tokens.len() && is_number(&tokens[i]) {
551 let x1 = cur_x + tokens[i].parse::<f64>().unwrap_or(0.0);
552 let y1 = cur_y + tokens[i + 1].parse::<f64>().unwrap_or(0.0);
553 let x2 = cur_x + tokens[i + 2].parse::<f64>().unwrap_or(0.0);
554 let y2 = cur_y + tokens[i + 3].parse::<f64>().unwrap_or(0.0);
555 cur_x += tokens[i + 4].parse::<f64>().unwrap_or(0.0);
556 cur_y += tokens[i + 5].parse::<f64>().unwrap_or(0.0);
557 commands.push(SvgCommand::CurveTo(x1, y1, x2, y2, cur_x, cur_y));
558 i += 6;
559 }
560 }
561 "Q" => {
562 i += 1;
563 while i + 3 < tokens.len() && is_number(&tokens[i]) {
564 let qx = tokens[i].parse::<f64>().unwrap_or(0.0);
565 let qy = tokens[i + 1].parse::<f64>().unwrap_or(0.0);
566 let end_x = tokens[i + 2].parse::<f64>().unwrap_or(0.0);
567 let end_y = tokens[i + 3].parse::<f64>().unwrap_or(0.0);
568 let c1x = cur_x + (2.0 / 3.0) * (qx - cur_x);
570 let c1y = cur_y + (2.0 / 3.0) * (qy - cur_y);
571 let c2x = end_x + (2.0 / 3.0) * (qx - end_x);
572 let c2y = end_y + (2.0 / 3.0) * (qy - end_y);
573 cur_x = end_x;
574 cur_y = end_y;
575 commands.push(SvgCommand::CurveTo(c1x, c1y, c2x, c2y, cur_x, cur_y));
576 i += 4;
577 }
578 }
579 "q" => {
580 i += 1;
581 while i + 3 < tokens.len() && is_number(&tokens[i]) {
582 let qx = cur_x + tokens[i].parse::<f64>().unwrap_or(0.0);
583 let qy = cur_y + tokens[i + 1].parse::<f64>().unwrap_or(0.0);
584 let end_x = cur_x + tokens[i + 2].parse::<f64>().unwrap_or(0.0);
585 let end_y = cur_y + tokens[i + 3].parse::<f64>().unwrap_or(0.0);
586 let c1x = cur_x + (2.0 / 3.0) * (qx - cur_x);
587 let c1y = cur_y + (2.0 / 3.0) * (qy - cur_y);
588 let c2x = end_x + (2.0 / 3.0) * (qx - end_x);
589 let c2y = end_y + (2.0 / 3.0) * (qy - end_y);
590 cur_x = end_x;
591 cur_y = end_y;
592 commands.push(SvgCommand::CurveTo(c1x, c1y, c2x, c2y, cur_x, cur_y));
593 i += 4;
594 }
595 }
596 "A" => {
597 i += 1;
598 while i + 6 < tokens.len() && is_number(&tokens[i]) {
599 let rx = tokens[i].parse::<f64>().unwrap_or(0.0);
600 let ry = tokens[i + 1].parse::<f64>().unwrap_or(0.0);
601 let x_rotation = tokens[i + 2].parse::<f64>().unwrap_or(0.0);
602 let large_arc = tokens[i + 3].parse::<f64>().unwrap_or(0.0) != 0.0;
603 let sweep = tokens[i + 4].parse::<f64>().unwrap_or(0.0) != 0.0;
604 let end_x = tokens[i + 5].parse::<f64>().unwrap_or(0.0);
605 let end_y = tokens[i + 6].parse::<f64>().unwrap_or(0.0);
606 commands.extend(svg_arc_to_curves(
607 cur_x, cur_y, rx, ry, x_rotation, large_arc, sweep, end_x, end_y,
608 ));
609 cur_x = end_x;
610 cur_y = end_y;
611 i += 7;
612 }
613 }
614 "a" => {
615 i += 1;
616 while i + 6 < tokens.len() && is_number(&tokens[i]) {
617 let rx = tokens[i].parse::<f64>().unwrap_or(0.0);
618 let ry = tokens[i + 1].parse::<f64>().unwrap_or(0.0);
619 let x_rotation = tokens[i + 2].parse::<f64>().unwrap_or(0.0);
620 let large_arc = tokens[i + 3].parse::<f64>().unwrap_or(0.0) != 0.0;
621 let sweep = tokens[i + 4].parse::<f64>().unwrap_or(0.0) != 0.0;
622 let end_x = cur_x + tokens[i + 5].parse::<f64>().unwrap_or(0.0);
623 let end_y = cur_y + tokens[i + 6].parse::<f64>().unwrap_or(0.0);
624 commands.extend(svg_arc_to_curves(
625 cur_x, cur_y, rx, ry, x_rotation, large_arc, sweep, end_x, end_y,
626 ));
627 cur_x = end_x;
628 cur_y = end_y;
629 i += 7;
630 }
631 }
632 "Z" | "z" => {
633 commands.push(SvgCommand::ClosePath);
634 cur_x = start_x;
635 cur_y = start_y;
636 i += 1;
637 }
638 _ => {
639 i += 1;
640 }
641 }
642 }
643
644 commands
645}
646
647fn tokenize_path(d: &str) -> Vec<String> {
649 let mut tokens = Vec::new();
650 let mut current = String::new();
651
652 let chars: Vec<char> = d.chars().collect();
653 let mut i = 0;
654
655 while i < chars.len() {
656 let ch = chars[i];
657
658 if ch.is_alphabetic() {
659 if !current.is_empty() {
660 tokens.push(current.clone());
661 current.clear();
662 }
663 tokens.push(ch.to_string());
664 i += 1;
665 } else if ch == '-'
666 && !current.is_empty()
667 && !current.ends_with('e')
668 && !current.ends_with('E')
669 {
670 tokens.push(current.clone());
672 current.clear();
673 current.push(ch);
674 i += 1;
675 } else if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
676 current.push(ch);
677 i += 1;
678 } else if ch == ',' || ch.is_whitespace() {
679 if !current.is_empty() {
680 tokens.push(current.clone());
681 current.clear();
682 }
683 i += 1;
684 } else {
685 i += 1;
686 }
687 }
688
689 if !current.is_empty() {
690 tokens.push(current);
691 }
692
693 tokens
694}
695
696fn is_number(s: &str) -> bool {
697 s.parse::<f64>().is_ok()
698}
699
700fn parse_svg_color(s: &str) -> Option<(f64, f64, f64)> {
702 let s = s.trim();
703 if let Some(hex) = s.strip_prefix('#') {
704 match hex.len() {
705 3 => {
706 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()? as f64 / 255.0;
707 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()? as f64 / 255.0;
708 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()? as f64 / 255.0;
709 Some((r, g, b))
710 }
711 6 => {
712 let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f64 / 255.0;
713 let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f64 / 255.0;
714 let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f64 / 255.0;
715 Some((r, g, b))
716 }
717 _ => None,
718 }
719 } else if s.starts_with("rgb(") {
720 let inner = s.trim_start_matches("rgb(").trim_end_matches(')');
721 let parts: Vec<&str> = inner.split(',').collect();
722 if parts.len() == 3 {
723 let r = parts[0].trim().parse::<f64>().ok()? / 255.0;
724 let g = parts[1].trim().parse::<f64>().ok()? / 255.0;
725 let b = parts[2].trim().parse::<f64>().ok()? / 255.0;
726 Some((r, g, b))
727 } else {
728 None
729 }
730 } else {
731 match s.to_lowercase().as_str() {
733 "black" => Some((0.0, 0.0, 0.0)),
734 "white" => Some((1.0, 1.0, 1.0)),
735 "red" => Some((1.0, 0.0, 0.0)),
736 "green" => Some((0.0, 0.502, 0.0)),
737 "blue" => Some((0.0, 0.0, 1.0)),
738 "yellow" => Some((1.0, 1.0, 0.0)),
739 "gray" | "grey" => Some((0.502, 0.502, 0.502)),
740 "orange" => Some((1.0, 0.647, 0.0)),
741 "purple" => Some((0.502, 0.0, 0.502)),
742 "cyan" => Some((0.0, 1.0, 1.0)),
743 "magenta" => Some((1.0, 0.0, 1.0)),
744 _ => None,
745 }
746 }
747}
748
749fn parse_points(s: &str) -> Vec<(f64, f64)> {
751 let nums: Vec<f64> = s
752 .split(|c: char| c == ',' || c.is_whitespace())
753 .filter(|s| !s.is_empty())
754 .filter_map(|s| s.parse::<f64>().ok())
755 .collect();
756
757 nums.chunks(2)
758 .filter(|c| c.len() == 2)
759 .map(|c| (c[0], c[1]))
760 .collect()
761}
762
763fn get_attr(e: &quick_xml::events::BytesStart, name: &str) -> Option<String> {
765 for attr in e.attributes().flatten() {
766 if attr.key.as_ref() == name.as_bytes() {
767 return String::from_utf8(attr.value.to_vec()).ok();
768 }
769 }
770 None
771}
772
773fn get_attr_f64(e: &quick_xml::events::BytesStart, name: &str) -> Option<f64> {
774 get_attr(e, name).and_then(|s| s.parse::<f64>().ok())
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780
781 #[test]
782 fn test_parse_view_box() {
783 let vb = parse_view_box("0 0 100 200").unwrap();
784 assert!((vb.min_x - 0.0).abs() < 0.001);
785 assert!((vb.width - 100.0).abs() < 0.001);
786 assert!((vb.height - 200.0).abs() < 0.001);
787 }
788
789 #[test]
790 fn test_parse_view_box_invalid() {
791 assert!(parse_view_box("bad").is_none());
792 }
793
794 #[test]
795 fn test_parse_rect() {
796 let cmds = parse_svg(
797 r##"<rect x="10" y="20" width="100" height="50" fill="#ff0000"/>"##,
798 ViewBox {
799 min_x: 0.0,
800 min_y: 0.0,
801 width: 200.0,
802 height: 200.0,
803 },
804 200.0,
805 200.0,
806 );
807 assert!(!cmds.is_empty());
808 assert!(cmds
810 .iter()
811 .any(|c| matches!(c, SvgCommand::SetFill(r, _, _) if (*r - 1.0).abs() < 0.01)));
812 }
813
814 #[test]
815 fn test_parse_circle() {
816 let cmds = parse_svg(
817 r#"<circle cx="50" cy="50" r="25" fill="blue"/>"#,
818 ViewBox {
819 min_x: 0.0,
820 min_y: 0.0,
821 width: 100.0,
822 height: 100.0,
823 },
824 100.0,
825 100.0,
826 );
827 assert!(!cmds.is_empty());
828 assert!(cmds.iter().any(|c| matches!(c, SvgCommand::CurveTo(..))));
829 }
830
831 #[test]
832 fn test_parse_path_m_l_z() {
833 let cmds = parse_path_d("M 10 20 L 30 40 Z");
834 assert!(
835 matches!(cmds[0], SvgCommand::MoveTo(x, y) if (x - 10.0).abs() < 0.001 && (y - 20.0).abs() < 0.001)
836 );
837 assert!(
838 matches!(cmds[1], SvgCommand::LineTo(x, y) if (x - 30.0).abs() < 0.001 && (y - 40.0).abs() < 0.001)
839 );
840 assert!(matches!(cmds[2], SvgCommand::ClosePath));
841 }
842
843 #[test]
844 fn test_parse_path_relative() {
845 let cmds = parse_path_d("m 10 20 l 5 5 z");
846 assert!(
847 matches!(cmds[0], SvgCommand::MoveTo(x, y) if (x - 10.0).abs() < 0.001 && (y - 20.0).abs() < 0.001)
848 );
849 assert!(
850 matches!(cmds[1], SvgCommand::LineTo(x, y) if (x - 15.0).abs() < 0.001 && (y - 25.0).abs() < 0.001)
851 );
852 }
853
854 #[test]
855 fn test_parse_hex_color() {
856 let (r, g, b) = parse_svg_color("#ff0000").unwrap();
857 assert!((r - 1.0).abs() < 0.01);
858 assert!((g - 0.0).abs() < 0.01);
859 assert!((b - 0.0).abs() < 0.01);
860 }
861
862 #[test]
863 fn test_parse_line() {
864 let cmds = parse_svg(
865 r#"<line x1="0" y1="0" x2="100" y2="100" stroke="black"/>"#,
866 ViewBox {
867 min_x: 0.0,
868 min_y: 0.0,
869 width: 100.0,
870 height: 100.0,
871 },
872 100.0,
873 100.0,
874 );
875 assert!(!cmds.is_empty());
876 assert!(cmds.iter().any(|c| matches!(c, SvgCommand::Stroke)));
877 }
878
879 #[test]
880 fn test_empty_svg() {
881 let cmds = parse_svg(
882 "",
883 ViewBox {
884 min_x: 0.0,
885 min_y: 0.0,
886 width: 100.0,
887 height: 100.0,
888 },
889 100.0,
890 100.0,
891 );
892 assert!(cmds.is_empty());
893 }
894}