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" if i + 2 < tokens.len() => {
480 cur_x = tokens[i + 1].parse().unwrap_or(0.0);
481 cur_y = tokens[i + 2].parse().unwrap_or(0.0);
482 start_x = cur_x;
483 start_y = cur_y;
484 commands.push(SvgCommand::MoveTo(cur_x, cur_y));
485 i += 3;
486 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 "m" if i + 2 < tokens.len() => {
495 cur_x += tokens[i + 1].parse::<f64>().unwrap_or(0.0);
496 cur_y += tokens[i + 2].parse::<f64>().unwrap_or(0.0);
497 start_x = cur_x;
498 start_y = cur_y;
499 commands.push(SvgCommand::MoveTo(cur_x, cur_y));
500 i += 3;
501 while i + 1 < tokens.len() && is_number(&tokens[i]) {
502 cur_x += tokens[i].parse::<f64>().unwrap_or(0.0);
503 cur_y += tokens[i + 1].parse::<f64>().unwrap_or(0.0);
504 commands.push(SvgCommand::LineTo(cur_x, cur_y));
505 i += 2;
506 }
507 }
508 "L" => {
509 i += 1;
510 while i + 1 < tokens.len() && is_number(&tokens[i]) {
511 cur_x = tokens[i].parse().unwrap_or(0.0);
512 cur_y = tokens[i + 1].parse().unwrap_or(0.0);
513 commands.push(SvgCommand::LineTo(cur_x, cur_y));
514 i += 2;
515 }
516 }
517 "l" => {
518 i += 1;
519 while i + 1 < tokens.len() && is_number(&tokens[i]) {
520 cur_x += tokens[i].parse::<f64>().unwrap_or(0.0);
521 cur_y += tokens[i + 1].parse::<f64>().unwrap_or(0.0);
522 commands.push(SvgCommand::LineTo(cur_x, cur_y));
523 i += 2;
524 }
525 }
526 "H" => {
527 i += 1;
528 while i < tokens.len() && is_number(&tokens[i]) {
529 cur_x = tokens[i].parse().unwrap_or(0.0);
530 commands.push(SvgCommand::LineTo(cur_x, cur_y));
531 i += 1;
532 }
533 }
534 "h" => {
535 i += 1;
536 while i < tokens.len() && is_number(&tokens[i]) {
537 cur_x += tokens[i].parse::<f64>().unwrap_or(0.0);
538 commands.push(SvgCommand::LineTo(cur_x, cur_y));
539 i += 1;
540 }
541 }
542 "V" => {
543 i += 1;
544 while i < tokens.len() && is_number(&tokens[i]) {
545 cur_y = tokens[i].parse().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::<f64>().unwrap_or(0.0);
554 commands.push(SvgCommand::LineTo(cur_x, cur_y));
555 i += 1;
556 }
557 }
558 "C" => {
559 i += 1;
560 while i + 5 < tokens.len() && is_number(&tokens[i]) {
561 let x1 = tokens[i].parse().unwrap_or(0.0);
562 let y1 = tokens[i + 1].parse().unwrap_or(0.0);
563 let x2 = tokens[i + 2].parse().unwrap_or(0.0);
564 let y2 = tokens[i + 3].parse().unwrap_or(0.0);
565 cur_x = tokens[i + 4].parse().unwrap_or(0.0);
566 cur_y = tokens[i + 5].parse().unwrap_or(0.0);
567 commands.push(SvgCommand::CurveTo(x1, y1, x2, y2, cur_x, cur_y));
568 i += 6;
569 }
570 }
571 "c" => {
572 i += 1;
573 while i + 5 < tokens.len() && is_number(&tokens[i]) {
574 let x1 = cur_x + tokens[i].parse::<f64>().unwrap_or(0.0);
575 let y1 = cur_y + tokens[i + 1].parse::<f64>().unwrap_or(0.0);
576 let x2 = cur_x + tokens[i + 2].parse::<f64>().unwrap_or(0.0);
577 let y2 = cur_y + tokens[i + 3].parse::<f64>().unwrap_or(0.0);
578 cur_x += tokens[i + 4].parse::<f64>().unwrap_or(0.0);
579 cur_y += tokens[i + 5].parse::<f64>().unwrap_or(0.0);
580 commands.push(SvgCommand::CurveTo(x1, y1, x2, y2, cur_x, cur_y));
581 i += 6;
582 }
583 }
584 "Q" => {
585 i += 1;
586 while i + 3 < tokens.len() && is_number(&tokens[i]) {
587 let qx = tokens[i].parse::<f64>().unwrap_or(0.0);
588 let qy = tokens[i + 1].parse::<f64>().unwrap_or(0.0);
589 let end_x = tokens[i + 2].parse::<f64>().unwrap_or(0.0);
590 let end_y = tokens[i + 3].parse::<f64>().unwrap_or(0.0);
591 let c1x = cur_x + (2.0 / 3.0) * (qx - cur_x);
593 let c1y = cur_y + (2.0 / 3.0) * (qy - cur_y);
594 let c2x = end_x + (2.0 / 3.0) * (qx - end_x);
595 let c2y = end_y + (2.0 / 3.0) * (qy - end_y);
596 cur_x = end_x;
597 cur_y = end_y;
598 commands.push(SvgCommand::CurveTo(c1x, c1y, c2x, c2y, cur_x, cur_y));
599 i += 4;
600 }
601 }
602 "q" => {
603 i += 1;
604 while i + 3 < tokens.len() && is_number(&tokens[i]) {
605 let qx = cur_x + tokens[i].parse::<f64>().unwrap_or(0.0);
606 let qy = cur_y + tokens[i + 1].parse::<f64>().unwrap_or(0.0);
607 let end_x = cur_x + tokens[i + 2].parse::<f64>().unwrap_or(0.0);
608 let end_y = cur_y + tokens[i + 3].parse::<f64>().unwrap_or(0.0);
609 let c1x = cur_x + (2.0 / 3.0) * (qx - cur_x);
610 let c1y = cur_y + (2.0 / 3.0) * (qy - cur_y);
611 let c2x = end_x + (2.0 / 3.0) * (qx - end_x);
612 let c2y = end_y + (2.0 / 3.0) * (qy - end_y);
613 cur_x = end_x;
614 cur_y = end_y;
615 commands.push(SvgCommand::CurveTo(c1x, c1y, c2x, c2y, cur_x, cur_y));
616 i += 4;
617 }
618 }
619 "A" => {
620 i += 1;
621 while i + 6 < tokens.len() && is_number(&tokens[i]) {
622 let rx = tokens[i].parse::<f64>().unwrap_or(0.0);
623 let ry = tokens[i + 1].parse::<f64>().unwrap_or(0.0);
624 let x_rotation = tokens[i + 2].parse::<f64>().unwrap_or(0.0);
625 let large_arc = tokens[i + 3].parse::<f64>().unwrap_or(0.0) != 0.0;
626 let sweep = tokens[i + 4].parse::<f64>().unwrap_or(0.0) != 0.0;
627 let end_x = tokens[i + 5].parse::<f64>().unwrap_or(0.0);
628 let end_y = tokens[i + 6].parse::<f64>().unwrap_or(0.0);
629 commands.extend(svg_arc_to_curves(
630 cur_x, cur_y, rx, ry, x_rotation, large_arc, sweep, end_x, end_y,
631 ));
632 cur_x = end_x;
633 cur_y = end_y;
634 i += 7;
635 }
636 }
637 "a" => {
638 i += 1;
639 while i + 6 < tokens.len() && is_number(&tokens[i]) {
640 let rx = tokens[i].parse::<f64>().unwrap_or(0.0);
641 let ry = tokens[i + 1].parse::<f64>().unwrap_or(0.0);
642 let x_rotation = tokens[i + 2].parse::<f64>().unwrap_or(0.0);
643 let large_arc = tokens[i + 3].parse::<f64>().unwrap_or(0.0) != 0.0;
644 let sweep = tokens[i + 4].parse::<f64>().unwrap_or(0.0) != 0.0;
645 let end_x = cur_x + tokens[i + 5].parse::<f64>().unwrap_or(0.0);
646 let end_y = cur_y + tokens[i + 6].parse::<f64>().unwrap_or(0.0);
647 commands.extend(svg_arc_to_curves(
648 cur_x, cur_y, rx, ry, x_rotation, large_arc, sweep, end_x, end_y,
649 ));
650 cur_x = end_x;
651 cur_y = end_y;
652 i += 7;
653 }
654 }
655 "Z" | "z" => {
656 commands.push(SvgCommand::ClosePath);
657 cur_x = start_x;
658 cur_y = start_y;
659 i += 1;
660 }
661 _ => {
662 i += 1;
663 }
664 }
665 }
666
667 commands
668}
669
670fn tokenize_path(d: &str) -> Vec<String> {
672 let mut tokens = Vec::new();
673 let mut current = String::new();
674
675 let chars: Vec<char> = d.chars().collect();
676 let mut i = 0;
677
678 while i < chars.len() {
679 let ch = chars[i];
680
681 if ch.is_alphabetic() {
682 if !current.is_empty() {
683 tokens.push(current.clone());
684 current.clear();
685 }
686 tokens.push(ch.to_string());
687 i += 1;
688 } else if ch == '-'
689 && !current.is_empty()
690 && !current.ends_with('e')
691 && !current.ends_with('E')
692 {
693 tokens.push(current.clone());
695 current.clear();
696 current.push(ch);
697 i += 1;
698 } else if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
699 current.push(ch);
700 i += 1;
701 } else if ch == ',' || ch.is_whitespace() {
702 if !current.is_empty() {
703 tokens.push(current.clone());
704 current.clear();
705 }
706 i += 1;
707 } else {
708 i += 1;
709 }
710 }
711
712 if !current.is_empty() {
713 tokens.push(current);
714 }
715
716 tokens
717}
718
719fn is_number(s: &str) -> bool {
720 s.parse::<f64>().is_ok()
721}
722
723fn parse_svg_color(s: &str) -> Option<(f64, f64, f64)> {
725 let s = s.trim();
726 if let Some(hex) = s.strip_prefix('#') {
727 match hex.len() {
728 3 => {
729 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()? as f64 / 255.0;
730 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()? as f64 / 255.0;
731 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()? as f64 / 255.0;
732 Some((r, g, b))
733 }
734 6 => {
735 let r = u8::from_str_radix(&hex[0..2], 16).ok()? as f64 / 255.0;
736 let g = u8::from_str_radix(&hex[2..4], 16).ok()? as f64 / 255.0;
737 let b = u8::from_str_radix(&hex[4..6], 16).ok()? as f64 / 255.0;
738 Some((r, g, b))
739 }
740 _ => None,
741 }
742 } else if s.starts_with("rgb(") {
743 let inner = s.trim_start_matches("rgb(").trim_end_matches(')');
744 let parts: Vec<&str> = inner.split(',').collect();
745 if parts.len() == 3 {
746 let r = parts[0].trim().parse::<f64>().ok()? / 255.0;
747 let g = parts[1].trim().parse::<f64>().ok()? / 255.0;
748 let b = parts[2].trim().parse::<f64>().ok()? / 255.0;
749 Some((r, g, b))
750 } else {
751 None
752 }
753 } else {
754 match s.to_lowercase().as_str() {
756 "black" => Some((0.0, 0.0, 0.0)),
757 "white" => Some((1.0, 1.0, 1.0)),
758 "red" => Some((1.0, 0.0, 0.0)),
759 "green" => Some((0.0, 0.502, 0.0)),
760 "blue" => Some((0.0, 0.0, 1.0)),
761 "yellow" => Some((1.0, 1.0, 0.0)),
762 "gray" | "grey" => Some((0.502, 0.502, 0.502)),
763 "orange" => Some((1.0, 0.647, 0.0)),
764 "purple" => Some((0.502, 0.0, 0.502)),
765 "cyan" => Some((0.0, 1.0, 1.0)),
766 "magenta" => Some((1.0, 0.0, 1.0)),
767 _ => None,
768 }
769 }
770}
771
772fn parse_points(s: &str) -> Vec<(f64, f64)> {
774 let nums: Vec<f64> = s
775 .split(|c: char| c == ',' || c.is_whitespace())
776 .filter(|s| !s.is_empty())
777 .filter_map(|s| s.parse::<f64>().ok())
778 .collect();
779
780 nums.chunks(2)
781 .filter(|c| c.len() == 2)
782 .map(|c| (c[0], c[1]))
783 .collect()
784}
785
786fn get_attr(e: &quick_xml::events::BytesStart, name: &str) -> Option<String> {
788 for attr in e.attributes().flatten() {
789 if attr.key.as_ref() == name.as_bytes() {
790 return String::from_utf8(attr.value.to_vec()).ok();
791 }
792 }
793 None
794}
795
796fn get_attr_f64(e: &quick_xml::events::BytesStart, name: &str) -> Option<f64> {
797 get_attr(e, name).and_then(|s| s.parse::<f64>().ok())
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 #[test]
805 fn test_parse_view_box() {
806 let vb = parse_view_box("0 0 100 200").unwrap();
807 assert!((vb.min_x - 0.0).abs() < 0.001);
808 assert!((vb.width - 100.0).abs() < 0.001);
809 assert!((vb.height - 200.0).abs() < 0.001);
810 }
811
812 #[test]
813 fn test_parse_view_box_invalid() {
814 assert!(parse_view_box("bad").is_none());
815 }
816
817 #[test]
818 fn test_parse_rect() {
819 let cmds = parse_svg(
820 r##"<rect x="10" y="20" width="100" height="50" fill="#ff0000"/>"##,
821 ViewBox {
822 min_x: 0.0,
823 min_y: 0.0,
824 width: 200.0,
825 height: 200.0,
826 },
827 200.0,
828 200.0,
829 );
830 assert!(!cmds.is_empty());
831 assert!(cmds
833 .iter()
834 .any(|c| matches!(c, SvgCommand::SetFill(r, _, _) if (*r - 1.0).abs() < 0.01)));
835 }
836
837 #[test]
838 fn test_parse_circle() {
839 let cmds = parse_svg(
840 r#"<circle cx="50" cy="50" r="25" fill="blue"/>"#,
841 ViewBox {
842 min_x: 0.0,
843 min_y: 0.0,
844 width: 100.0,
845 height: 100.0,
846 },
847 100.0,
848 100.0,
849 );
850 assert!(!cmds.is_empty());
851 assert!(cmds.iter().any(|c| matches!(c, SvgCommand::CurveTo(..))));
852 }
853
854 #[test]
855 fn test_parse_path_m_l_z() {
856 let cmds = parse_path_d("M 10 20 L 30 40 Z");
857 assert!(
858 matches!(cmds[0], SvgCommand::MoveTo(x, y) if (x - 10.0).abs() < 0.001 && (y - 20.0).abs() < 0.001)
859 );
860 assert!(
861 matches!(cmds[1], SvgCommand::LineTo(x, y) if (x - 30.0).abs() < 0.001 && (y - 40.0).abs() < 0.001)
862 );
863 assert!(matches!(cmds[2], SvgCommand::ClosePath));
864 }
865
866 #[test]
867 fn test_parse_path_relative() {
868 let cmds = parse_path_d("m 10 20 l 5 5 z");
869 assert!(
870 matches!(cmds[0], SvgCommand::MoveTo(x, y) if (x - 10.0).abs() < 0.001 && (y - 20.0).abs() < 0.001)
871 );
872 assert!(
873 matches!(cmds[1], SvgCommand::LineTo(x, y) if (x - 15.0).abs() < 0.001 && (y - 25.0).abs() < 0.001)
874 );
875 }
876
877 #[test]
878 fn test_parse_hex_color() {
879 let (r, g, b) = parse_svg_color("#ff0000").unwrap();
880 assert!((r - 1.0).abs() < 0.01);
881 assert!((g - 0.0).abs() < 0.01);
882 assert!((b - 0.0).abs() < 0.01);
883 }
884
885 #[test]
886 fn test_parse_line() {
887 let cmds = parse_svg(
888 r#"<line x1="0" y1="0" x2="100" y2="100" stroke="black"/>"#,
889 ViewBox {
890 min_x: 0.0,
891 min_y: 0.0,
892 width: 100.0,
893 height: 100.0,
894 },
895 100.0,
896 100.0,
897 );
898 assert!(!cmds.is_empty());
899 assert!(cmds.iter().any(|c| matches!(c, SvgCommand::Stroke)));
900 }
901
902 #[test]
903 fn test_empty_svg() {
904 let cmds = parse_svg(
905 "",
906 ViewBox {
907 min_x: 0.0,
908 min_y: 0.0,
909 width: 100.0,
910 height: 100.0,
911 },
912 100.0,
913 100.0,
914 );
915 assert!(cmds.is_empty());
916 }
917}