1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
//! SVG image data and CSS intrinsic sizing for SVG.
use std::sync::Arc;
use usvg::roxmltree;
/// Dimensions declared on the root `<svg>` element, before any resolution.
///
/// Unlike [`usvg::Tree::size`], which always produces a concrete size, this
/// preserves what the SVG actually declared: absent attributes are `None` and
/// percentage lengths are kept unresolved.
#[derive(Debug, Clone, Copy, Default)]
pub struct SvgIntrinsicDimensions {
/// The root `width` attribute, if declared. Percentages are unresolved.
pub width: Option<svgtypes::Length>,
/// The root `height` attribute, if declared. Percentages are unresolved.
pub height: Option<svgtypes::Length>,
/// The root `viewBox`, if declared and valid.
pub view_box: Option<usvg::NonZeroRect>,
/// The root `preserveAspectRatio` (default `xMidYMid meet`).
pub preserve_aspect_ratio: svgtypes::AspectRatio,
/// Whether the root declared a `viewBox` with a zero width or height,
/// which disables rendering of the element per the SVG spec.
pub degenerate_view_box: bool,
}
impl SvgIntrinsicDimensions {
/// Extract the `width`/`height`/`viewBox` attributes declared on the root
/// element of an already-parsed SVG document, with absent attributes as
/// `None` and percentages unresolved. [`usvg::Tree`] does not preserve
/// these (it always resolves to a concrete size), so they are read from
/// the XML document here.
pub fn from_xmltree(doc: &roxmltree::Document) -> Self {
let root = doc.root_element();
let parse_length = |name: &str| -> Option<svgtypes::Length> {
root.attribute(name)?.parse::<svgtypes::Length>().ok()
};
// Parsed manually rather than via `svgtypes::ViewBox`, which rejects
// zero sizes: a zero `viewBox` width/height is distinguished from an
// invalid `viewBox` as it disables rendering of the element.
let view_box_dims = root.attribute("viewBox").and_then(|s| {
let mut numbers = svgtypes::NumberListParser::from(s);
let x = numbers.next()?.ok()? as f32;
let y = numbers.next()?.ok()? as f32;
let w = numbers.next()?.ok()? as f32;
let h = numbers.next()?.ok()? as f32;
(numbers.next().is_none() && w.is_finite() && h.is_finite() && w >= 0.0 && h >= 0.0)
.then_some((x, y, w, h))
});
let view_box =
view_box_dims.and_then(|(x, y, w, h)| usvg::NonZeroRect::from_xywh(x, y, w, h));
let degenerate_view_box = view_box_dims.is_some_and(|(_, _, w, h)| w == 0.0 || h == 0.0);
let preserve_aspect_ratio = root
.attribute("preserveAspectRatio")
.and_then(|s| s.parse().ok())
.unwrap_or_default();
Self {
width: parse_length("width"),
height: parse_length("height"),
view_box,
preserve_aspect_ratio,
degenerate_view_box,
}
}
}
/// A parsed SVG image.
///
/// usvg always resolves the root `<svg>` to a concrete [`usvg::Tree::size`],
/// falling back to the `viewBox` size when `width`/`height` are absent or given
/// as percentages. For CSS sizing purposes, however, such an SVG has *no*
/// intrinsic width/height (only an intrinsic aspect ratio). The accessors on
/// this type resolve the CSS intrinsic dimensions from the declared root
/// attributes, which are captured at parse time.
#[derive(Debug, Clone)]
pub struct SvgImageData {
/// The parsed SVG tree.
pub tree: Arc<usvg::Tree>,
/// The dimensions declared on the root `<svg>` element.
pub intrinsic_dimensions: SvgIntrinsicDimensions,
}
impl SvgImageData {
/// Parse an SVG image from raw data, capturing both the rendered
/// [`usvg::Tree`] and the declared root dimensions from a single XML
/// parse.
///
/// Like [`usvg::Tree::from_data`], gzip-compressed data (SVGZ) is
/// decompressed first.
pub fn from_data(data: &[u8], options: &usvg::Options) -> Result<Self, usvg::Error> {
// Gzip magic bytes, matching the SVGZ detection in `usvg::Tree::from_data`.
let decompressed;
let data = if data.starts_with(&[0x1f, 0x8b]) {
decompressed = usvg::decompress_svgz(data)?;
decompressed.as_slice()
} else {
data
};
let text = std::str::from_utf8(data).map_err(|_| usvg::Error::NotAnUtf8Str)?;
let xml_options = roxmltree::ParsingOptions {
allow_dtd: true,
..Default::default()
};
let doc = roxmltree::Document::parse_with_options(text, xml_options)
.map_err(usvg::Error::ParsingFailed)?;
let tree = usvg::Tree::from_xmltree(&doc, options)?;
Ok(Self {
tree: Arc::new(tree),
intrinsic_dimensions: SvgIntrinsicDimensions::from_xmltree(&doc),
})
}
/// The intrinsic width in CSS px, present only when the root `<svg>`
/// declared an absolute (non-percentage) `width`.
pub fn intrinsic_width(&self) -> Option<f32> {
use svgtypes::LengthUnit;
let declared = self
.intrinsic_dimensions
.width
.is_some_and(|len| len.unit != LengthUnit::Percent);
declared.then(|| self.tree.size().width())
}
/// The intrinsic height in CSS px, present only when the root `<svg>`
/// declared an absolute (non-percentage) `height`.
pub fn intrinsic_height(&self) -> Option<f32> {
use svgtypes::LengthUnit;
let declared = self
.intrinsic_dimensions
.height
.is_some_and(|len| len.unit != LengthUnit::Percent);
declared.then(|| self.tree.size().height())
}
/// The aspect ratio of the root `<svg>`'s `viewBox`, if it declares one.
pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
self.intrinsic_dimensions
.view_box
.map(|view_box| view_box.width() / view_box.height())
}
/// The transform from the rendered tree (in [`usvg::Tree::size`] units) to a
/// viewport of the given size, applying the root `viewBox` and
/// `preserveAspectRatio`. Without a `viewBox` the tree is stretched to fill it.
pub fn viewport_transform(&self, width: f32, height: f32) -> usvg::Transform {
let size = self.tree.size();
let stretch = usvg::Transform::from_scale(width / size.width(), height / size.height());
let Some(view_box) = self.intrinsic_dimensions.view_box else {
return stretch;
};
let aspect = self.intrinsic_dimensions.preserve_aspect_ratio;
// usvg has already mapped the `viewBox` onto `Tree::size`, so undo that first.
let view_box_to_tree = view_box_transform(view_box, aspect, size.width(), size.height());
match view_box_to_tree.invert() {
Some(tree_to_view_box) => {
view_box_transform(view_box, aspect, width, height).pre_concat(tree_to_view_box)
}
None => stretch,
}
}
/// The root `width` attribute resolved against a containing block width:
/// percentages resolve against the containing block (`None` if it is
/// indefinite) and an absent attribute is `None`.
///
/// This is only appropriate for an inline `<svg>` element, where the
/// attributes behave as presentation attributes. SVG used as an image
/// (e.g. `<img src>` or a background) must use [`Self::intrinsic_width`],
/// as its intrinsic dimensions are context-free per CSS.
pub fn resolved_width(&self, container_width: Option<f32>) -> Option<f32> {
use svgtypes::LengthUnit;
match self.intrinsic_dimensions.width {
Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().width()),
Some(len) => container_width.map(|cw| cw * (len.number as f32) / 100.0),
None => None,
}
}
/// The root `height` attribute resolved against a containing block height.
/// See [`Self::resolved_width`].
pub fn resolved_height(&self, container_height: Option<f32>) -> Option<f32> {
use svgtypes::LengthUnit;
match self.intrinsic_dimensions.height {
Some(len) if len.unit != LengthUnit::Percent => Some(self.tree.size().height()),
Some(len) => container_height.map(|ch| ch * (len.number as f32) / 100.0),
None => None,
}
}
/// The intrinsic aspect ratio of the SVG: the ratio of its declared
/// `width`/`height` when both are absolute lengths, otherwise the
/// `viewBox` ratio, otherwise the ratio of the resolved
/// [`usvg::Tree::size`] (which is always non-zero).
pub fn aspect_ratio(&self) -> f32 {
match (self.intrinsic_width(), self.intrinsic_height()) {
(Some(w), Some(h)) => w / h,
_ => self.viewbox_aspect_ratio().unwrap_or_else(|| {
let size = self.tree.size();
size.width() / size.height()
}),
}
}
/// The intrinsic dimensions of the SVG resolved per CSS replaced element
/// sizing: a missing dimension is computed from the declared one and the
/// intrinsic aspect ratio; if neither is declared, the resolved
/// [`usvg::Tree::size`] is used as a fallback.
pub fn intrinsic_size(&self) -> (f32, f32) {
let aspect_ratio = self.aspect_ratio();
match (self.intrinsic_width(), self.intrinsic_height()) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => (w, w / aspect_ratio),
(None, Some(h)) => (h * aspect_ratio, h),
(None, None) => {
// No intrinsic dimensions. If there is an intrinsic aspect ratio, apply
// the CSS default sizing algorithm: contain within the default object
// size of 300x150. Otherwise fall back to the resolved tree size.
if self.viewbox_aspect_ratio().is_some() {
let scale = (300.0 / aspect_ratio).min(150.0);
(scale * aspect_ratio, scale)
} else {
let size = self.tree.size();
(size.width(), size.height())
}
}
}
}
}
/// Map a `viewBox` onto a viewport of the given size per `preserveAspectRatio`.
fn view_box_transform(
view_box: usvg::NonZeroRect,
aspect: svgtypes::AspectRatio,
width: f32,
height: f32,
) -> usvg::Transform {
use svgtypes::Align;
let sx = width / view_box.width();
let sy = height / view_box.height();
let (sx, sy) = if aspect.align == Align::None {
(sx, sy)
} else if aspect.slice {
(sx.max(sy), sx.max(sy))
} else {
(sx.min(sy), sx.min(sy))
};
let (align_x, align_y) = match aspect.align {
Align::None | Align::XMinYMin => (0.0, 0.0),
Align::XMidYMin => (0.5, 0.0),
Align::XMaxYMin => (1.0, 0.0),
Align::XMinYMid => (0.0, 0.5),
Align::XMidYMid => (0.5, 0.5),
Align::XMaxYMid => (1.0, 0.5),
Align::XMinYMax => (0.0, 1.0),
Align::XMidYMax => (0.5, 1.0),
Align::XMaxYMax => (1.0, 1.0),
};
let tx = (width - view_box.width() * sx) * align_x - view_box.x() * sx;
let ty = (height - view_box.height() * sy) * align_y - view_box.y() * sy;
usvg::Transform::from_row(sx, 0.0, 0.0, sy, tx, ty)
}