reflexo_vec2bbox/
lib.rs

1use std::collections::HashMap;
2
3use tiny_skia as sk;
4
5use reflexo::{hash::Fingerprint, vector::ir::*};
6
7#[derive(Default)]
8pub struct Vec2BBoxPass {
9    bbox_caches: HashMap<(Fingerprint, Transform), Option<Rect>>,
10}
11
12impl Vec2BBoxPass {
13    /// Calculate the bounding box of a vector item with a given transform.
14    /// The transform is required to calculate the accurate bounding box for
15    /// irregular shapes.
16    pub fn bbox_of(&mut self, module: &Module, v: Fingerprint, ts: Transform) -> Option<Rect> {
17        if let Some(bbox) = self.bbox_caches.get(&(v, ts)) {
18            return *bbox;
19        }
20
21        let bbox = self.bbox_of_(module, v, ts);
22        eprintln!("bbox_of({v:?}, {ts:?}) = {bbox:?}");
23        self.bbox_caches.insert((v, ts), bbox);
24        bbox
25    }
26
27    fn bbox_of_(&mut self, module: &Module, v: Fingerprint, ts: Transform) -> Option<Rect> {
28        let item = module.get_item(&v).unwrap();
29        match item {
30            VecItem::Item(item) => {
31                let ts_group: Transform = item.0.clone().into();
32                let ts = ts.pre_concat(ts_group);
33                self.bbox_of(module, item.1, ts)
34            }
35            VecItem::Labelled(item) => self.bbox_of(module, item.1, ts),
36            VecItem::Group(g) => {
37                let mut r = Rect::default();
38                for (p, f) in g.0.iter() {
39                    let sub_bbox = self.bbox_of(module, *f, ts);
40                    if let Some(sub_bbox) = sub_bbox {
41                        union(&mut r, *p, sub_bbox);
42                    }
43                }
44                Some(r)
45            }
46            VecItem::Image(ImageItem { size, .. })
47            | VecItem::Link(LinkItem { size, .. })
48            | VecItem::SizedRawHtml(SizedRawHtmlItem { size, .. }) => self.rect(*size, ts),
49            // todo: I'm writing this in my leg
50            VecItem::Text(t) => {
51                let width = t.width();
52                let height = t.shape.size.0;
53                tiny_skia_path::Rect::from_xywh(0.0, 0.0, width.0, height).map(|e| e.into())
54            }
55            VecItem::Path(p) => self.path(p, ts),
56            VecItem::ContentHint(..)
57            | VecItem::ColorTransform(..)
58            | VecItem::Pattern(..)
59            | VecItem::Gradient(..)
60            | VecItem::Color32(..)
61            | VecItem::Html(..)
62            | VecItem::None => None,
63        }
64    }
65
66    pub fn path(&mut self, p: &PathItem, ts: Transform) -> Option<Rect> {
67        Self::path_bbox(p, ts.into())
68    }
69
70    fn rect(&self, size: Axes<Scalar>, ts: Transform) -> Option<Rect> {
71        let r = tiny_skia_path::Rect::from_xywh(0.0, 0.0, size.x.0, size.y.0);
72        r.and_then(|e| e.transform(ts.into())).map(|e| e.into())
73    }
74
75    pub fn simple_path_bbox(p: &str, ts: sk::Transform) -> Option<Rect> {
76        let d = convert_path(p);
77        d.and_then(|e| e.transform(ts))
78            .and_then(|e| e.compute_tight_bounds())
79            .map(|e| e.into())
80    }
81
82    pub fn path_bbox(p: &PathItem, ts: sk::Transform) -> Option<Rect> {
83        let d = convert_path(&p.d);
84        d.and_then(|e| e.transform(ts))
85            .and_then(|e| e.compute_tight_bounds())
86            .and_then(|e| {
87                let Some(stroke) = p.styles.iter().find_map(|s| match s {
88                    PathStyle::StrokeWidth(w) => Some(w.0),
89                    _ => None,
90                }) else {
91                    return Some(e);
92                };
93                let sk::Transform { sx, sy, kx, ky, .. } = ts;
94                let stroke_x = (stroke * (sx + ky)).abs();
95                let stroke_y = (stroke * (sy + kx)).abs();
96                // extend the bounding box by the stroke width
97                let x = e.x() - stroke_x;
98                let y = e.y() - stroke_y;
99                let w = e.width() + stroke_x * 2.0;
100                let h = e.height() + stroke_y * 2.0;
101                tiny_skia_path::Rect::from_xywh(x, y, w, h)
102            })
103            .map(|e| e.into())
104    }
105}
106
107fn union(r: &mut Rect, p: Axes<Scalar>, sub_bbox: Rect) {
108    *r = r.union(&sub_bbox.translate(p))
109}
110
111fn convert_path(path_data: &str) -> Option<tiny_skia_path::Path> {
112    let mut builder = tiny_skia_path::PathBuilder::new();
113    for segment in svgtypes::SimplifyingPathParser::from(path_data) {
114        let segment = match segment {
115            Ok(v) => v,
116            Err(_) => break,
117        };
118
119        match segment {
120            svgtypes::SimplePathSegment::MoveTo { x, y } => {
121                builder.move_to(x as f32, y as f32);
122            }
123            svgtypes::SimplePathSegment::LineTo { x, y } => {
124                builder.line_to(x as f32, y as f32);
125            }
126            svgtypes::SimplePathSegment::Quadratic { x1, y1, x, y } => {
127                builder.quad_to(x1 as f32, y1 as f32, x as f32, y as f32);
128            }
129            svgtypes::SimplePathSegment::CurveTo {
130                x1,
131                y1,
132                x2,
133                y2,
134                x,
135                y,
136            } => {
137                builder.cubic_to(
138                    x1 as f32, y1 as f32, x2 as f32, y2 as f32, x as f32, y as f32,
139                );
140            }
141            svgtypes::SimplePathSegment::ClosePath => {
142                builder.close();
143            }
144        }
145    }
146
147    builder.finish()
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_path_bbox() {
156        let data = "M 0 0 M 0 4.8 C 0 2.1490333 2.1490333 0 4.8 0 L 975.2 0 C 977.85095 0 980 2.1490333 980 4.8 L 980 122.256 C 980 124.90697 977.85095 127.056 975.2 127.056 L 4.8 127.056 C 2.1490333 127.056 0 124.90697 0 122.256 Z ";
157
158        let p = PathItem {
159            d: data.into(),
160            size: None,
161            styles: vec![],
162        };
163
164        let ts = sk::Transform::from_scale(4.5, 4.5);
165
166        assert!(Vec2BBoxPass::path_bbox(&p, ts).is_some());
167    }
168}