Skip to main content

blitz_dom/layout/
svg_geometry.rs

1use std::collections::{HashMap, HashSet};
2
3use blitz_traits::events::HitResult;
4use kurbo::{Affine, Point, Rect};
5use usvg::svgtypes::{Align, AspectRatio, Length, LengthUnit, Transform};
6
7use crate::document::BoundingRect;
8use crate::{BaseDocument, Node, NodeId, local_name};
9
10const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg";
11
12pub(crate) fn is_svg(node: &Node) -> bool {
13    node.element_data()
14        .is_some_and(|element| element.name.ns.as_ref() == SVG_NAMESPACE)
15}
16
17fn is_graphics(node: &Node) -> bool {
18    is_svg(node)
19        && node.element_data().is_some_and(|element| {
20            matches!(
21                element.name.local.as_ref(),
22                "svg"
23                    | "g"
24                    | "a"
25                    | "path"
26                    | "rect"
27                    | "circle"
28                    | "ellipse"
29                    | "line"
30                    | "polyline"
31                    | "polygon"
32                    | "text"
33                    | "use"
34                    | "image"
35                    | "foreignObject"
36                    | "foreignobject"
37            )
38        })
39}
40
41fn attr<'a>(node: &'a Node, name: &str) -> Option<&'a str> {
42    node.element_data()?
43        .attrs()
44        .iter()
45        .find(|attribute| {
46            attribute.name.ns.as_ref().is_empty()
47                && attribute.name.local.as_ref().eq_ignore_ascii_case(name)
48        })
49        .map(|attribute| attribute.value.as_ref())
50}
51
52/// IDs exist only in the image source. Existing IDs and fragment references
53/// retain their values, and the live DOM is never mutated.
54pub(crate) fn serialization_ids(doc: &BaseDocument, root: NodeId) -> HashMap<NodeId, String> {
55    let mut used: HashSet<String> = doc
56        .nodes
57        .iter()
58        .filter_map(|(_, node)| node.attr(local_name!("id")))
59        .map(str::to_owned)
60        .collect();
61    let mut ids = HashMap::new();
62    let mut pending = vec![root];
63
64    while let Some(id) = pending.pop() {
65        let node = &doc.nodes[id];
66        if is_graphics(node) {
67            let source_id = match node.attr(local_name!("id")).filter(|id| !id.is_empty()) {
68                Some(id) => id.to_owned(),
69                None => {
70                    let mut generated =
71                        format!("blitz-svg-geometry-{}-{}", root.as_u64(), id.as_u64());
72                    while !used.insert(generated.clone()) {
73                        generated.push('_');
74                    }
75                    generated
76                }
77            };
78            ids.insert(id, source_id);
79        }
80        pending.extend(node.children.iter().rev().copied());
81    }
82
83    ids
84}
85
86fn rect(rect: usvg::Rect) -> Rect {
87    Rect::new(
88        f64::from(rect.x()),
89        f64::from(rect.y()),
90        f64::from(rect.right()),
91        f64::from(rect.bottom()),
92    )
93}
94
95fn bounding_rect(rect: Rect) -> BoundingRect {
96    BoundingRect {
97        x: rect.x0,
98        y: rect.y0,
99        width: rect.width(),
100        height: rect.height(),
101    }
102}
103
104fn union(bounds: &mut HashMap<NodeId, Rect>, id: NodeId, rect: Rect) {
105    bounds
106        .entry(id)
107        .and_modify(|old| *old = old.union(rect))
108        .or_insert(rect);
109}
110
111/// Immutable geometry travels with the parsed image. Only the viewport
112/// transform depends on current layout, so resizing and scrolling do not
113/// require parsing the SVG again.
114#[derive(Debug)]
115pub(crate) struct SvgGeometry {
116    bounds: HashMap<NodeId, Rect>,
117    bboxes: HashMap<NodeId, Rect>,
118    hits: Vec<(NodeId, Rect)>,
119    view_box: Option<Rect>,
120    aspect: AspectRatio,
121    user_to_tree: Affine,
122}
123
124#[derive(Clone, Copy, Default)]
125struct SvgOwnership {
126    owner: Option<NodeId>,
127    in_use: bool,
128}
129
130impl SvgGeometry {
131    pub(crate) fn build(
132        doc: &BaseDocument,
133        root: NodeId,
134        tree: &usvg::Tree,
135        ids: &HashMap<NodeId, String>,
136    ) -> Self {
137        let view_box = tree.intrinsic_dimensions().view_box.map(|view_box| {
138            Rect::new(
139                f64::from(view_box.x()),
140                f64::from(view_box.y()),
141                f64::from(view_box.x() + view_box.width()),
142                f64::from(view_box.y() + view_box.height()),
143            )
144        });
145        let aspect = attr(&doc.nodes[root], "preserveAspectRatio")
146            .and_then(|value| value.parse().ok())
147            .unwrap_or_default();
148        let user_to_tree = view_box
149            .map(|view_box| {
150                viewport_transform(
151                    view_box,
152                    aspect,
153                    f64::from(tree.size().width()),
154                    f64::from(tree.size().height()),
155                )
156            })
157            .unwrap_or(Affine::IDENTITY);
158        let mut geometry = Self {
159            bounds: HashMap::new(),
160            bboxes: HashMap::new(),
161            hits: Vec::new(),
162            view_box,
163            aspect,
164            user_to_tree,
165        };
166        let by_id: HashMap<&str, NodeId> = ids
167            .iter()
168            .map(|(id, source_id)| (source_id.as_str(), *id))
169            .collect();
170        let mut hit_bounds = HashMap::new();
171
172        for child in tree.root().children() {
173            geometry.collect(
174                doc,
175                root,
176                child,
177                &by_id,
178                SvgOwnership::default(),
179                &mut hit_bounds,
180            );
181        }
182        geometry.bboxes.insert(
183            root,
184            user_to_tree
185                .inverse()
186                .transform_rect_bbox(rect(tree.root().abs_bounding_box())),
187        );
188
189        // usvg does not render foreignObject contents. Its SVG viewport box
190        // still has geometry, without introducing a synthetic painted shape.
191        let mut pending = vec![root];
192        while let Some(id) = pending.pop() {
193            let node = &doc.nodes[id];
194            if is_svg(node)
195                && node.element_data().is_some_and(|element| {
196                    element
197                        .name
198                        .local
199                        .as_ref()
200                        .eq_ignore_ascii_case("foreignObject")
201                })
202                && let Some(box_) = foreign_object_box(node, view_box, tree.size())
203            {
204                geometry.bboxes.insert(id, box_);
205                let transform = user_to_tree * element_transform(doc, root, id);
206                let painted = transform.transform_rect_bbox(box_);
207                geometry.record(doc, root, id, painted, &mut hit_bounds);
208            }
209
210            // DOM order is paint order for the authored SVG elements.
211            // Definition instances are attributed to their <use>, not to the
212            // original definition's DOM nodes.
213            if let Some(bounds) = hit_bounds.get(&id) {
214                geometry.hits.push((id, *bounds));
215            }
216            pending.extend(node.children.iter().rev().copied());
217        }
218
219        geometry
220    }
221
222    fn collect(
223        &mut self,
224        doc: &BaseDocument,
225        root: NodeId,
226        node: &usvg::Node,
227        ids: &HashMap<&str, NodeId>,
228        ownership: SvgOwnership,
229        hit_bounds: &mut HashMap<NodeId, Rect>,
230    ) {
231        let mapped = (!ownership.in_use)
232            .then(|| ids.get(node.id()).copied())
233            .flatten();
234        let owner = mapped.or(ownership.owner);
235
236        if let Some(id) = mapped {
237            union(&mut self.bboxes, id, rect(node.bounding_box()));
238        }
239
240        match node {
241            usvg::Node::Group(group) => {
242                if group.opacity().get() == 0.0 {
243                    return;
244                }
245                let ownership = SvgOwnership {
246                    owner,
247                    in_use: ownership.in_use
248                        || mapped.is_some_and(|id| {
249                            doc.nodes[id]
250                                .element_data()
251                                .is_some_and(|element| element.name.local == local_name!("use"))
252                        }),
253                };
254                for child in group.children() {
255                    self.collect(doc, root, child, ids, ownership, hit_bounds);
256                }
257            }
258            usvg::Node::Path(path) if !path.is_visible() => {}
259            usvg::Node::Image(image) if !image.is_visible() => {}
260            _ => {
261                if let Some(owner) = owner {
262                    self.record(
263                        doc,
264                        root,
265                        owner,
266                        rect(node.abs_stroke_bounding_box()),
267                        hit_bounds,
268                    );
269                }
270            }
271        }
272    }
273
274    fn record(
275        &mut self,
276        doc: &BaseDocument,
277        root: NodeId,
278        owner: NodeId,
279        bounds: Rect,
280        hit_bounds: &mut HashMap<NodeId, Rect>,
281    ) {
282        if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
283            return;
284        }
285        union(hit_bounds, owner, bounds);
286        let mut current = Some(owner);
287        while let Some(id) = current {
288            let node = &doc.nodes[id];
289            if is_graphics(node) {
290                union(&mut self.bounds, id, bounds);
291            }
292            if id == root {
293                break;
294            }
295            current = node.parent;
296        }
297    }
298
299    fn content_transform(&self, width: f64, height: f64) -> Affine {
300        self.view_box
301            .map(|view_box| {
302                viewport_transform(view_box, self.aspect, width, height)
303                    * self.user_to_tree.inverse()
304            })
305            .unwrap_or(Affine::IDENTITY)
306    }
307}
308
309fn viewport_transform(view_box: Rect, aspect: AspectRatio, width: f64, height: f64) -> Affine {
310    let mut sx = width / view_box.width();
311    let mut sy = height / view_box.height();
312    if aspect.align != Align::None {
313        let scale = if aspect.slice { sx.max(sy) } else { sx.min(sy) };
314        sx = scale;
315        sy = scale;
316    }
317    let horizontal = match aspect.align {
318        Align::XMidYMin | Align::XMidYMid | Align::XMidYMax => 0.5,
319        Align::XMaxYMin | Align::XMaxYMid | Align::XMaxYMax => 1.0,
320        _ => 0.0,
321    };
322    let vertical = match aspect.align {
323        Align::XMinYMid | Align::XMidYMid | Align::XMaxYMid => 0.5,
324        Align::XMinYMax | Align::XMidYMax | Align::XMaxYMax => 1.0,
325        _ => 0.0,
326    };
327    Affine::translate((
328        (width - view_box.width() * sx) * horizontal,
329        (height - view_box.height() * sy) * vertical,
330    )) * Affine::scale_non_uniform(sx, sy)
331        * Affine::translate((-view_box.x0, -view_box.y0))
332}
333
334fn element_transform(doc: &BaseDocument, root: NodeId, id: NodeId) -> Affine {
335    let mut transform = Affine::IDENTITY;
336    let mut current = Some(id);
337    while let Some(id) = current {
338        if id == root {
339            break;
340        }
341        let node = &doc.nodes[id];
342        if let Some(value) = attr(node, "transform")
343            && let Ok(value) = value.parse::<Transform>()
344        {
345            transform =
346                Affine::new([value.a, value.b, value.c, value.d, value.e, value.f]) * transform;
347        }
348        current = node.parent;
349    }
350    transform
351}
352
353fn foreign_object_box(node: &Node, view_box: Option<Rect>, size: usvg::Size) -> Option<Rect> {
354    let width = view_box.map_or(f64::from(size.width()), |rect| rect.width());
355    let height = view_box.map_or(f64::from(size.height()), |rect| rect.height());
356    let length = |name, reference, default| {
357        let Some(value) = attr(node, name) else {
358            return Some(default);
359        };
360        let value = value.parse::<Length>().ok()?;
361        let factor = match value.unit {
362            LengthUnit::None | LengthUnit::Px => 1.0,
363            LengthUnit::Percent => reference / 100.0,
364            LengthUnit::In => 96.0,
365            LengthUnit::Cm => 96.0 / 2.54,
366            LengthUnit::Mm => 96.0 / 25.4,
367            LengthUnit::Pt => 96.0 / 72.0,
368            LengthUnit::Pc => 16.0,
369            _ => return None,
370        };
371        Some(value.number * factor)
372    };
373    let x = length("x", width, 0.0)?;
374    let y = length("y", height, 0.0)?;
375    let width = length("width", width, 0.0)?;
376    let height = length("height", height, 0.0)?;
377    (width > 0.0 && height > 0.0).then(|| Rect::new(x, y, x + width, y + height))
378}
379
380impl Node {
381    /// Map parsed SVG canvas coordinates into this root's CSS border box.
382    /// Paint uses this same transform, scaled to device pixels.
383    pub fn svg_content_transform(&self) -> Option<Affine> {
384        let geometry = self.element_data()?.svg_geometry.as_ref()?;
385        let layout = self.final_layout();
386        let left = f64::from(layout.border.left + layout.padding.left);
387        let top = f64::from(layout.border.top + layout.padding.top);
388        let width = f64::from(layout.size.width)
389            - left
390            - f64::from(layout.border.right + layout.padding.right);
391        let height = f64::from(layout.size.height)
392            - top
393            - f64::from(layout.border.bottom + layout.padding.bottom);
394        if width <= 0.0 || height <= 0.0 {
395            return None;
396        }
397        Some(Affine::translate((left, top)) * geometry.content_transform(width, height))
398    }
399
400    pub(crate) fn hit_inline_svg(
401        &self,
402        x: f32,
403        y: f32,
404        hits: &mut Option<Vec<HitResult>>,
405    ) -> Option<HitResult> {
406        use style::computed_values::pointer_events::T as PointerEvents;
407        use style::computed_values::visibility::T as Visibility;
408
409        let geometry = self.element_data()?.svg_geometry.as_ref()?;
410        let transform = self.svg_content_transform()?;
411        let point = transform.inverse() * Point::new(f64::from(x), f64::from(y));
412        for (id, bounds) in geometry.hits.iter().rev() {
413            if !bounds.contains(point) {
414                continue;
415            }
416            let node = self.tree().get(*id)?;
417            if node.is_display_none()
418                || node.primary_styles().is_some_and(|style| {
419                    style.clone_pointer_events() == PointerEvents::None
420                        || matches!(
421                            style.clone_visibility(),
422                            Visibility::Hidden | Visibility::Collapse
423                        )
424                })
425            {
426                continue;
427            }
428            let bounds = transform.transform_rect_bbox(*bounds);
429            let hit = HitResult {
430                node_id: *id,
431                x: x - bounds.x0 as f32,
432                y: y - bounds.y0 as f32,
433                is_text: false,
434            };
435            if let Some(hits) = hits.as_mut() {
436                hits.push(hit);
437            } else {
438                return Some(hit);
439            }
440        }
441        None
442    }
443}
444
445impl BaseDocument {
446    pub(crate) fn svg_geometry_root(&self, id: NodeId) -> Option<NodeId> {
447        let node = self.get_node(id)?;
448        if !is_svg(node) {
449            return None;
450        }
451        let mut current = node.parent;
452        while let Some(id) = current {
453            let node = self.get_node(id)?;
454            if node
455                .element_data()
456                .is_some_and(|element| element.svg_geometry.is_some())
457            {
458                return Some(id);
459            }
460            current = node.parent;
461        }
462        None
463    }
464
465    pub(crate) fn svg_client_rect(&self, root: NodeId, id: NodeId) -> Option<BoundingRect> {
466        let root_node = self.get_node(root)?;
467        let geometry = root_node.element_data()?.svg_geometry.as_ref()?;
468        let bounds = *geometry.bounds.get(&id)?;
469        let mut current = Some(id);
470        while let Some(id) = current {
471            let node = self.get_node(id)?;
472            if node.is_element() && node.is_display_none() {
473                return None;
474            }
475            current = node.parent;
476        }
477        let bounds = root_node
478            .svg_content_transform()?
479            .transform_rect_bbox(bounds);
480        let origin = root_node.absolute_position(0.0, 0.0);
481        let bounds = bounding_rect(
482            bounds
483                + kurbo::Vec2::new(
484                    f64::from(origin.x) - self.viewport_scroll.x,
485                    f64::from(origin.y) - self.viewport_scroll.y,
486                ),
487        );
488        Some(self.transformed_client_rect(root, bounds))
489    }
490
491    pub(crate) fn svg_user_bbox(&self, id: NodeId) -> Option<BoundingRect> {
492        let node = self.get_node(id)?;
493        if !is_graphics(node) {
494            return None;
495        }
496        let root = if node
497            .element_data()
498            .is_some_and(|element| element.svg_geometry.is_some())
499        {
500            Some(id)
501        } else {
502            self.svg_geometry_root(id)
503        };
504        let bounds = root
505            .and_then(|root| self.get_node(root))
506            .and_then(Node::element_data)
507            .and_then(|element| element.svg_geometry.as_ref())
508            .and_then(|geometry| geometry.bboxes.get(&id))
509            .copied()
510            .unwrap_or(Rect::ZERO);
511        Some(bounding_rect(bounds))
512    }
513}