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
124impl SvgGeometry {
125 pub(crate) fn build(
126 doc: &BaseDocument,
127 root: NodeId,
128 tree: &usvg::Tree,
129 ids: &HashMap<NodeId, String>,
130 ) -> Self {
131 let view_box = tree.intrinsic_dimensions().view_box.map(|view_box| {
132 Rect::new(
133 f64::from(view_box.x()),
134 f64::from(view_box.y()),
135 f64::from(view_box.x() + view_box.width()),
136 f64::from(view_box.y() + view_box.height()),
137 )
138 });
139 let aspect = attr(&doc.nodes[root], "preserveAspectRatio")
140 .and_then(|value| value.parse().ok())
141 .unwrap_or_default();
142 let user_to_tree = view_box
143 .map(|view_box| {
144 viewport_transform(
145 view_box,
146 aspect,
147 f64::from(tree.size().width()),
148 f64::from(tree.size().height()),
149 )
150 })
151 .unwrap_or(Affine::IDENTITY);
152 let mut geometry = Self {
153 bounds: HashMap::new(),
154 bboxes: HashMap::new(),
155 hits: Vec::new(),
156 view_box,
157 aspect,
158 user_to_tree,
159 };
160 let by_id: HashMap<&str, NodeId> = ids
161 .iter()
162 .map(|(id, source_id)| (source_id.as_str(), *id))
163 .collect();
164 let mut hit_bounds = HashMap::new();
165
166 for child in tree.root().children() {
167 geometry.collect(doc, root, child, &by_id, None, false, &mut hit_bounds);
168 }
169 geometry.bboxes.insert(
170 root,
171 user_to_tree
172 .inverse()
173 .transform_rect_bbox(rect(tree.root().abs_bounding_box())),
174 );
175
176 let mut pending = vec![root];
179 while let Some(id) = pending.pop() {
180 let node = &doc.nodes[id];
181 if is_svg(node)
182 && node.element_data().is_some_and(|element| {
183 element
184 .name
185 .local
186 .as_ref()
187 .eq_ignore_ascii_case("foreignObject")
188 })
189 && let Some(box_) = foreign_object_box(node, view_box, tree.size())
190 {
191 geometry.bboxes.insert(id, box_);
192 let transform = user_to_tree * element_transform(doc, root, id);
193 let painted = transform.transform_rect_bbox(box_);
194 geometry.record(doc, root, id, painted, &mut hit_bounds);
195 }
196
197 if let Some(bounds) = hit_bounds.get(&id) {
201 geometry.hits.push((id, *bounds));
202 }
203 pending.extend(node.children.iter().rev().copied());
204 }
205
206 geometry
207 }
208
209 fn collect(
210 &mut self,
211 doc: &BaseDocument,
212 root: NodeId,
213 node: &usvg::Node,
214 ids: &HashMap<&str, NodeId>,
215 inherited_owner: Option<NodeId>,
216 in_use: bool,
217 hit_bounds: &mut HashMap<NodeId, Rect>,
218 ) {
219 let mapped = (!in_use).then(|| ids.get(node.id()).copied()).flatten();
220 let owner = mapped.or(inherited_owner);
221
222 if let Some(id) = mapped {
223 union(&mut self.bboxes, id, rect(node.bounding_box()));
224 }
225
226 match node {
227 usvg::Node::Group(group) => {
228 if group.opacity().get() == 0.0 {
229 return;
230 }
231 let in_use = in_use
232 || mapped.is_some_and(|id| {
233 doc.nodes[id]
234 .element_data()
235 .is_some_and(|element| element.name.local == local_name!("use"))
236 });
237 for child in group.children() {
238 self.collect(doc, root, child, ids, owner, in_use, hit_bounds);
239 }
240 }
241 usvg::Node::Path(path) if !path.is_visible() => {}
242 usvg::Node::Image(image) if !image.is_visible() => {}
243 _ => {
244 if let Some(owner) = owner {
245 self.record(
246 doc,
247 root,
248 owner,
249 rect(node.abs_stroke_bounding_box()),
250 hit_bounds,
251 );
252 }
253 }
254 }
255 }
256
257 fn record(
258 &mut self,
259 doc: &BaseDocument,
260 root: NodeId,
261 owner: NodeId,
262 bounds: Rect,
263 hit_bounds: &mut HashMap<NodeId, Rect>,
264 ) {
265 if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
266 return;
267 }
268 union(hit_bounds, owner, bounds);
269 let mut current = Some(owner);
270 while let Some(id) = current {
271 let node = &doc.nodes[id];
272 if is_graphics(node) {
273 union(&mut self.bounds, id, bounds);
274 }
275 if id == root {
276 break;
277 }
278 current = node.parent;
279 }
280 }
281
282 fn content_transform(&self, width: f64, height: f64) -> Affine {
283 self.view_box
284 .map(|view_box| {
285 viewport_transform(view_box, self.aspect, width, height)
286 * self.user_to_tree.inverse()
287 })
288 .unwrap_or(Affine::IDENTITY)
289 }
290}
291
292fn viewport_transform(view_box: Rect, aspect: AspectRatio, width: f64, height: f64) -> Affine {
293 let mut sx = width / view_box.width();
294 let mut sy = height / view_box.height();
295 if aspect.align != Align::None {
296 let scale = if aspect.slice { sx.max(sy) } else { sx.min(sy) };
297 sx = scale;
298 sy = scale;
299 }
300 let horizontal = match aspect.align {
301 Align::XMidYMin | Align::XMidYMid | Align::XMidYMax => 0.5,
302 Align::XMaxYMin | Align::XMaxYMid | Align::XMaxYMax => 1.0,
303 _ => 0.0,
304 };
305 let vertical = match aspect.align {
306 Align::XMinYMid | Align::XMidYMid | Align::XMaxYMid => 0.5,
307 Align::XMinYMax | Align::XMidYMax | Align::XMaxYMax => 1.0,
308 _ => 0.0,
309 };
310 Affine::translate((
311 (width - view_box.width() * sx) * horizontal,
312 (height - view_box.height() * sy) * vertical,
313 )) * Affine::scale_non_uniform(sx, sy)
314 * Affine::translate((-view_box.x0, -view_box.y0))
315}
316
317fn element_transform(doc: &BaseDocument, root: NodeId, id: NodeId) -> Affine {
318 let mut transform = Affine::IDENTITY;
319 let mut current = Some(id);
320 while let Some(id) = current {
321 if id == root {
322 break;
323 }
324 let node = &doc.nodes[id];
325 if let Some(value) = attr(node, "transform")
326 && let Ok(value) = value.parse::<Transform>()
327 {
328 transform =
329 Affine::new([value.a, value.b, value.c, value.d, value.e, value.f]) * transform;
330 }
331 current = node.parent;
332 }
333 transform
334}
335
336fn foreign_object_box(node: &Node, view_box: Option<Rect>, size: usvg::Size) -> Option<Rect> {
337 let width = view_box.map_or(f64::from(size.width()), |rect| rect.width());
338 let height = view_box.map_or(f64::from(size.height()), |rect| rect.height());
339 let length = |name, reference, default| {
340 let Some(value) = attr(node, name) else {
341 return Some(default);
342 };
343 let value = value.parse::<Length>().ok()?;
344 let factor = match value.unit {
345 LengthUnit::None | LengthUnit::Px => 1.0,
346 LengthUnit::Percent => reference / 100.0,
347 LengthUnit::In => 96.0,
348 LengthUnit::Cm => 96.0 / 2.54,
349 LengthUnit::Mm => 96.0 / 25.4,
350 LengthUnit::Pt => 96.0 / 72.0,
351 LengthUnit::Pc => 16.0,
352 _ => return None,
353 };
354 Some(value.number * factor)
355 };
356 let x = length("x", width, 0.0)?;
357 let y = length("y", height, 0.0)?;
358 let width = length("width", width, 0.0)?;
359 let height = length("height", height, 0.0)?;
360 (width > 0.0 && height > 0.0).then(|| Rect::new(x, y, x + width, y + height))
361}
362
363impl Node {
364 pub fn svg_content_transform(&self) -> Option<Affine> {
367 let geometry = self.element_data()?.svg_geometry.as_ref()?;
368 let layout = self.final_layout();
369 let left = f64::from(layout.border.left + layout.padding.left);
370 let top = f64::from(layout.border.top + layout.padding.top);
371 let width = f64::from(layout.size.width)
372 - left
373 - f64::from(layout.border.right + layout.padding.right);
374 let height = f64::from(layout.size.height)
375 - top
376 - f64::from(layout.border.bottom + layout.padding.bottom);
377 if width <= 0.0 || height <= 0.0 {
378 return None;
379 }
380 Some(Affine::translate((left, top)) * geometry.content_transform(width, height))
381 }
382
383 pub(crate) fn hit_inline_svg(
384 &self,
385 x: f32,
386 y: f32,
387 hits: &mut Option<Vec<HitResult>>,
388 ) -> Option<HitResult> {
389 use style::computed_values::pointer_events::T as PointerEvents;
390 use style::computed_values::visibility::T as Visibility;
391
392 let geometry = self.element_data()?.svg_geometry.as_ref()?;
393 let transform = self.svg_content_transform()?;
394 let point = transform.inverse() * Point::new(f64::from(x), f64::from(y));
395 for (id, bounds) in geometry.hits.iter().rev() {
396 if !bounds.contains(point) {
397 continue;
398 }
399 let node = self.tree().get(*id)?;
400 if node.is_display_none()
401 || node.primary_styles().is_some_and(|style| {
402 style.clone_pointer_events() == PointerEvents::None
403 || matches!(
404 style.clone_visibility(),
405 Visibility::Hidden | Visibility::Collapse
406 )
407 })
408 {
409 continue;
410 }
411 let bounds = transform.transform_rect_bbox(*bounds);
412 let hit = HitResult {
413 node_id: *id,
414 x: x - bounds.x0 as f32,
415 y: y - bounds.y0 as f32,
416 is_text: false,
417 };
418 if let Some(hits) = hits.as_mut() {
419 hits.push(hit);
420 } else {
421 return Some(hit);
422 }
423 }
424 None
425 }
426}
427
428impl BaseDocument {
429 pub(crate) fn svg_geometry_root(&self, id: NodeId) -> Option<NodeId> {
430 let node = self.get_node(id)?;
431 if !is_svg(node) {
432 return None;
433 }
434 let mut current = node.parent;
435 while let Some(id) = current {
436 let node = self.get_node(id)?;
437 if node
438 .element_data()
439 .is_some_and(|element| element.svg_geometry.is_some())
440 {
441 return Some(id);
442 }
443 current = node.parent;
444 }
445 None
446 }
447
448 pub(crate) fn svg_client_rect(&self, root: NodeId, id: NodeId) -> Option<BoundingRect> {
449 let root_node = self.get_node(root)?;
450 let geometry = root_node.element_data()?.svg_geometry.as_ref()?;
451 let bounds = *geometry.bounds.get(&id)?;
452 let mut current = Some(id);
453 while let Some(id) = current {
454 let node = self.get_node(id)?;
455 if node.is_element() && node.is_display_none() {
456 return None;
457 }
458 current = node.parent;
459 }
460 let bounds = root_node
461 .svg_content_transform()?
462 .transform_rect_bbox(bounds);
463 let origin = root_node.absolute_position(0.0, 0.0);
464 let bounds = bounding_rect(
465 bounds
466 + kurbo::Vec2::new(
467 f64::from(origin.x) - self.viewport_scroll.x,
468 f64::from(origin.y) - self.viewport_scroll.y,
469 ),
470 );
471 Some(self.transformed_client_rect(root, bounds))
472 }
473
474 pub(crate) fn svg_user_bbox(&self, id: NodeId) -> Option<BoundingRect> {
475 let node = self.get_node(id)?;
476 if !is_graphics(node) {
477 return None;
478 }
479 let root = if node
480 .element_data()
481 .is_some_and(|element| element.svg_geometry.is_some())
482 {
483 Some(id)
484 } else {
485 self.svg_geometry_root(id)
486 };
487 let bounds = root
488 .and_then(|root| self.get_node(root))
489 .and_then(Node::element_data)
490 .and_then(|element| element.svg_geometry.as_ref())
491 .and_then(|geometry| geometry.bboxes.get(&id))
492 .copied()
493 .unwrap_or(Rect::ZERO);
494 Some(bounding_rect(bounds))
495 }
496}