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
52pub(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#[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 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 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 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}