blitz_dom/node/svg.rs
1//! SVG image data and CSS intrinsic sizing for SVG.
2
3use std::sync::Arc;
4
5use usvg::roxmltree;
6
7/// Dimensions declared on the root `<svg>` element, before any resolution.
8///
9/// Unlike [`usvg::Tree::size`], which always produces a concrete size, this
10/// preserves what the SVG actually declared: absent attributes are `None` and
11/// percentage lengths are kept unresolved.
12#[derive(Debug, Clone, Copy, Default)]
13pub struct SvgIntrinsicDimensions {
14 /// The root `width` attribute, if declared. Percentages are unresolved.
15 pub width: Option<svgtypes::Length>,
16 /// The root `height` attribute, if declared. Percentages are unresolved.
17 pub height: Option<svgtypes::Length>,
18 /// The root `viewBox` width/height, if declared and valid.
19 pub view_box_size: Option<(f32, f32)>,
20 /// Whether the root declared a `viewBox` with a zero width or height,
21 /// which disables rendering of the element per the SVG spec.
22 pub degenerate_view_box: bool,
23}
24
25impl SvgIntrinsicDimensions {
26 /// Extract the `width`/`height`/`viewBox` attributes declared on the root
27 /// element of an already-parsed SVG document, with absent attributes as
28 /// `None` and percentages unresolved. [`usvg::Tree`] does not preserve
29 /// these (it always resolves to a concrete size), so they are read from
30 /// the XML document here.
31 pub fn from_xmltree(doc: &roxmltree::Document) -> Self {
32 let root = doc.root_element();
33
34 let parse_length = |name: &str| -> Option<svgtypes::Length> {
35 root.attribute(name)?.parse::<svgtypes::Length>().ok()
36 };
37 // Parsed manually rather than via `svgtypes::ViewBox`, which rejects
38 // zero sizes: a zero `viewBox` width/height is distinguished from an
39 // invalid `viewBox` as it disables rendering of the element.
40 let view_box_dims = root.attribute("viewBox").and_then(|s| {
41 let mut numbers = svgtypes::NumberListParser::from(s);
42 let _x = numbers.next()?.ok()?;
43 let _y = numbers.next()?.ok()?;
44 let w = numbers.next()?.ok()? as f32;
45 let h = numbers.next()?.ok()? as f32;
46 (numbers.next().is_none() && w.is_finite() && h.is_finite() && w >= 0.0 && h >= 0.0)
47 .then_some((w, h))
48 });
49 let view_box_size = view_box_dims.filter(|&(w, h)| w > 0.0 && h > 0.0);
50 let degenerate_view_box = view_box_dims.is_some_and(|(w, h)| w == 0.0 || h == 0.0);
51
52 Self {
53 width: parse_length("width"),
54 height: parse_length("height"),
55 view_box_size,
56 degenerate_view_box,
57 }
58 }
59}
60
61/// A parsed SVG image.
62///
63/// usvg always resolves the root `<svg>` to a concrete [`usvg::Tree::size`],
64/// falling back to the `viewBox` size when `width`/`height` are absent or given
65/// as percentages. For CSS sizing purposes, however, such an SVG has *no*
66/// intrinsic width/height (only an intrinsic aspect ratio). The accessors on
67/// this type resolve the CSS intrinsic dimensions from the declared root
68/// attributes, which are captured at parse time.
69#[derive(Debug, Clone)]
70pub struct SvgImageData {
71 /// The parsed SVG tree.
72 pub tree: Arc<usvg::Tree>,
73 /// The dimensions declared on the root `<svg>` element.
74 pub intrinsic_dimensions: SvgIntrinsicDimensions,
75}
76
77impl SvgImageData {
78 /// Parse an SVG image from raw data, capturing both the rendered
79 /// [`usvg::Tree`] and the declared root dimensions from a single XML
80 /// parse.
81 ///
82 /// Like [`usvg::Tree::from_data`], gzip-compressed data (SVGZ) is
83 /// decompressed first.
84 pub fn from_data(data: &[u8], options: &usvg::Options) -> Result<Self, usvg::Error> {
85 // Gzip magic bytes, matching the SVGZ detection in `usvg::Tree::from_data`.
86 let decompressed;
87 let data = if data.starts_with(&[0x1f, 0x8b]) {
88 decompressed = usvg::decompress_svgz(data)?;
89 decompressed.as_slice()
90 } else {
91 data
92 };
93
94 let text = std::str::from_utf8(data).map_err(|_| usvg::Error::NotAnUtf8Str)?;
95 let xml_options = roxmltree::ParsingOptions {
96 allow_dtd: true,
97 ..Default::default()
98 };
99 let doc = roxmltree::Document::parse_with_options(text, xml_options)
100 .map_err(usvg::Error::ParsingFailed)?;
101 let tree = usvg::Tree::from_xmltree(&doc, options)?;
102 Ok(Self {
103 tree: Arc::new(tree),
104 intrinsic_dimensions: SvgIntrinsicDimensions::from_xmltree(&doc),
105 })
106 }
107
108 /// The intrinsic width in CSS px, present only when the root `<svg>`
109 /// declared an absolute (non-percentage) `width`.
110 pub fn intrinsic_width(&self) -> Option<f32> {
111 use svgtypes::LengthUnit;
112 let declared = self
113 .intrinsic_dimensions
114 .width
115 .is_some_and(|len| len.unit != LengthUnit::Percent);
116 declared.then(|| self.tree.size().width())
117 }
118
119 /// The intrinsic height in CSS px, present only when the root `<svg>`
120 /// declared an absolute (non-percentage) `height`.
121 pub fn intrinsic_height(&self) -> Option<f32> {
122 use svgtypes::LengthUnit;
123 let declared = self
124 .intrinsic_dimensions
125 .height
126 .is_some_and(|len| len.unit != LengthUnit::Percent);
127 declared.then(|| self.tree.size().height())
128 }
129
130 /// The aspect ratio of the root `<svg>`'s `viewBox`, if it declares one.
131 pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
132 self.intrinsic_dimensions.view_box_size.map(|(w, h)| w / h)
133 }
134
135 /// The root `width` attribute resolved against a containing block width:
136 /// percentages resolve against the containing block (`None` if it is
137 /// indefinite) and an absent attribute is `None`.
138 ///
139 /// This is only appropriate for an inline `<svg>` element, where the
140 /// attributes behave as presentation attributes. SVG used as an image
141 /// (e.g. `<img src>` or a background) must use [`Self::intrinsic_width`],
142 /// as its intrinsic dimensions are context-free per CSS.
143 pub fn resolved_width(&self, container_width: Option<f32>) -> Option<f32> {
144 use svgtypes::LengthUnit;
145 match self.intrinsic_dimensions.width {
146 Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()),
147 Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0),
148 None => None,
149 }
150 }
151
152 /// The root `height` attribute resolved against a containing block height.
153 /// See [`Self::resolved_width`].
154 pub fn resolved_height(&self, container_height: Option<f32>) -> Option<f32> {
155 use svgtypes::LengthUnit;
156 match self.intrinsic_dimensions.height {
157 Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()),
158 Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0),
159 None => None,
160 }
161 }
162
163 /// The intrinsic aspect ratio of the SVG: the ratio of its declared
164 /// `width`/`height` when both are absolute lengths, otherwise the
165 /// `viewBox` ratio, otherwise the ratio of the resolved
166 /// [`usvg::Tree::size`] (which is always non-zero).
167 pub fn aspect_ratio(&self) -> f32 {
168 match (self.intrinsic_width(), self.intrinsic_height()) {
169 (Some(w), Some(h)) => w / h,
170 _ => self.viewbox_aspect_ratio().unwrap_or_else(|| {
171 let size = self.tree.size();
172 size.width() / size.height()
173 }),
174 }
175 }
176
177 /// The intrinsic dimensions of the SVG resolved per CSS replaced element
178 /// sizing: a missing dimension is computed from the declared one and the
179 /// intrinsic aspect ratio; if neither is declared, the resolved
180 /// [`usvg::Tree::size`] is used as a fallback.
181 pub fn intrinsic_size(&self) -> (f32, f32) {
182 let aspect_ratio = self.aspect_ratio();
183 match (self.intrinsic_width(), self.intrinsic_height()) {
184 (Some(w), Some(h)) => (w, h),
185 (Some(w), None) => (w, w / aspect_ratio),
186 (None, Some(h)) => (h * aspect_ratio, h),
187 (None, None) => {
188 // No intrinsic dimensions. If there is an intrinsic aspect ratio, apply
189 // the CSS default sizing algorithm: contain within the default object
190 // size of 300x150. Otherwise fall back to the resolved tree size.
191 if self.viewbox_aspect_ratio().is_some() {
192 let scale = (300.0 / aspect_ratio).min(150.0);
193 (scale * aspect_ratio, scale)
194 } else {
195 let size = self.tree.size();
196 (size.width(), size.height())
197 }
198 }
199 }
200 }
201}