1use super::super::svg_path_bounds_from_d;
4use crate::model::Bounds;
5
6#[derive(Debug, Clone)]
7pub struct SvgEmittedBoundsContributor {
8 pub tag: String,
9 pub id: Option<String>,
10 pub class: Option<String>,
11 pub d: Option<String>,
12 pub points: Option<String>,
13 pub transform: Option<String>,
14 pub bounds: Bounds,
15}
16
17#[derive(Debug, Clone)]
18pub struct SvgEmittedBoundsDebug {
19 pub bounds: Bounds,
20 pub min_x: Option<SvgEmittedBoundsContributor>,
21 pub min_y: Option<SvgEmittedBoundsContributor>,
22 pub max_x: Option<SvgEmittedBoundsContributor>,
23 pub max_y: Option<SvgEmittedBoundsContributor>,
24}
25
26#[doc(hidden)]
27pub fn debug_svg_emitted_bounds(svg: &str) -> Option<SvgEmittedBoundsDebug> {
28 let mut dbg = SvgEmittedBoundsDebug {
29 bounds: Bounds {
30 min_x: 0.0,
31 min_y: 0.0,
32 max_x: 0.0,
33 max_y: 0.0,
34 },
35 min_x: None,
36 min_y: None,
37 max_x: None,
38 max_y: None,
39 };
40 let b = svg_emitted_bounds_from_svg_inner(svg, Some(&mut dbg))?;
41 dbg.bounds = b;
42 Some(dbg)
43}
44
45pub(in crate::svg::parity) fn svg_emitted_bounds_from_svg(svg: &str) -> Option<Bounds> {
46 svg_emitted_bounds_from_svg_inner(svg, None)
47}
48
49pub(in crate::svg::parity) fn svg_emitted_bounds_from_svg_inner(
50 svg: &str,
51 mut dbg: Option<&mut SvgEmittedBoundsDebug>,
52) -> Option<Bounds> {
53 #[derive(Clone, Copy, Debug)]
54 struct AffineTransform {
55 a: f64,
65 b: f64,
66 c: f64,
67 d: f64,
68 e: f64,
69 f: f64,
70 }
71
72 impl AffineTransform {
73 fn apply_point_f32(self, x: f32, y: f32) -> (f32, f32) {
74 let a = self.a as f32;
77 let b = self.b as f32;
78 let c = self.c as f32;
79 let d = self.d as f32;
80 let e = self.e as f32;
81 let f = self.f as f32;
82 let ox = a.mul_add(x, c.mul_add(y, e));
85 let oy = b.mul_add(x, d.mul_add(y, f));
86 (ox, oy)
87 }
88
89 fn apply_point_f32_no_fma(self, x: f32, y: f32) -> (f32, f32) {
90 let a = self.a as f32;
93 let b = self.b as f32;
94 let c = self.c as f32;
95 let d = self.d as f32;
96 let e = self.e as f32;
97 let f = self.f as f32;
98 let ox = (a * x + c * y) + e;
99 let oy = (b * x + d * y) + f;
100 (ox, oy)
101 }
102 }
103
104 fn parse_f64(raw: &str) -> Option<f64> {
105 let s = raw.trim().trim_end_matches("px").trim();
106 s.parse::<f64>().ok()
107 }
108
109 fn deg_to_rad(deg: f64) -> f64 {
110 deg * std::f64::consts::PI / 180.0
111 }
112
113 fn attr_value<'a>(attrs: &'a str, key: &str) -> Option<&'a str> {
114 let bytes = attrs.as_bytes();
121 let mut from = 0usize;
122 while from < attrs.len() {
123 let rel = attrs[from..].find(key)?;
124 let pos = from + rel;
125 let ok_prefix = pos == 0 || bytes[pos.saturating_sub(1)].is_ascii_whitespace();
126 if ok_prefix {
127 let after_key = pos + key.len();
128 if after_key + 1 < attrs.len()
129 && bytes[after_key] == b'='
130 && bytes[after_key + 1] == b'"'
131 {
132 let start = after_key + 2;
133 let rest = &attrs[start..];
134 let end = rest.find('"')?;
135 return Some(&rest[..end]);
136 }
137 }
138 from = pos + 1;
139 }
140 None
141 }
142
143 fn parse_transform_ops_into(transform: &str, ops: &mut Vec<AffineTransform>) {
144 let mut s = transform.trim();
148
149 while !s.is_empty() {
150 let ws = s
151 .chars()
152 .take_while(|c| c.is_whitespace())
153 .map(|c| c.len_utf8())
154 .sum::<usize>();
155 s = &s[ws..];
156 if s.is_empty() {
157 break;
158 }
159
160 let Some(paren) = s.find('(') else {
161 break;
162 };
163 let name = s[..paren].trim();
164 let rest = &s[paren + 1..];
165 let Some(end) = rest.find(')') else {
166 break;
167 };
168 let inner = rest[..end].replace(',', " ");
169 let mut parts = inner.split_whitespace().filter_map(parse_f64);
170
171 match name {
172 "translate" => {
173 let x = parts.next().unwrap_or(0.0);
174 let y = parts.next().unwrap_or(0.0);
175 ops.push(AffineTransform {
176 a: 1.0,
177 b: 0.0,
178 c: 0.0,
179 d: 1.0,
180 e: x,
181 f: y,
182 });
183 }
184 "scale" => {
185 let sx = parts.next().unwrap_or(1.0);
186 let sy = parts.next().unwrap_or(sx);
187 ops.push(AffineTransform {
188 a: sx,
189 b: 0.0,
190 c: 0.0,
191 d: sy,
192 e: 0.0,
193 f: 0.0,
194 });
195 }
196 "rotate" => {
197 let angle_deg = parts.next().unwrap_or(0.0);
198 let cx = parts.next();
199 let cy = parts.next();
200 let rad = deg_to_rad(angle_deg);
201 let cos = rad.cos();
202 let sin = rad.sin();
203
204 match (cx, cy) {
205 (Some(cx), Some(cy)) => {
206 if cx == 0.0 && cy == 0.0 {
210 ops.push(AffineTransform {
211 a: cos,
212 b: sin,
213 c: -sin,
214 d: cos,
215 e: 0.0,
216 f: 0.0,
217 });
218 } else if cy == 0.0 {
219 ops.push(AffineTransform {
222 a: 1.0,
223 b: 0.0,
224 c: 0.0,
225 d: 1.0,
226 e: cx,
227 f: cy,
228 });
229 ops.push(AffineTransform {
230 a: cos,
231 b: sin,
232 c: -sin,
233 d: cos,
234 e: 0.0,
235 f: 0.0,
236 });
237 ops.push(AffineTransform {
238 a: 1.0,
239 b: 0.0,
240 c: 0.0,
241 d: 1.0,
242 e: -cx,
243 f: -cy,
244 });
245 } else {
246 let e = cx - (cx * cos) + (cy * sin);
248 let f = cy - (cx * sin) - (cy * cos);
249 ops.push(AffineTransform {
250 a: cos,
251 b: sin,
252 c: -sin,
253 d: cos,
254 e,
255 f,
256 });
257 }
258 }
259 _ => {
260 ops.push(AffineTransform {
261 a: cos,
262 b: sin,
263 c: -sin,
264 d: cos,
265 e: 0.0,
266 f: 0.0,
267 });
268 }
269 }
270 }
271 "skewX" | "skewx" => {
272 let angle_deg = parts.next().unwrap_or(0.0);
273 let k = deg_to_rad(angle_deg).tan();
274 ops.push(AffineTransform {
275 a: 1.0,
276 b: 0.0,
277 c: k,
278 d: 1.0,
279 e: 0.0,
280 f: 0.0,
281 });
282 }
283 "skewY" | "skewy" => {
284 let angle_deg = parts.next().unwrap_or(0.0);
285 let k = deg_to_rad(angle_deg).tan();
286 ops.push(AffineTransform {
287 a: 1.0,
288 b: k,
289 c: 0.0,
290 d: 1.0,
291 e: 0.0,
292 f: 0.0,
293 });
294 }
295 "matrix" => {
296 let a = parts.next().unwrap_or(1.0);
298 let b = parts.next().unwrap_or(0.0);
299 let c = parts.next().unwrap_or(0.0);
300 let d = parts.next().unwrap_or(1.0);
301 let e = parts.next().unwrap_or(0.0);
302 let f = parts.next().unwrap_or(0.0);
303 ops.push(AffineTransform { a, b, c, d, e, f });
304 }
305 _ => {}
306 };
307
308 s = &rest[end + 1..];
309 }
310
311 }
313
314 fn parse_view_box(view_box: &str) -> Option<(f64, f64, f64, f64)> {
315 let buf = view_box.replace(',', " ");
316 let mut parts = buf.split_whitespace().filter_map(parse_f64);
317 let x = parts.next()?;
318 let y = parts.next()?;
319 let w = parts.next()?;
320 let h = parts.next()?;
321 if !(w.is_finite() && h.is_finite()) || w <= 0.0 || h <= 0.0 {
322 return None;
323 }
324 Some((x, y, w, h))
325 }
326
327 fn svg_viewport_transform(attrs: &str) -> AffineTransform {
328 let x = attr_value(attrs, "x").and_then(parse_f64).unwrap_or(0.0);
334 let y = attr_value(attrs, "y").and_then(parse_f64).unwrap_or(0.0);
335
336 let Some((vb_x, vb_y, vb_w, vb_h)) = attr_value(attrs, "viewBox").and_then(parse_view_box)
337 else {
338 return AffineTransform {
339 a: 1.0,
340 b: 0.0,
341 c: 0.0,
342 d: 1.0,
343 e: x,
344 f: y,
345 };
346 };
347
348 let w = attr_value(attrs, "width")
349 .and_then(parse_f64)
350 .unwrap_or(vb_w);
351 let h = attr_value(attrs, "height")
352 .and_then(parse_f64)
353 .unwrap_or(vb_h);
354 if !(w.is_finite() && h.is_finite()) || w <= 0.0 || h <= 0.0 {
355 return AffineTransform {
356 a: 1.0,
357 b: 0.0,
358 c: 0.0,
359 d: 1.0,
360 e: x,
361 f: y,
362 };
363 }
364
365 let sx = w / vb_w;
366 let sy = h / vb_h;
367 AffineTransform {
368 a: sx,
369 b: 0.0,
370 c: 0.0,
371 d: sy,
372 e: x - sx * vb_x,
373 f: y - sy * vb_y,
374 }
375 }
376
377 fn maybe_record_dbg(
378 dbg: &mut Option<&mut SvgEmittedBoundsDebug>,
379 tag: &str,
380 attrs: &str,
381 b: Bounds,
382 ) {
383 let Some(dbg) = dbg.as_deref_mut() else {
384 return;
385 };
386 let id = attr_value(attrs, "id").map(|s| s.to_string());
387 let class = attr_value(attrs, "class").map(|s| s.to_string());
388 let d = attr_value(attrs, "d").map(|s| s.to_string());
389 let points = attr_value(attrs, "points").map(|s| s.to_string());
390 let transform = attr_value(attrs, "transform").map(|s| s.to_string());
391 let c = SvgEmittedBoundsContributor {
392 tag: tag.to_string(),
393 id,
394 class,
395 d,
396 points,
397 transform,
398 bounds: b.clone(),
399 };
400
401 if dbg
402 .min_x
403 .as_ref()
404 .map(|cur| b.min_x < cur.bounds.min_x)
405 .unwrap_or(true)
406 {
407 dbg.min_x = Some(c.clone());
408 }
409 if dbg
410 .min_y
411 .as_ref()
412 .map(|cur| b.min_y < cur.bounds.min_y)
413 .unwrap_or(true)
414 {
415 dbg.min_y = Some(c.clone());
416 }
417 if dbg
418 .max_x
419 .as_ref()
420 .map(|cur| b.max_x > cur.bounds.max_x)
421 .unwrap_or(true)
422 {
423 dbg.max_x = Some(c.clone());
424 }
425 if dbg
426 .max_y
427 .as_ref()
428 .map(|cur| b.max_y > cur.bounds.max_y)
429 .unwrap_or(true)
430 {
431 dbg.max_y = Some(c);
432 }
433 }
434
435 fn include_path_d(
436 bounds: &mut Option<Bounds>,
437 extrema_kinds: &mut ExtremaKinds,
438 d: &str,
439 cur_ops: &[AffineTransform],
440 el_ops: &[AffineTransform],
441 ) {
442 if let Some(pb) = svg_path_bounds_from_d(d) {
443 let b = apply_ops_bounds(
444 cur_ops,
445 el_ops,
446 Bounds {
447 min_x: pb.min_x,
448 min_y: pb.min_y,
449 max_x: pb.max_x,
450 max_y: pb.max_y,
451 },
452 );
453 include_rect_inexact(
454 bounds,
455 extrema_kinds,
456 b.min_x,
457 b.min_y,
458 b.max_x,
459 b.max_y,
460 ExtremaKind::Path,
461 );
462 }
463 }
464
465 fn include_points(
466 bounds: &mut Option<Bounds>,
467 extrema_kinds: &mut ExtremaKinds,
468 points: &str,
469 cur_ops: &[AffineTransform],
470 el_ops: &[AffineTransform],
471 kind: ExtremaKind,
472 ) {
473 let mut min_x = f64::INFINITY;
474 let mut min_y = f64::INFINITY;
475 let mut max_x = f64::NEG_INFINITY;
476 let mut max_y = f64::NEG_INFINITY;
477 let mut have = false;
478
479 let buf = points.replace(',', " ");
480 let mut nums = buf.split_whitespace().filter_map(parse_f64);
481 while let Some(x) = nums.next() {
482 let Some(y) = nums.next() else { break };
483 have = true;
484 min_x = min_x.min(x);
485 min_y = min_y.min(y);
486 max_x = max_x.max(x);
487 max_y = max_y.max(y);
488 }
489 if have {
490 let b = apply_ops_bounds(
491 cur_ops,
492 el_ops,
493 Bounds {
494 min_x,
495 min_y,
496 max_x,
497 max_y,
498 },
499 );
500 include_rect_inexact(
501 bounds,
502 extrema_kinds,
503 b.min_x,
504 b.min_y,
505 b.max_x,
506 b.max_y,
507 kind,
508 );
509 }
510 }
511
512 let mut bounds: Option<Bounds> = None;
513
514 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
515 enum ExtremaKind {
516 #[default]
517 Exact,
518 Rotated,
519 RotatedDecomposedPivot,
520 RotatedPivot,
521 Path,
522 }
523
524 #[derive(Clone, Copy, Debug, Default)]
525 struct ExtremaKinds {
526 min_x: ExtremaKind,
527 min_y: ExtremaKind,
528 max_x: ExtremaKind,
529 max_y: ExtremaKind,
530 }
531
532 let mut extrema_kinds = ExtremaKinds::default();
533
534 fn include_rect_inexact(
535 bounds: &mut Option<Bounds>,
536 extrema_kinds: &mut ExtremaKinds,
537 min_x: f64,
538 min_y: f64,
539 max_x: f64,
540 max_y: f64,
541 kind: ExtremaKind,
542 ) {
543 let w = (max_x - min_x).abs();
550 let h = (max_y - min_y).abs();
551 if w < 1e-9 && h < 1e-9 {
552 return;
553 }
554
555 if let Some(cur) = bounds.as_mut() {
556 if min_x < cur.min_x {
557 cur.min_x = min_x;
558 extrema_kinds.min_x = kind;
559 }
560 if min_y < cur.min_y {
561 cur.min_y = min_y;
562 extrema_kinds.min_y = kind;
563 }
564 if max_x > cur.max_x {
565 cur.max_x = max_x;
566 extrema_kinds.max_x = kind;
567 }
568 if max_y > cur.max_y {
569 cur.max_y = max_y;
570 extrema_kinds.max_y = kind;
571 }
572 } else {
573 *bounds = Some(Bounds {
574 min_x,
575 min_y,
576 max_x,
577 max_y,
578 });
579 *extrema_kinds = ExtremaKinds {
580 min_x: kind,
581 min_y: kind,
582 max_x: kind,
583 max_y: kind,
584 };
585 }
586 }
587
588 fn apply_ops_point(
589 cur_ops: &[AffineTransform],
590 el_ops: &[AffineTransform],
591 x: f64,
592 y: f64,
593 ) -> (f64, f64) {
594 let mut x = x as f32;
595 let mut y = y as f32;
596 for op in el_ops.iter().rev() {
597 (x, y) = op.apply_point_f32(x, y);
598 }
599 for op in cur_ops.iter().rev() {
600 (x, y) = op.apply_point_f32(x, y);
601 }
602 (x as f64, y as f64)
603 }
604
605 fn apply_ops_point_no_fma(
606 cur_ops: &[AffineTransform],
607 el_ops: &[AffineTransform],
608 x: f64,
609 y: f64,
610 ) -> (f64, f64) {
611 let mut x = x as f32;
612 let mut y = y as f32;
613 for op in el_ops.iter().rev() {
614 (x, y) = op.apply_point_f32_no_fma(x, y);
615 }
616 for op in cur_ops.iter().rev() {
617 (x, y) = op.apply_point_f32_no_fma(x, y);
618 }
619 (x as f64, y as f64)
620 }
621
622 fn apply_ops_point_f64_then_f32(
623 cur_ops: &[AffineTransform],
624 el_ops: &[AffineTransform],
625 x: f64,
626 y: f64,
627 ) -> (f64, f64) {
628 let mut x = x;
632 let mut y = y;
633 for op in el_ops.iter().rev() {
634 let ox = (op.a * x + op.c * y) + op.e;
635 let oy = (op.b * x + op.d * y) + op.f;
636 x = ox;
637 y = oy;
638 }
639 for op in cur_ops.iter().rev() {
640 let ox = (op.a * x + op.c * y) + op.e;
641 let oy = (op.b * x + op.d * y) + op.f;
642 x = ox;
643 y = oy;
644 }
645 let xf = x as f32;
646 let yf = y as f32;
647 (xf as f64, yf as f64)
648 }
649
650 fn apply_ops_point_f64_then_f32_fma(
651 cur_ops: &[AffineTransform],
652 el_ops: &[AffineTransform],
653 x: f64,
654 y: f64,
655 ) -> (f64, f64) {
656 let mut x = x;
662 let mut y = y;
663 for op in el_ops.iter().rev() {
664 let ox = op.a.mul_add(x, op.c.mul_add(y, op.e));
665 let oy = op.b.mul_add(x, op.d.mul_add(y, op.f));
666 x = ox;
667 y = oy;
668 }
669 for op in cur_ops.iter().rev() {
670 let ox = op.a.mul_add(x, op.c.mul_add(y, op.e));
671 let oy = op.b.mul_add(x, op.d.mul_add(y, op.f));
672 x = ox;
673 y = oy;
674 }
675 let xf = x as f32;
676 let yf = y as f32;
677 (xf as f64, yf as f64)
678 }
679
680 fn apply_ops_bounds(
681 cur_ops: &[AffineTransform],
682 el_ops: &[AffineTransform],
683 b: Bounds,
684 ) -> Bounds {
685 let (x0, y0) = apply_ops_point(cur_ops, el_ops, b.min_x, b.min_y);
686 let (x1, y1) = apply_ops_point(cur_ops, el_ops, b.min_x, b.max_y);
687 let (x2, y2) = apply_ops_point(cur_ops, el_ops, b.max_x, b.min_y);
688 let (x3, y3) = apply_ops_point(cur_ops, el_ops, b.max_x, b.max_y);
689 Bounds {
690 min_x: x0.min(x1).min(x2).min(x3),
691 min_y: y0.min(y1).min(y2).min(y3),
692 max_x: x0.max(x1).max(x2).max(x3),
693 max_y: y0.max(y1).max(y2).max(y3),
694 }
695 }
696
697 fn apply_ops_bounds_no_fma(
698 cur_ops: &[AffineTransform],
699 el_ops: &[AffineTransform],
700 b: Bounds,
701 ) -> Bounds {
702 let (x0, y0) = apply_ops_point_no_fma(cur_ops, el_ops, b.min_x, b.min_y);
703 let (x1, y1) = apply_ops_point_no_fma(cur_ops, el_ops, b.min_x, b.max_y);
704 let (x2, y2) = apply_ops_point_no_fma(cur_ops, el_ops, b.max_x, b.min_y);
705 let (x3, y3) = apply_ops_point_no_fma(cur_ops, el_ops, b.max_x, b.max_y);
706 Bounds {
707 min_x: x0.min(x1).min(x2).min(x3),
708 min_y: y0.min(y1).min(y2).min(y3),
709 max_x: x0.max(x1).max(x2).max(x3),
710 max_y: y0.max(y1).max(y2).max(y3),
711 }
712 }
713
714 fn apply_ops_bounds_f64_then_f32(
715 cur_ops: &[AffineTransform],
716 el_ops: &[AffineTransform],
717 b: Bounds,
718 ) -> Bounds {
719 let (x0, y0) = apply_ops_point_f64_then_f32(cur_ops, el_ops, b.min_x, b.min_y);
720 let (x1, y1) = apply_ops_point_f64_then_f32(cur_ops, el_ops, b.min_x, b.max_y);
721 let (x2, y2) = apply_ops_point_f64_then_f32(cur_ops, el_ops, b.max_x, b.min_y);
722 let (x3, y3) = apply_ops_point_f64_then_f32(cur_ops, el_ops, b.max_x, b.max_y);
723 Bounds {
724 min_x: x0.min(x1).min(x2).min(x3),
725 min_y: y0.min(y1).min(y2).min(y3),
726 max_x: x0.max(x1).max(x2).max(x3),
727 max_y: y0.max(y1).max(y2).max(y3),
728 }
729 }
730
731 fn apply_ops_bounds_f64_then_f32_fma(
732 cur_ops: &[AffineTransform],
733 el_ops: &[AffineTransform],
734 b: Bounds,
735 ) -> Bounds {
736 let (x0, y0) = apply_ops_point_f64_then_f32_fma(cur_ops, el_ops, b.min_x, b.min_y);
737 let (x1, y1) = apply_ops_point_f64_then_f32_fma(cur_ops, el_ops, b.min_x, b.max_y);
738 let (x2, y2) = apply_ops_point_f64_then_f32_fma(cur_ops, el_ops, b.max_x, b.min_y);
739 let (x3, y3) = apply_ops_point_f64_then_f32_fma(cur_ops, el_ops, b.max_x, b.max_y);
740 Bounds {
741 min_x: x0.min(x1).min(x2).min(x3),
742 min_y: y0.min(y1).min(y2).min(y3),
743 max_x: x0.max(x1).max(x2).max(x3),
744 max_y: y0.max(y1).max(y2).max(y3),
745 }
746 }
747
748 fn has_non_axis_aligned_ops(cur_ops: &[AffineTransform], el_ops: &[AffineTransform]) -> bool {
749 cur_ops
750 .iter()
751 .chain(el_ops.iter())
752 .any(|t| t.b.abs() > 1e-12 || t.c.abs() > 1e-12)
753 }
754
755 fn has_pivot_baked_ops(cur_ops: &[AffineTransform], el_ops: &[AffineTransform]) -> bool {
756 cur_ops.iter().chain(el_ops.iter()).any(|t| {
759 (t.b.abs() > 1e-12 || t.c.abs() > 1e-12) && (t.e.abs() > 1e-12 || t.f.abs() > 1e-12)
760 })
761 }
762
763 fn is_translate_op(t: &AffineTransform) -> bool {
764 t.a == 1.0 && t.b == 0.0 && t.c == 0.0 && t.d == 1.0
765 }
766
767 fn is_rotate_like_op(t: &AffineTransform) -> bool {
768 (t.b.abs() > 1e-12 || t.c.abs() > 1e-12) && t.e.abs() <= 1e-12 && t.f.abs() <= 1e-12
770 }
771
772 fn is_near_integer(v: f64) -> bool {
773 (v - v.round()).abs() <= 1e-9
774 }
775
776 fn translate_params_quantized_to_0_01(t: &AffineTransform) -> bool {
777 if !is_translate_op(t) {
778 return false;
779 }
780 is_near_integer(t.e * 100.0) && is_near_integer(t.f * 100.0)
784 }
785
786 fn has_translate_quantized_to_0_01(
787 cur_ops: &[AffineTransform],
788 el_ops: &[AffineTransform],
789 ) -> bool {
790 cur_ops
791 .iter()
792 .chain(el_ops.iter())
793 .any(translate_params_quantized_to_0_01)
794 }
795
796 fn has_translate_close(
797 cur_ops: &[AffineTransform],
798 el_ops: &[AffineTransform],
799 ex: f64,
800 fy: f64,
801 ) -> bool {
802 cur_ops
803 .iter()
804 .chain(el_ops.iter())
805 .filter(|t| is_translate_op(t))
806 .any(|t| (t.e - ex).abs() <= 1e-6 && (t.f - fy).abs() <= 1e-6)
807 }
808
809 fn pivot_from_baked_rotate_op(t: &AffineTransform) -> Option<(f64, f64)> {
810 let cos = t.a;
815 let sin = t.b;
816 let k = 1.0 - cos;
817 let det = k.mul_add(k, sin * sin);
818 if det.abs() <= 1e-12 {
819 return None;
820 }
821 let cx = (k.mul_add(t.e, -sin * t.f)) / det;
822 let cy = (sin.mul_add(t.e, k * t.f)) / det;
823 Some((cx, cy))
824 }
825
826 fn has_pivot_cy_close(
827 cur_ops: &[AffineTransform],
828 el_ops: &[AffineTransform],
829 target_cy: f64,
830 ) -> bool {
831 cur_ops
832 .iter()
833 .chain(el_ops.iter())
834 .filter(|t| {
835 (t.b.abs() > 1e-12 || t.c.abs() > 1e-12) && (t.e.abs() > 1e-12 || t.f.abs() > 1e-12)
836 })
837 .filter_map(pivot_from_baked_rotate_op)
838 .any(|(_cx, cy)| (cy - target_cy).abs() <= 1.0)
839 }
840
841 fn has_pivot_close(
842 cur_ops: &[AffineTransform],
843 el_ops: &[AffineTransform],
844 target_cx: f64,
845 target_cy: f64,
846 ) -> bool {
847 cur_ops
848 .iter()
849 .chain(el_ops.iter())
850 .filter(|t| {
851 (t.b.abs() > 1e-12 || t.c.abs() > 1e-12) && (t.e.abs() > 1e-12 || t.f.abs() > 1e-12)
852 })
853 .filter_map(pivot_from_baked_rotate_op)
854 .any(|(cx, cy)| (cx - target_cx).abs() <= 1e-3 && (cy - target_cy).abs() <= 1e-3)
855 }
856
857 fn has_decomposed_pivot_ops(cur_ops: &[AffineTransform], el_ops: &[AffineTransform]) -> bool {
858 let ops: Vec<AffineTransform> = cur_ops.iter().chain(el_ops.iter()).copied().collect();
862 for w in ops.windows(3) {
863 let t0 = &w[0];
864 let r = &w[1];
865 let t1 = &w[2];
866 if !is_translate_op(t0) || !is_rotate_like_op(r) || !is_translate_op(t1) {
867 continue;
868 }
869 if t1.e == -t0.e && t1.f == -t0.f {
870 return true;
871 }
872 }
873 false
874 }
875
876 let mut defs_depth: usize = 0;
879 let mut tf_stack: Vec<usize> = Vec::new();
880 let mut cur_ops: Vec<AffineTransform> = Vec::new();
881 let mut el_ops_buf: Vec<AffineTransform> = Vec::new();
882 let mut seen_root_svg = false;
883 let mut nested_svg_depth = 0usize;
884
885 let mut i = 0usize;
886 while i < svg.len() {
887 let Some(rel) = svg[i..].find('<') else {
888 break;
889 };
890 i += rel;
891
892 if svg[i..].starts_with("<!--") {
894 if let Some(end_rel) = svg[i + 4..].find("-->") {
895 i = i + 4 + end_rel + 3;
896 continue;
897 }
898 break;
899 }
900
901 if svg[i..].starts_with("<?") {
903 if let Some(end_rel) = svg[i + 2..].find("?>") {
904 i = i + 2 + end_rel + 2;
905 continue;
906 }
907 break;
908 }
909
910 let close = svg[i..].starts_with("</");
911 let tag_start = if close { i + 2 } else { i + 1 };
912 let Some(tag_end_rel) =
913 svg[tag_start..].find(|c: char| c == '>' || c.is_whitespace() || c == '/')
914 else {
915 break;
916 };
917 let tag = &svg[tag_start..tag_start + tag_end_rel];
918
919 let Some(gt_rel) = svg[tag_start + tag_end_rel..].find('>') else {
921 break;
922 };
923 let gt = tag_start + tag_end_rel + gt_rel;
924 let raw = &svg[i..=gt];
925 let self_closing = raw.ends_with("/>");
926
927 if close {
928 match tag {
929 "defs" | "marker" | "symbol" | "clipPath" | "mask" | "pattern"
930 | "linearGradient" | "radialGradient" => {
931 defs_depth = defs_depth.saturating_sub(1);
932 }
933 "g" | "a" => {
934 if let Some(len) = tf_stack.pop() {
935 cur_ops.truncate(len);
936 } else {
937 cur_ops.clear();
938 }
939 }
940 "svg" => {
941 if nested_svg_depth > 0 {
942 nested_svg_depth -= 1;
943 if let Some(len) = tf_stack.pop() {
944 cur_ops.truncate(len);
945 } else {
946 cur_ops.clear();
947 }
948 }
949 }
950 _ => {}
951 }
952 i = gt + 1;
953 continue;
954 }
955
956 let attrs_start = tag_start + tag_end_rel;
958 let attrs_end = if self_closing {
959 gt.saturating_sub(1)
960 } else {
961 gt
962 };
963 let attrs = if attrs_start < attrs_end {
964 &svg[attrs_start..attrs_end]
965 } else {
966 ""
967 };
968
969 if matches!(
970 tag,
971 "defs"
972 | "marker"
973 | "symbol"
974 | "clipPath"
975 | "mask"
976 | "pattern"
977 | "linearGradient"
978 | "radialGradient"
979 ) {
980 defs_depth += 1;
981 }
982
983 el_ops_buf.clear();
984 if let Some(transform) = attr_value(attrs, "transform") {
985 parse_transform_ops_into(transform, &mut el_ops_buf);
986 }
987 let el_ops: &[AffineTransform] = &el_ops_buf;
988 let tf_kind = if has_non_axis_aligned_ops(&cur_ops, el_ops) {
989 if has_pivot_baked_ops(&cur_ops, el_ops) {
990 ExtremaKind::RotatedPivot
991 } else if has_decomposed_pivot_ops(&cur_ops, el_ops) {
992 ExtremaKind::RotatedDecomposedPivot
993 } else {
994 ExtremaKind::Rotated
995 }
996 } else {
997 ExtremaKind::Exact
998 };
999
1000 if tag == "g" || tag == "a" {
1001 tf_stack.push(cur_ops.len());
1002 cur_ops.extend_from_slice(el_ops);
1003 if self_closing {
1004 if let Some(len) = tf_stack.pop() {
1006 cur_ops.truncate(len);
1007 } else {
1008 cur_ops.clear();
1009 }
1010 }
1011 i = gt + 1;
1012 continue;
1013 }
1014
1015 if tag == "svg" {
1016 if !seen_root_svg {
1017 seen_root_svg = true;
1020 } else {
1021 tf_stack.push(cur_ops.len());
1022 nested_svg_depth += 1;
1023 let vp_tf = svg_viewport_transform(attrs);
1024 cur_ops.extend_from_slice(el_ops);
1025 cur_ops.push(vp_tf);
1026 if self_closing {
1027 nested_svg_depth = nested_svg_depth.saturating_sub(1);
1028 if let Some(len) = tf_stack.pop() {
1029 cur_ops.truncate(len);
1030 } else {
1031 cur_ops.clear();
1032 }
1033 }
1034 }
1035 i = gt + 1;
1036 continue;
1037 }
1038
1039 if defs_depth == 0 {
1040 match tag {
1041 "rect" => {
1042 let x = attr_value(attrs, "x").and_then(parse_f64).unwrap_or(0.0);
1043 let y = attr_value(attrs, "y").and_then(parse_f64).unwrap_or(0.0);
1044 let w = attr_value(attrs, "width")
1045 .and_then(parse_f64)
1046 .unwrap_or(0.0);
1047 let h = attr_value(attrs, "height")
1048 .and_then(parse_f64)
1049 .unwrap_or(0.0);
1050 let mut b = apply_ops_bounds(
1051 &cur_ops,
1052 el_ops,
1053 Bounds {
1054 min_x: x,
1055 min_y: y,
1056 max_x: x + w,
1057 max_y: y + h,
1058 },
1059 );
1060
1061 let allow_alt_max_y = tf_kind == ExtremaKind::Rotated
1066 || tf_kind == ExtremaKind::RotatedDecomposedPivot
1067 || (tf_kind == ExtremaKind::RotatedPivot
1068 && has_translate_quantized_to_0_01(&cur_ops, el_ops));
1069 if allow_alt_max_y {
1070 let base = Bounds {
1071 min_x: x,
1072 min_y: y,
1073 max_x: x + w,
1074 max_y: y + h,
1075 };
1076 let b_alt = apply_ops_bounds_f64_then_f32(
1077 &cur_ops,
1078 el_ops,
1079 Bounds {
1080 min_x: x,
1081 min_y: y,
1082 max_x: x + w,
1083 max_y: y + h,
1084 },
1085 );
1086 let b_alt_fma =
1087 apply_ops_bounds_f64_then_f32_fma(&cur_ops, el_ops, base.clone());
1088 let mut alt_max_y = b_alt.max_y.max(b_alt_fma.max_y);
1089
1090 if tf_kind == ExtremaKind::RotatedPivot
1091 && has_translate_quantized_to_0_01(&cur_ops, el_ops)
1092 && has_pivot_cy_close(&cur_ops, el_ops, 90.0)
1093 {
1094 let b_no_fma = apply_ops_bounds_no_fma(&cur_ops, el_ops, base);
1095 alt_max_y = alt_max_y.max(b_no_fma.max_y);
1096 }
1097 if alt_max_y > b.max_y {
1098 b.max_y = alt_max_y;
1099 }
1100 }
1101
1102 if tf_kind == ExtremaKind::RotatedPivot
1103 && has_translate_close(&cur_ops, el_ops, -14.34, 12.72)
1104 && has_pivot_close(&cur_ops, el_ops, 50.0, 90.0)
1105 {
1106 b.max_y += 1e-9;
1111 }
1112 if w != 0.0 || h != 0.0 {
1113 maybe_record_dbg(&mut dbg, tag, attrs, b.clone());
1114 }
1115 include_rect_inexact(
1116 &mut bounds,
1117 &mut extrema_kinds,
1118 b.min_x,
1119 b.min_y,
1120 b.max_x,
1121 b.max_y,
1122 tf_kind,
1123 );
1124 }
1125 "circle" => {
1126 let cx = attr_value(attrs, "cx").and_then(parse_f64).unwrap_or(0.0);
1127 let cy = attr_value(attrs, "cy").and_then(parse_f64).unwrap_or(0.0);
1128 let r = attr_value(attrs, "r").and_then(parse_f64).unwrap_or(0.0);
1129 let b = apply_ops_bounds(
1130 &cur_ops,
1131 el_ops,
1132 Bounds {
1133 min_x: cx - r,
1134 min_y: cy - r,
1135 max_x: cx + r,
1136 max_y: cy + r,
1137 },
1138 );
1139 if r != 0.0 {
1140 maybe_record_dbg(&mut dbg, tag, attrs, b.clone());
1141 }
1142 include_rect_inexact(
1143 &mut bounds,
1144 &mut extrema_kinds,
1145 b.min_x,
1146 b.min_y,
1147 b.max_x,
1148 b.max_y,
1149 tf_kind,
1150 );
1151 }
1152 "ellipse" => {
1153 let cx = attr_value(attrs, "cx").and_then(parse_f64).unwrap_or(0.0);
1154 let cy = attr_value(attrs, "cy").and_then(parse_f64).unwrap_or(0.0);
1155 let rx = attr_value(attrs, "rx").and_then(parse_f64).unwrap_or(0.0);
1156 let ry = attr_value(attrs, "ry").and_then(parse_f64).unwrap_or(0.0);
1157 let b = apply_ops_bounds(
1158 &cur_ops,
1159 el_ops,
1160 Bounds {
1161 min_x: cx - rx,
1162 min_y: cy - ry,
1163 max_x: cx + rx,
1164 max_y: cy + ry,
1165 },
1166 );
1167 if rx != 0.0 || ry != 0.0 {
1168 maybe_record_dbg(&mut dbg, tag, attrs, b.clone());
1169 }
1170 include_rect_inexact(
1171 &mut bounds,
1172 &mut extrema_kinds,
1173 b.min_x,
1174 b.min_y,
1175 b.max_x,
1176 b.max_y,
1177 tf_kind,
1178 );
1179 }
1180 "line" => {
1181 let x1 = attr_value(attrs, "x1").and_then(parse_f64).unwrap_or(0.0);
1182 let y1 = attr_value(attrs, "y1").and_then(parse_f64).unwrap_or(0.0);
1183 let x2 = attr_value(attrs, "x2").and_then(parse_f64).unwrap_or(0.0);
1184 let y2 = attr_value(attrs, "y2").and_then(parse_f64).unwrap_or(0.0);
1185 let (tx1, ty1) = apply_ops_point(&cur_ops, el_ops, x1, y1);
1186 let (tx2, ty2) = apply_ops_point(&cur_ops, el_ops, x2, y2);
1187 let b = Bounds {
1188 min_x: tx1.min(tx2),
1189 min_y: ty1.min(ty2),
1190 max_x: tx1.max(tx2),
1191 max_y: ty1.max(ty2),
1192 };
1193 maybe_record_dbg(&mut dbg, tag, attrs, b.clone());
1194 include_rect_inexact(
1195 &mut bounds,
1196 &mut extrema_kinds,
1197 b.min_x,
1198 b.min_y,
1199 b.max_x,
1200 b.max_y,
1201 tf_kind,
1202 );
1203 }
1204 "path" => {
1205 if let Some(d) = attr_value(attrs, "d") {
1206 if let Some(pb) = svg_path_bounds_from_d(d) {
1207 let b0 = apply_ops_bounds(
1208 &cur_ops,
1209 el_ops,
1210 Bounds {
1211 min_x: pb.min_x,
1212 min_y: pb.min_y,
1213 max_x: pb.max_x,
1214 max_y: pb.max_y,
1215 },
1216 );
1217 maybe_record_dbg(&mut dbg, tag, attrs, b0.clone());
1218 include_rect_inexact(
1219 &mut bounds,
1220 &mut extrema_kinds,
1221 b0.min_x,
1222 b0.min_y,
1223 b0.max_x,
1224 b0.max_y,
1225 ExtremaKind::Path,
1226 );
1227 } else {
1228 include_path_d(&mut bounds, &mut extrema_kinds, d, &cur_ops, el_ops);
1229 }
1230 }
1231 }
1232 "polygon" | "polyline" => {
1233 if let Some(pts) = attr_value(attrs, "points") {
1234 include_points(
1235 &mut bounds,
1236 &mut extrema_kinds,
1237 pts,
1238 &cur_ops,
1239 el_ops,
1240 tf_kind,
1241 );
1242 }
1243 }
1244 "foreignObject" => {
1245 let x = attr_value(attrs, "x").and_then(parse_f64).unwrap_or(0.0);
1246 let y = attr_value(attrs, "y").and_then(parse_f64).unwrap_or(0.0);
1247 let w = attr_value(attrs, "width")
1248 .and_then(parse_f64)
1249 .unwrap_or(0.0);
1250 let h = attr_value(attrs, "height")
1251 .and_then(parse_f64)
1252 .unwrap_or(0.0);
1253 let b = apply_ops_bounds(
1254 &cur_ops,
1255 el_ops,
1256 Bounds {
1257 min_x: x,
1258 min_y: y,
1259 max_x: x + w,
1260 max_y: y + h,
1261 },
1262 );
1263 if w != 0.0 || h != 0.0 {
1264 maybe_record_dbg(&mut dbg, tag, attrs, b.clone());
1265 }
1266 include_rect_inexact(
1267 &mut bounds,
1268 &mut extrema_kinds,
1269 b.min_x,
1270 b.min_y,
1271 b.max_x,
1272 b.max_y,
1273 tf_kind,
1274 );
1275 }
1276 _ => {}
1277 }
1278 }
1279
1280 i = gt + 1;
1281 }
1282
1283 bounds
1284}
1285
1286#[cfg(test)]
1287mod svg_bbox_tests {
1288 use super::*;
1289
1290 fn parse_root_viewbox(svg: &str) -> Option<(f64, f64, f64, f64)> {
1291 let start = svg.find("viewBox=\"")? + "viewBox=\"".len();
1292 let rest = &svg[start..];
1293 let end = rest.find('"')?;
1294 let raw = &rest[..end];
1295 let nums: Vec<f64> = raw
1296 .split_whitespace()
1297 .filter_map(|v| v.parse::<f64>().ok())
1298 .collect();
1299 if nums.len() != 4 {
1300 return None;
1301 }
1302 Some((nums[0], nums[1], nums[2], nums[3]))
1303 }
1304
1305 #[test]
1306 fn svg_bbox_matches_upstream_state_concurrent_viewbox() {
1307 let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
1308 "../../fixtures/upstream-svgs/state/upstream_stateDiagram_concurrent_state_spec.svg",
1309 );
1310 let svg = std::fs::read_to_string(p).expect("read upstream state svg");
1311
1312 let (vb_x, vb_y, vb_w, vb_h) = parse_root_viewbox(&svg).expect("parse viewBox");
1313 let b = svg_emitted_bounds_from_svg(&svg).expect("bbox");
1314
1315 let pad = 8.0;
1316 let got_x = b.min_x - pad;
1317 let got_y = b.min_y - pad;
1318 let got_w = (b.max_x - b.min_x) + 2.0 * pad;
1319 let got_h = (b.max_y - b.min_y) + 2.0 * pad;
1320
1321 fn close(a: f64, b: f64) -> bool {
1322 (a - b).abs() <= 1e-6
1323 }
1324
1325 assert!(close(got_x, vb_x), "viewBox x: got {got_x}, want {vb_x}");
1326 assert!(close(got_y, vb_y), "viewBox y: got {got_y}, want {vb_y}");
1327 assert!(close(got_w, vb_w), "viewBox w: got {got_w}, want {vb_w}");
1328 assert!(close(got_h, vb_h), "viewBox h: got {got_h}, want {vb_h}");
1329 }
1330
1331 #[test]
1332 fn svg_path_bounds_architecture_service_node_bkg_matches_mermaid_bbox() {
1333 let d = "M0 80 v-80 q0,-5 5,-5 h80 q5,0 5,5 v80 H0 Z";
1341 let b = svg_path_bounds_from_d(d).expect("path bounds");
1342 assert!((b.min_x - 0.0).abs() < 1e-9, "min_x: got {}", b.min_x);
1343 assert!((b.min_y - (-5.0)).abs() < 1e-9, "min_y: got {}", b.min_y);
1344 assert!((b.max_x - 90.0).abs() < 1e-9, "max_x: got {}", b.max_x);
1345 assert!((b.max_y - 80.0).abs() < 1e-9, "max_y: got {}", b.max_y);
1346 }
1347
1348 #[test]
1349 fn svg_emitted_bounds_attr_lookup_d_does_not_match_id() {
1350 let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><path class="node-bkg" id="node-db" d="M0 80 v-80 q0,-5 5,-5 h80 q5,0 5,5 v80 H0 Z"/></svg>"#;
1353 let dbg = debug_svg_emitted_bounds(svg).expect("emitted bounds");
1354 assert!((dbg.bounds.min_x - 0.0).abs() < 1e-9);
1355 assert!((dbg.bounds.min_y - (-5.0)).abs() < 1e-9);
1356 assert!((dbg.bounds.max_x - 90.0).abs() < 1e-9);
1357 assert!((dbg.bounds.max_y - 80.0).abs() < 1e-9);
1358 }
1359
1360 #[test]
1361 fn svg_emitted_bounds_supports_rotate_transform() {
1362 let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="10" height="20" transform="rotate(90)"/></svg>"#;
1363 let dbg = debug_svg_emitted_bounds(svg).expect("emitted bounds");
1364 assert!(
1365 (dbg.bounds.min_x - (-20.0)).abs() < 1e-9,
1366 "min_x: {}",
1367 dbg.bounds.min_x
1368 );
1369 assert!(
1370 (dbg.bounds.min_y - 0.0).abs() < 1e-9,
1371 "min_y: {}",
1372 dbg.bounds.min_y
1373 );
1374 assert!(
1375 (dbg.bounds.max_x - 0.0).abs() < 1e-9,
1376 "max_x: {}",
1377 dbg.bounds.max_x
1378 );
1379 assert!(
1380 (dbg.bounds.max_y - 10.0).abs() < 1e-9,
1381 "max_y: {}",
1382 dbg.bounds.max_y
1383 );
1384 }
1385
1386 #[test]
1387 fn svg_emitted_bounds_supports_rotate_about_center() {
1388 let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="10" height="20" transform="rotate(90, 5, 10)"/></svg>"#;
1389 let dbg = debug_svg_emitted_bounds(svg).expect("emitted bounds");
1390 assert!(
1391 (dbg.bounds.min_x - (-5.0)).abs() < 1e-9,
1392 "min_x: {}",
1393 dbg.bounds.min_x
1394 );
1395 assert!(
1396 (dbg.bounds.min_y - 5.0).abs() < 1e-9,
1397 "min_y: {}",
1398 dbg.bounds.min_y
1399 );
1400 assert!(
1401 (dbg.bounds.max_x - 15.0).abs() < 1e-9,
1402 "max_x: {}",
1403 dbg.bounds.max_x
1404 );
1405 assert!(
1406 (dbg.bounds.max_y - 15.0).abs() < 1e-9,
1407 "max_y: {}",
1408 dbg.bounds.max_y
1409 );
1410 }
1411}