1use std::collections::BTreeMap;
2use std::f64::consts::{FRAC_PI_2, TAU};
3use std::str::FromStr;
4
5use svgtypes::{SimplePathSegment, SimplifyingPathParser, Transform};
6
7const SVG_NS: &str = "http://www.w3.org/2000/svg";
8const SHAPES: [&str; 7] = [
9 "path", "rect", "circle", "ellipse", "polygon", "polyline", "line",
10];
11const SKIPPED: [&str; 11] = [
12 "defs",
13 "mask",
14 "clipPath",
15 "linearGradient",
16 "radialGradient",
17 "title",
18 "desc",
19 "symbol",
20 "pattern",
21 "marker",
22 "metadata",
23];
24const STYLE_KEYS: [&str; 8] = [
25 "fill",
26 "stroke",
27 "stroke-width",
28 "stroke-linecap",
29 "stroke-linejoin",
30 "mask",
31 "fill-rule",
32 "opacity",
33];
34
35#[derive(Clone, Debug, PartialEq)]
36pub enum Paint {
37 Color([u8; 3]),
38 Gradient(Gradient),
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub struct Gradient {
43 pub id: String,
44 pub stops: Vec<(f32, [u8; 3])>,
45 pub x1: f32,
46 pub x2: f32,
47 pub y1: f32,
48 pub y2: f32,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum FillRule {
53 NonZero,
54 EvenOdd,
55}
56
57#[derive(Clone, Debug, PartialEq)]
58pub struct Path {
59 pub points: Vec<[f64; 2]>,
60 pub closed: bool,
61}
62
63#[derive(Clone, Debug, PartialEq)]
64pub struct Element {
65 pub tag: String,
66 pub paths: Vec<Path>,
67 pub fill: Option<Paint>,
68 pub stroke: Option<Paint>,
69 pub stroke_width: f64,
70 pub joined: bool,
71 pub fill_rule: FillRule,
72 pub holes: Vec<Vec<[f64; 2]>>,
73 pub ground: bool,
74}
75
76#[derive(Clone, Debug, PartialEq)]
77pub struct Drawing {
78 pub view: [f64; 4],
79 pub elements: Vec<Element>,
80 pub tolerance: f64,
81}
82
83impl Drawing {
84 pub fn parse(text: &str, tolerance: f64) -> Result<Drawing, String> {
85 if text.contains("<!DOCTYPE") || text.contains("<!ENTITY") {
86 return Err("refusing an SVG with a DOCTYPE or ENTITY".to_string());
87 }
88 let document = roxmltree::Document::parse(text).map_err(|e| format!("svg: {e}"))?;
89 let root = document.root_element();
90 if root.tag_name().name() != "svg" {
91 return Err("svg: the root element is not <svg>".to_string());
92 }
93 let view = match root.attribute("viewBox") {
94 Some(value) => view_box(value)?,
95 None => [0.0, 0.0, 100.0, 100.0],
96 };
97 let size = view[2].max(view[3]).max(1e-9);
98 let tolerance = (tolerance * size).max(1e-9);
99 let mut gradients = BTreeMap::new();
100 let mut masks = BTreeMap::new();
101 for node in root.descendants().filter(|n| is_svg(n)) {
102 let id = node.attribute("id").unwrap_or("").to_string();
103 match node.tag_name().name() {
104 "linearGradient" | "radialGradient" => {
105 gradients.insert(id.clone(), gradient(&node, id)?);
106 }
107 "mask" => {
108 masks.insert(id, mask_holes(&node, tolerance)?);
109 }
110 _ => {}
111 }
112 }
113 let mut walker = Walker {
114 view,
115 tolerance,
116 gradients: &gradients,
117 masks: &masks,
118 elements: Vec::new(),
119 };
120 let mut start = BTreeMap::new();
121 start.insert("fill".to_string(), "black".to_string());
122 walker.walk(root, &start, Transform::default())?;
123 Ok(Drawing {
124 view,
125 elements: walker.elements,
126 tolerance,
127 })
128 }
129}
130
131fn is_svg(node: &roxmltree::Node) -> bool {
132 node.is_element() && matches!(node.tag_name().namespace(), None | Some(SVG_NS))
133}
134
135fn view_box(value: &str) -> Result<[f64; 4], String> {
136 let numbers = numbers(value)?;
137 if numbers.len() != 4 || numbers[2] <= 0.0 || numbers[3] <= 0.0 {
138 return Err(format!("svg: bad viewBox \"{value}\""));
139 }
140 Ok([numbers[0], numbers[1], numbers[2], numbers[3]])
141}
142
143fn numbers(value: &str) -> Result<Vec<f64>, String> {
144 value
145 .split(|c: char| c == ',' || c.is_whitespace())
146 .filter(|part| !part.is_empty())
147 .map(|part| {
148 part.parse::<f64>()
149 .map_err(|_| format!("svg: \"{part}\" is not a number"))
150 })
151 .collect()
152}
153
154fn length(value: Option<&str>, default: f64) -> Result<f64, String> {
155 let Some(value) = value else {
156 return Ok(default);
157 };
158 let trimmed = value.trim().trim_end_matches('%');
159 if trimmed.is_empty() {
160 return Ok(default);
161 }
162 svgtypes::Length::from_str(trimmed)
163 .map(|length| length.number)
164 .map_err(|_| format!("svg: \"{value}\" is not a length"))
165}
166
167fn style_of(
168 node: &roxmltree::Node,
169 inherited: &BTreeMap<String, String>,
170) -> BTreeMap<String, String> {
171 let mut out = inherited.clone();
172 for key in STYLE_KEYS {
173 if let Some(value) = node.attribute(key) {
174 out.insert(key.to_string(), value.to_string());
175 }
176 }
177 for part in node.attribute("style").unwrap_or("").split(';') {
178 if let Some((key, value)) = part.split_once(':') {
179 out.insert(key.trim().to_string(), value.trim().to_string());
180 }
181 }
182 out
183}
184
185fn color(value: &str) -> Result<[u8; 3], String> {
186 let parsed = svgtypes::Color::from_str(value.trim())
187 .map_err(|_| format!("svg: bad colour \"{value}\""))?;
188 Ok([parsed.red, parsed.green, parsed.blue])
189}
190
191fn gradient(node: &roxmltree::Node, id: String) -> Result<Gradient, String> {
192 let mut stops = Vec::new();
193 for stop in node
194 .children()
195 .filter(|n| is_svg(n) && n.tag_name().name() == "stop")
196 {
197 let style = style_of(&stop, &BTreeMap::new());
198 let value = stop
199 .attribute("stop-color")
200 .or(style.get("stop-color").map(String::as_str))
201 .unwrap_or("#000000");
202 let offset = stop.attribute("offset").unwrap_or("0").trim();
203 let offset = match offset.strip_suffix('%') {
204 Some(percent) => percent.parse::<f64>().map(|v| v / 100.0),
205 None => offset.parse::<f64>(),
206 }
207 .map_err(|_| format!("svg: gradient {id} has a bad stop offset \"{offset}\""))?;
208 stops.push((offset as f32, color(value)?));
209 }
210 let coordinate = |key: &str, default: &str| -> Result<f32, String> {
211 let raw = node.attribute(key).unwrap_or(default);
212 raw.trim()
213 .trim_end_matches('%')
214 .parse::<f32>()
215 .map_err(|_| format!("svg: gradient {id} has a bad {key} \"{raw}\""))
216 };
217 Ok(Gradient {
218 x1: coordinate("x1", "0")?,
219 x2: coordinate("x2", "1")?,
220 y1: coordinate("y1", "0")?,
221 y2: coordinate("y2", "0")?,
222 id,
223 stops,
224 })
225}
226
227fn mask_holes(node: &roxmltree::Node, tolerance: f64) -> Result<Vec<Vec<[f64; 2]>>, String> {
228 let mut holes = Vec::new();
229 for shape in node.descendants().filter(|n| is_svg(n)) {
230 if shape.tag_name().name() != "rect" {
231 continue;
232 }
233 let fill = shape.attribute("fill").unwrap_or("").to_ascii_lowercase();
234 if !matches!(fill.as_str(), "black" | "#000" | "#000000") {
235 continue;
236 }
237 let x = length(shape.attribute("x"), 0.0)?;
238 let y = length(shape.attribute("y"), 0.0)?;
239 let w = length(shape.attribute("width"), 0.0)?;
240 let h = length(shape.attribute("height"), 0.0)?;
241 let rx = length(shape.attribute("rx"), 0.0)?;
242 if w > 0.0 && h > 0.0 {
243 holes.push(rounded_rect(x, y, w, h, rx, rx, tolerance));
244 }
245 }
246 Ok(holes)
247}
248
249struct Walker<'a> {
250 view: [f64; 4],
251 tolerance: f64,
252 gradients: &'a BTreeMap<String, Gradient>,
253 masks: &'a BTreeMap<String, Vec<Vec<[f64; 2]>>>,
254 elements: Vec<Element>,
255}
256
257impl Walker<'_> {
258 fn walk(
259 &mut self,
260 node: roxmltree::Node,
261 inherited: &BTreeMap<String, String>,
262 parent: Transform,
263 ) -> Result<(), String> {
264 if !is_svg(&node) {
265 return Ok(());
266 }
267 let tag = node.tag_name().name();
268 if SKIPPED.contains(&tag) {
269 return Ok(());
270 }
271 let own = match node.attribute("transform") {
272 Some(text) => {
273 Transform::from_str(text).map_err(|_| format!("svg: bad transform \"{text}\""))?
274 }
275 None => Transform::default(),
276 };
277 let transform = multiply(&parent, &own);
278 let style = style_of(&node, inherited);
279 if SHAPES.contains(&tag) {
280 self.shape(&node, tag, &style, &transform)?;
281 }
282 for child in node.children() {
283 self.walk(child, &style, transform)?;
284 }
285 Ok(())
286 }
287
288 fn paint(&self, value: Option<&String>) -> Result<Option<Paint>, String> {
289 let Some(value) = value.map(|v| v.trim()) else {
290 return Ok(None);
291 };
292 if value.is_empty() || value == "none" || value == "transparent" {
293 return Ok(None);
294 }
295 if let Some(rest) = value.strip_prefix("url(#") {
296 let id = rest.trim_end_matches(')').trim();
297 return self
298 .gradients
299 .get(id)
300 .map(|g| Some(Paint::Gradient(g.clone())))
301 .ok_or_else(|| format!("svg: no gradient #{id}"));
302 }
303 Ok(Some(Paint::Color(color(value)?)))
304 }
305
306 fn shape(
307 &mut self,
308 node: &roxmltree::Node,
309 tag: &str,
310 style: &BTreeMap<String, String>,
311 transform: &Transform,
312 ) -> Result<(), String> {
313 let det = transform.a * transform.d - transform.b * transform.c;
314 let spread = det.abs().sqrt();
315 let local_tolerance = self.tolerance / spread.max(1e-9);
316 let attr = |key: &str| length(node.attribute(key), 0.0);
317 let mut ground = false;
318 let local: Vec<Path> = match tag {
319 "path" => path_data(node.attribute("d").unwrap_or(""), transform, self.tolerance)?,
320 "rect" => {
321 let (x, y, w, h) = (attr("x")?, attr("y")?, attr("width")?, attr("height")?);
322 let square = (self.view[2] - self.view[3]).abs() < 1e-6;
323 ground = square && w >= self.view[2] * 0.98 && h >= self.view[3] * 0.98;
324 let rx = node.attribute("rx").map(|_| attr("rx")).transpose()?;
325 let ry = node.attribute("ry").map(|_| attr("ry")).transpose()?;
326 let (rx, ry) = match (rx, ry) {
327 (Some(rx), Some(ry)) => (rx, ry),
328 (Some(r), None) | (None, Some(r)) => (r, r),
329 (None, None) => (0.0, 0.0),
330 };
331 closed(rounded_rect(x, y, w, h, rx, ry, local_tolerance), transform)
332 }
333 "circle" => {
334 let r = attr("r")?;
335 closed(
336 ellipse(attr("cx")?, attr("cy")?, r, r, local_tolerance),
337 transform,
338 )
339 }
340 "ellipse" => closed(
341 ellipse(
342 attr("cx")?,
343 attr("cy")?,
344 attr("rx")?,
345 attr("ry")?,
346 local_tolerance,
347 ),
348 transform,
349 ),
350 "polygon" | "polyline" => {
351 let values = numbers(node.attribute("points").unwrap_or(""))?;
352 let points = values
353 .chunks_exact(2)
354 .map(|p| apply(transform, [p[0], p[1]]))
355 .collect::<Vec<_>>();
356 vec![Path {
357 points,
358 closed: tag == "polygon",
359 }]
360 }
361 _ => vec![Path {
362 points: vec![
363 apply(transform, [attr("x1")?, attr("y1")?]),
364 apply(transform, [attr("x2")?, attr("y2")?]),
365 ],
366 closed: false,
367 }],
368 };
369 let paths = local
370 .into_iter()
371 .map(|mut path| {
372 path.points.dedup_by(|a, b| distance(*a, *b) <= 1e-12);
373 if path.closed
374 && path.points.len() > 1
375 && distance(path.points[0], *path.points.last().unwrap()) <= 1e-12
376 {
377 path.points.pop();
378 }
379 path
380 })
381 .filter(|path| path.points.len() > 1)
382 .collect::<Vec<_>>();
383 let fill = self.paint(style.get("fill"))?;
384 let stroke = self.paint(style.get("stroke"))?;
385 let width = length(style.get("stroke-width").map(String::as_str), 0.0)?;
386 let joined = fill.is_some()
387 && stroke.is_some()
388 && width > 0.0
389 && style.get("stroke") == style.get("fill");
390 let fill_rule = match style.get("fill-rule").map(String::as_str) {
391 Some("evenodd") => FillRule::EvenOdd,
392 _ => FillRule::NonZero,
393 };
394 let holes = match style.get("mask").map(|m| m.trim()) {
395 Some(mask) => match mask.strip_prefix("url(#") {
396 Some(rest) => self
397 .masks
398 .get(rest.trim_end_matches(')').trim())
399 .cloned()
400 .unwrap_or_default(),
401 None => Vec::new(),
402 },
403 None => Vec::new(),
404 };
405 self.elements.push(Element {
406 tag: tag.to_string(),
407 paths,
408 fill,
409 stroke,
410 stroke_width: width * spread,
411 joined,
412 fill_rule,
413 holes,
414 ground,
415 });
416 Ok(())
417 }
418}
419
420fn multiply(outer: &Transform, inner: &Transform) -> Transform {
421 Transform::new(
422 outer.a * inner.a + outer.c * inner.b,
423 outer.b * inner.a + outer.d * inner.b,
424 outer.a * inner.c + outer.c * inner.d,
425 outer.b * inner.c + outer.d * inner.d,
426 outer.a * inner.e + outer.c * inner.f + outer.e,
427 outer.b * inner.e + outer.d * inner.f + outer.f,
428 )
429}
430
431fn apply(t: &Transform, p: [f64; 2]) -> [f64; 2] {
432 [t.a * p[0] + t.c * p[1] + t.e, t.b * p[0] + t.d * p[1] + t.f]
433}
434
435fn distance(a: [f64; 2], b: [f64; 2]) -> f64 {
436 ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
437}
438
439fn closed(points: Vec<[f64; 2]>, transform: &Transform) -> Vec<Path> {
440 if points.len() < 3 {
441 return Vec::new();
442 }
443 vec![Path {
444 points: points.into_iter().map(|p| apply(transform, p)).collect(),
445 closed: true,
446 }]
447}
448
449fn arc_steps(radius: f64, tolerance: f64, span: f64) -> usize {
450 if radius <= tolerance {
451 return 1;
452 }
453 let half = (1.0 - tolerance / radius).clamp(-1.0, 1.0).acos();
454 ((span / (2.0 * half)).ceil() as usize).clamp(1, 4096)
455}
456
457fn ellipse(cx: f64, cy: f64, rx: f64, ry: f64, tolerance: f64) -> Vec<[f64; 2]> {
458 if rx <= 0.0 || ry <= 0.0 {
459 return Vec::new();
460 }
461 let steps = arc_steps(rx.max(ry), tolerance, TAU).max(8);
462 (0..steps)
463 .map(|i| {
464 let angle = TAU * i as f64 / steps as f64;
465 [cx + rx * angle.cos(), cy + ry * angle.sin()]
466 })
467 .collect()
468}
469
470fn rounded_rect(x: f64, y: f64, w: f64, h: f64, rx: f64, ry: f64, tolerance: f64) -> Vec<[f64; 2]> {
471 if w <= 0.0 || h <= 0.0 {
472 return Vec::new();
473 }
474 let rx = rx.clamp(0.0, w * 0.5);
475 let ry = ry.clamp(0.0, h * 0.5);
476 if rx <= 0.0 || ry <= 0.0 {
477 return vec![[x, y], [x + w, y], [x + w, y + h], [x, y + h]];
478 }
479 let steps = arc_steps(rx.max(ry), tolerance, FRAC_PI_2).max(2);
480 let corners = [
481 ([x + w - rx, y + ry], -FRAC_PI_2),
482 ([x + w - rx, y + h - ry], 0.0),
483 ([x + rx, y + h - ry], FRAC_PI_2),
484 ([x + rx, y + ry], std::f64::consts::PI),
485 ];
486 let mut out = Vec::with_capacity(4 * (steps + 1));
487 for (center, from) in corners {
488 for i in 0..=steps {
489 let angle = from + FRAC_PI_2 * i as f64 / steps as f64;
490 out.push([center[0] + rx * angle.cos(), center[1] + ry * angle.sin()]);
491 }
492 }
493 out
494}
495
496fn path_data(d: &str, transform: &Transform, tolerance: f64) -> Result<Vec<Path>, String> {
497 let mut paths = Vec::new();
498 let mut current: Vec<[f64; 2]> = Vec::new();
499 let mut start = [0.0, 0.0];
500 let mut last = [0.0, 0.0];
501 let flush = |current: &mut Vec<[f64; 2]>, paths: &mut Vec<Path>, closed: bool| {
502 if current.len() > 1 {
503 paths.push(Path {
504 points: std::mem::take(current),
505 closed,
506 });
507 } else {
508 current.clear();
509 }
510 };
511 for segment in SimplifyingPathParser::from(d) {
512 let segment = segment.map_err(|e| format!("svg: path data: {e}"))?;
513 match segment {
514 SimplePathSegment::MoveTo { x, y } => {
515 flush(&mut current, &mut paths, false);
516 start = apply(transform, [x, y]);
517 last = start;
518 current.push(start);
519 }
520 SimplePathSegment::LineTo { x, y } => {
521 if current.is_empty() {
522 current.push(last);
523 }
524 last = apply(transform, [x, y]);
525 current.push(last);
526 }
527 SimplePathSegment::Quadratic { x1, y1, x, y } => {
528 if current.is_empty() {
529 current.push(last);
530 }
531 let c = apply(transform, [x1, y1]);
532 let end = apply(transform, [x, y]);
533 let bend = [last[0] - 2.0 * c[0] + end[0], last[1] - 2.0 * c[1] + end[1]];
534 let steps = wang(bend, 0.25, tolerance);
535 for i in 1..=steps {
536 let t = i as f64 / steps as f64;
537 let u = 1.0 - t;
538 current.push([
539 u * u * last[0] + 2.0 * u * t * c[0] + t * t * end[0],
540 u * u * last[1] + 2.0 * u * t * c[1] + t * t * end[1],
541 ]);
542 }
543 last = end;
544 }
545 SimplePathSegment::CurveTo {
546 x1,
547 y1,
548 x2,
549 y2,
550 x,
551 y,
552 } => {
553 if current.is_empty() {
554 current.push(last);
555 }
556 let c1 = apply(transform, [x1, y1]);
557 let c2 = apply(transform, [x2, y2]);
558 let end = apply(transform, [x, y]);
559 let b1 = [last[0] - 2.0 * c1[0] + c2[0], last[1] - 2.0 * c1[1] + c2[1]];
560 let b2 = [c1[0] - 2.0 * c2[0] + end[0], c1[1] - 2.0 * c2[1] + end[1]];
561 let bend = if b1[0].hypot(b1[1]) > b2[0].hypot(b2[1]) {
562 b1
563 } else {
564 b2
565 };
566 let steps = wang(bend, 0.75, tolerance);
567 for i in 1..=steps {
568 let t = i as f64 / steps as f64;
569 let u = 1.0 - t;
570 let (w0, w1, w2, w3) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
571 current.push([
572 w0 * last[0] + w1 * c1[0] + w2 * c2[0] + w3 * end[0],
573 w0 * last[1] + w1 * c1[1] + w2 * c2[1] + w3 * end[1],
574 ]);
575 }
576 last = end;
577 }
578 SimplePathSegment::ClosePath => {
579 flush(&mut current, &mut paths, true);
580 last = start;
581 }
582 }
583 }
584 flush(&mut current, &mut paths, false);
585 Ok(paths)
586}
587
588fn wang(bend: [f64; 2], factor: f64, tolerance: f64) -> usize {
589 let size = bend[0].hypot(bend[1]);
590 ((factor * size / tolerance).sqrt().ceil() as usize).clamp(1, 1024)
591}