Skip to main content

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}