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