use std::sync::Arc;
use usvg::roxmltree;
#[derive(Debug, Clone, Copy, Default)]
pub struct SvgIntrinsicDimensions {
pub width: Option<svgtypes::Length>,
pub height: Option<svgtypes::Length>,
pub view_box: Option<usvg::NonZeroRect>,
pub preserve_aspect_ratio: svgtypes::AspectRatio,
pub degenerate_view_box: bool,
}
impl SvgIntrinsicDimensions {
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()
};
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,
}
}
}
#[derive(Debug, Clone)]
pub struct SvgImageData {
pub tree: Arc<usvg::Tree>,
pub intrinsic_dimensions: SvgIntrinsicDimensions,
}
impl SvgImageData {
pub fn from_data(data: &[u8], options: &usvg::Options) -> Result<Self, usvg::Error> {
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),
})
}
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())
}
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())
}
pub fn viewbox_aspect_ratio(&self) -> Option<f32> {
self.intrinsic_dimensions
.view_box
.map(|view_box| view_box.width() / view_box.height())
}
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;
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,
}
}
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,
}
}
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,
}
}
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()
}),
}
}
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) => {
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())
}
}
}
}
}
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)
}