Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion packages/blitz-dom/src/node/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,27 @@ impl SvgIntrinsicDimensions {
/// 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)]
#[derive(Debug)]
pub struct SvgImageData {
/// The parsed SVG tree.
pub tree: Arc<usvg::Tree>,
/// The dimensions declared on the root `<svg>` element.
pub intrinsic_dimensions: SvgIntrinsicDimensions,
/// The (decompressed) SVG source, for viewport re-resolution.
source: Arc<[u8]>,
/// The tree re-parsed for the last requested viewport size.
viewport_tree: std::sync::Mutex<Option<(f32, f32, Arc<usvg::Tree>)>>,
}

impl Clone for SvgImageData {
fn clone(&self) -> Self {
Self {
tree: Arc::clone(&self.tree),
intrinsic_dimensions: self.intrinsic_dimensions,
source: Arc::clone(&self.source),
viewport_tree: std::sync::Mutex::new(None),
}
}
}

impl SvgImageData {
Expand Down Expand Up @@ -89,9 +104,36 @@ impl SvgImageData {
Ok(Self {
tree: Arc::new(tree),
intrinsic_dimensions: SvgIntrinsicDimensions::from_xmltree(&doc),
source: data.into(),
viewport_tree: std::sync::Mutex::new(None),
})
}

/// The tree re-resolved against the given viewport size: for an SVG
/// without a `viewBox`, percentage lengths resolve against the viewport,
/// so the source is re-parsed with the root `width`/`height` set to the
/// viewport size. An SVG with a `viewBox` scales instead, so its original
/// tree is returned as-is.
pub fn tree_for_viewport(&self, width: f32, height: f32) -> Arc<usvg::Tree> {
if self.intrinsic_dimensions.view_box_size.is_some() {
return Arc::clone(&self.tree);
}

let mut cache = self.viewport_tree.lock().unwrap();
if let Some((w, h, tree)) = &*cache {
if *w == width && *h == height {
return Arc::clone(tree);
}
}

let tree = crate::util::parse_svg_for_viewport(&self.source, width, height)
.map(Arc::new)
.unwrap_or_else(|_| Arc::clone(&self.tree));

*cache = Some((width, height, Arc::clone(&tree)));
tree
}

/// 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> {
Expand Down
70 changes: 70 additions & 0 deletions packages/blitz-dom/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,76 @@ pub(crate) fn parse_svg_image(source: &[u8]) -> Result<crate::node::SvgImageData
crate::node::SvgImageData::from_data(source, &options)
}

/// Re-parse an SVG so that its viewport is exactly the given size: the root
/// `width`/`height` attributes are replaced with the viewport size, so
/// percentage lengths within the SVG resolve against it.
#[cfg(feature = "svg")]
pub(crate) fn parse_svg_for_viewport(
source: &[u8],
width: f32,
height: f32,
) -> Result<usvg::Tree, usvg::Error> {
usvg::Size::from_wh(width, height).ok_or(usvg::Error::InvalidSize)?;
let options = usvg::Options {
fontdb: Arc::clone(&*FONT_DB),
..Default::default()
};
let source = set_root_svg_size_attrs(source, width, height);
usvg::Tree::from_data(&source, &options)
}

/// Replace the `width` and `height` attributes on the root `<svg>` tag with
/// the given values.
#[cfg(feature = "svg")]
fn set_root_svg_size_attrs(source: &[u8], width: f32, height: f32) -> Vec<u8> {
let Ok(text) = std::str::from_utf8(source) else {
return source.to_vec();
};
let Some(tag_start) = text.find("<svg") else {
return source.to_vec();
};
let Some(tag_len) = text[tag_start..].find('>') else {
return source.to_vec();
};
let tag_end = tag_start + tag_len;

// The byte range of the attribute (name through closing quote) within `tag`.
fn attr_range(tag: &str, name: &str) -> Option<(usize, usize)> {
let mut search = 0;
while let Some(rel) = tag[search..].find(name) {
let idx = search + rel;
search = idx + name.len();
// Must be a standalone attribute name preceded by whitespace
// (not e.g. `stroke-width`) and followed by `=`.
if !tag[..idx].ends_with(char::is_whitespace) {
continue;
}
let after = &tag[idx + name.len()..];
let after_eq = after.trim_start().strip_prefix('=')?;
let value = after_eq.trim_start();
let quote = value.chars().next().filter(|c| *c == '"' || *c == '\'')?;
let close = value[1..].find(quote)?;
let value_start = idx + name.len() + (after.len() - value.len());
return Some((idx, value_start + 1 + close + 1));
}
None
}

let mut tag = text[tag_start..tag_end].to_string();
for name in ["width", "height"] {
if let Some((start, end)) = attr_range(&tag, name) {
tag.replace_range(start..end, "");
}
}
tag.insert_str(4, &format!(" width=\"{width}\" height=\"{height}\""));

let mut out = String::with_capacity(text.len());
out.push_str(&text[..tag_start]);
out.push_str(&tag);
out.push_str(&text[tag_end..]);
out.into_bytes()
}

pub trait ToColorColor {
/// Converts a color into the `AlphaColor<Srgb>` type from the `color` crate
fn as_color_color(&self) -> Color;
Expand Down
18 changes: 14 additions & 4 deletions packages/blitz-paint/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ impl<'dom, 'a> BlitzDomPainter<'dom, 'a> {
#[cfg(feature = "svg")]
let is_svg = node
.element_data()
.and_then(|e| e.svg_data())
.is_some_and(|tree| !blends_with_backdrop(tree.root()));
.and_then(|e| e.svg_image_data())
.is_some_and(|data| !blends_with_backdrop(data.tree.root()));
#[cfg(not(feature = "svg"))]
let is_svg = false;
let is_image = is_svg
Expand Down Expand Up @@ -562,7 +562,7 @@ impl<'dom, 'a> BlitzDomPainter<'dom, 'a> {
element,
transform,
#[cfg(feature = "svg")]
svg: element.svg_data(),
svg: element.svg_image_data(),
text_input: element.text_input_data(),
list_item: element.list_item_data.as_deref(),
devtools: self.dom.devtools(),
Expand Down Expand Up @@ -608,7 +608,7 @@ struct ElementCx<'dom, 'a> {
element: &'dom ElementData,
transform: Affine,
#[cfg(feature = "svg")]
svg: Option<&'dom usvg::Tree>,
svg: Option<&'dom blitz_dom::node::SvgImageData>,
text_input: Option<&'dom TextInputData>,
list_item: Option<&'dom ListItemLayout>,
devtools: &'dom DevtoolSettings,
Expand Down Expand Up @@ -957,6 +957,16 @@ impl ElementCx<'_, '_> {
let width = self.frame.content_box.width() as u32;
let height = self.frame.content_box.height() as u32;

// The tree re-resolved for the element's used size: an SVG without a
// viewBox cannot be scaled, so its percentage lengths are re-resolved
// against the viewport (in unzoomed CSS px; the object-fit transform
// below then scales 1:1 content by the zoom and HiDPI factors).
let zoom = self.style.effective_zoom;
let svg = svg.tree_for_viewport(
zoom.unzoom(width as f32 / self.scale as f32),
zoom.unzoom(height as f32 / self.scale as f32),
);
let svg = &*svg;
let svg_size = svg.size();

let x = self.frame.content_box.origin().x;
Expand Down
Loading