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