render: use resolved SVG viewports
This commit is contained in:
1 parent
237ba35639
commit
792a51ba81
1 file changed
+139
-2
@@ -2247,8 +2247,19 @@ fn render_svg(bytes: &[u8], width: u32, height: u32) -> Option<Pixmap> {
|
||||
if width == 0 || height == 0 {
|
||||
return None;
|
||||
}
|
||||
let opts = usvg::Options::default();
|
||||
let tree = usvg::Tree::from_data(bytes, &opts).ok()?;
|
||||
let mut opts = usvg::Options::default();
|
||||
// The outer replaced element supplies the SVG document viewport. Force
|
||||
// that used CSS size onto the root before usvg resolves `viewBox`:
|
||||
// a missing height is represented as 100%, which usvg otherwise resolves
|
||||
// against the viewBox height itself. `<svg width=32 viewBox="0 0 223
|
||||
// 236">` would therefore become a 32x236 viewport; its artwork is fitted
|
||||
// into a thin centered strip and then the whole strip is scaled to 32x34.
|
||||
// Author `preserveAspectRatio` still controls fitting inside this viewport.
|
||||
// usvg resolves root dimensions before an injected stylesheet can
|
||||
// override them, so provide the used viewport as actual root attributes.
|
||||
let viewport_svg = svg_with_root_viewport(bytes, width, height)?;
|
||||
opts.default_size = usvg::Size::from_wh(width as f32, height as f32)?;
|
||||
let tree = usvg::Tree::from_data(&viewport_svg, &opts).ok()?;
|
||||
let size = tree.size();
|
||||
if size.width() <= 0.0 || size.height() <= 0.0 {
|
||||
return None;
|
||||
@@ -2259,6 +2270,108 @@ fn render_svg(bytes: &[u8], width: u32, height: u32) -> Option<Pixmap> {
|
||||
Some(svg_pixmap)
|
||||
}
|
||||
|
||||
/// Return SVG XML whose root `width`/`height` are the resolved CSS viewport.
|
||||
///
|
||||
/// This is deliberately a narrow XML start-tag rewrite rather than a DOM
|
||||
/// reserialization: all namespaces, styles, definitions, and source order
|
||||
/// remain byte-for-byte intact. Existing attribute values are replaced;
|
||||
/// missing ones are appended. Quoted `>` characters are respected while
|
||||
/// finding the end of the root tag.
|
||||
fn svg_with_root_viewport(bytes: &[u8], width: u32, height: u32) -> Option<Vec<u8>> {
|
||||
let source = std::str::from_utf8(bytes).ok()?;
|
||||
let start = source.find("<svg")?;
|
||||
let tail = &source[start..];
|
||||
let mut quote = None;
|
||||
let mut tag_end = None;
|
||||
for (offset, ch) in tail.char_indices() {
|
||||
match (quote, ch) {
|
||||
(Some(open), close) if close == open => quote = None,
|
||||
(None, '"' | '\'') => quote = Some(ch),
|
||||
(None, '>') => {
|
||||
tag_end = Some(start + offset);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let tag_end = tag_end?;
|
||||
let mut root = source[start..=tag_end].to_string();
|
||||
for (name, value) in [("width", width), ("height", height)] {
|
||||
if let Some((value_start, value_end)) = svg_root_attr_value_range(&root, name) {
|
||||
root.replace_range(value_start..value_end, &value.to_string());
|
||||
} else {
|
||||
root.insert_str(root.len() - 1, &format!(" {name}=\"{value}\""));
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = String::with_capacity(source.len() + 32);
|
||||
output.push_str(&source[..start]);
|
||||
output.push_str(&root);
|
||||
output.push_str(&source[tag_end + 1..]);
|
||||
Some(output.into_bytes())
|
||||
}
|
||||
|
||||
/// Value byte range for one attribute in an `<svg ...>` start tag.
|
||||
fn svg_root_attr_value_range(tag: &str, wanted: &str) -> Option<(usize, usize)> {
|
||||
let bytes = tag.as_bytes();
|
||||
let mut index = "<svg".len();
|
||||
while index < bytes.len() {
|
||||
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
||||
index += 1;
|
||||
}
|
||||
if index >= bytes.len() || bytes[index] == b'>' || bytes[index] == b'/' {
|
||||
return None;
|
||||
}
|
||||
let name_start = index;
|
||||
while index < bytes.len()
|
||||
&& !bytes[index].is_ascii_whitespace()
|
||||
&& bytes[index] != b'='
|
||||
&& bytes[index] != b'>'
|
||||
&& bytes[index] != b'/'
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
let name_end = index;
|
||||
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
||||
index += 1;
|
||||
}
|
||||
if index >= bytes.len() || bytes[index] != b'=' {
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
while index < bytes.len() && bytes[index].is_ascii_whitespace() {
|
||||
index += 1;
|
||||
}
|
||||
let (value_start, value_end) = if index < bytes.len()
|
||||
&& matches!(bytes[index], b'"' | b'\'')
|
||||
{
|
||||
let delimiter = bytes[index];
|
||||
index += 1;
|
||||
let start = index;
|
||||
while index < bytes.len() && bytes[index] != delimiter {
|
||||
index += 1;
|
||||
}
|
||||
let end = index;
|
||||
index = (index + 1).min(bytes.len());
|
||||
(start, end)
|
||||
} else {
|
||||
let start = index;
|
||||
while index < bytes.len()
|
||||
&& !bytes[index].is_ascii_whitespace()
|
||||
&& bytes[index] != b'>'
|
||||
&& bytes[index] != b'/'
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
(start, index)
|
||||
};
|
||||
if &tag[name_start..name_end] == wanted {
|
||||
return Some((value_start, value_end));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Serialize an inline `<svg>` subtree (rooted at `root`) back to a standalone
|
||||
/// SVG document string. Emits `<tag attr="v">children</tag>` for the element
|
||||
/// and every descendant, preserving the root's `viewBox`/`width`/`height` and
|
||||
@@ -3198,6 +3311,30 @@ mod tests {
|
||||
assert!(found_red, "expected inline svg <rect> to paint red");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_missing_root_height_uses_the_final_css_viewport() {
|
||||
let svg = br##"<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="32" viewBox="0 0 223 236">
|
||||
<rect width="223" height="236" fill="#ed174c"/>
|
||||
</svg>"##;
|
||||
let pixmap = render_svg(svg, 32, 34).expect("svg raster");
|
||||
let mut min_y = 34u32;
|
||||
let mut max_y = 0u32;
|
||||
for y in 0..34 {
|
||||
for x in 0..32 {
|
||||
let pixel = pixmap.pixel(x, y).unwrap();
|
||||
if pixel.alpha() > 0 {
|
||||
min_y = min_y.min(y);
|
||||
max_y = max_y.max(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
max_y.saturating_sub(min_y) >= 30,
|
||||
"viewBox artwork should fill the resolved 32x34 viewport, got rows {min_y}..{max_y}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paints_inline_svg_current_color_from_computed_style() {
|
||||
let tree = parse_html(
|
||||
|
||||
Reference in new issue
Block a user