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