render: resolve document-level SVG symbols

This commit is contained in:
SGavrl committed 2026-07-26 15:52:32 +00:00
1 parent bc13488df9
commit 1dc4bf1b63
1 file changed
+59 -18
+59 -18
View File
@@ -1890,16 +1890,10 @@ fn svg_escape_attr(s: &str, buf: &mut String) {
} }
} }
/// Resolve `<use>` elements in an inline `<svg>` subtree that point at an /// Resolve `<use>` elements in an inline `<svg>` subtree against either a
/// EXTERNAL sprite file (`href`/`xlink:href` = `url#id` with a non-empty url), /// document-level symbol sprite (`href="#id"`) or an external sprite file
/// splicing the referenced symbol into the already-serialized `markup` so resvg /// (`href="url#id"`), splicing the referenced symbol into the standalone SVG
/// can rasterize it. For each distinct external reference, fetch the sprite /// handed to resvg. Symbols already inside `root` need no injection.
/// (once, via the shared image cache and resolving relative urls against
/// `base_url`), pull out the element carrying `id="id"`, inject it inside a
/// `<defs>` right after the opening `<svg>` tag, and rewrite the `<use>` href to
/// a now-local `#id`. Same-document `<use href="#id">` (empty url) is ignored
/// and left to resvg's own resolution. A fetch or parse failure is a silent
/// no-op: the affected `<use>` simply renders nothing, exactly as before.
fn inject_external_sprites( fn inject_external_sprites(
tree: &DomTree, tree: &DomTree,
root: obscura_dom::tree::NodeId, root: obscura_dom::tree::NodeId,
@@ -1911,7 +1905,9 @@ fn inject_external_sprites(
// Distinct external references (full href, url, fragment id), in first-seen // Distinct external references (full href, url, fragment id), in first-seen
// order. Dedupe so one symbol referenced by several `<use>` is fetched and // order. Dedupe so one symbol referenced by several `<use>` is fetched and
// injected once (the rewrite below still fixes every occurrence). // injected once (the rewrite below still fixes every occurrence).
let root_descendants = tree.descendants(root);
let mut refs: Vec<(String, String, String)> = Vec::new(); let mut refs: Vec<(String, String, String)> = Vec::new();
let mut local_fragments = Vec::new();
for nid in tree.descendants(root) { for nid in tree.descendants(root) {
let Some(node) = tree.get_node(nid) else { continue }; let Some(node) = tree.get_node(nid) else { continue };
let Some(el) = node.as_element() else { continue }; let Some(el) = node.as_element() else { continue };
@@ -1929,8 +1925,13 @@ fn inject_external_sprites(
}; };
let Some(hash) = href.find('#') else { continue }; let Some(hash) = href.find('#') else { continue };
let (url, frag) = (&href[..hash], &href[hash + 1..]); let (url, frag) = (&href[..hash], &href[hash + 1..]);
if url.is_empty() || frag.is_empty() { if frag.is_empty() {
// Same-document `#id` (or a malformed ref): nothing to fetch. continue;
}
if url.is_empty() {
if !local_fragments.iter().any(|existing| existing == frag) {
local_fragments.push(frag.to_string());
}
continue; continue;
} }
let entry = (href.to_string(), url.to_string(), frag.to_string()); let entry = (href.to_string(), url.to_string(), frag.to_string());
@@ -1938,12 +1939,27 @@ fn inject_external_sprites(
refs.push(entry); refs.push(entry);
} }
} }
if refs.is_empty() {
return;
}
let mut defs = String::new(); let mut defs = String::new();
let mut rewrites: Vec<(String, String)> = Vec::new(); let mut rewrites: Vec<(String, String)> = Vec::new();
let wanted_local: std::collections::HashSet<&str> =
local_fragments.iter().map(String::as_str).collect();
let mut local_nodes = std::collections::HashMap::new();
if !wanted_local.is_empty() {
for nid in tree.descendants(tree.document()) {
let Some(node) = tree.get_node(nid) else { continue };
let Some(id) = node.get_attribute("id") else { continue };
if wanted_local.contains(id) {
local_nodes.entry(id.to_string()).or_insert(nid);
}
}
}
for frag in local_fragments {
let Some(&symbol_id) = local_nodes.get(&frag) else { continue };
if symbol_id == root || root_descendants.contains(&symbol_id) {
continue;
}
serialize_svg_node(tree, symbol_id, false, &mut defs);
}
for (href, url, frag) in &refs { for (href, url, frag) in &refs {
let key = format!("{url}#{frag}"); let key = format!("{url}#{frag}");
let symbol = sprite_cache let symbol = sprite_cache
@@ -2620,8 +2636,8 @@ mod tests {
#[test] #[test]
fn same_document_use_left_unchanged_by_inject() { fn same_document_use_left_unchanged_by_inject() {
// A same-document `<use href="#a">` has an empty url, so inject does no // A same-document symbol already inside the target SVG needs no
// network work and leaves the serialized markup byte-for-byte unchanged. // injection and leaves the serialized markup byte-for-byte unchanged.
let tree = parse_html( let tree = parse_html(
r##"<html><body><svg viewBox="0 0 10 10"><use href="#a"/><symbol id="a"><path d="M0 0h10v10z"/></symbol></svg></body></html>"##, r##"<html><body><svg viewBox="0 0 10 10"><use href="#a"/><symbol id="a"><path d="M0 0h10v10z"/></symbol></svg></body></html>"##,
); );
@@ -2634,6 +2650,31 @@ mod tests {
assert_eq!(markup, before, "same-document use must be untouched"); assert_eq!(markup, before, "same-document use must be untouched");
} }
#[test]
fn injects_document_level_symbol_into_target_svg() {
// Frameworks commonly keep one hidden sprite beside the application
// root and reference it from otherwise independent inline SVGs.
let tree = parse_html(
r##"<html><body>
<svg style="display:none"><symbol id="arrow" viewBox="0 0 10 10"><path d="M0 0h10v10z"/></symbol></svg>
<svg id="icon" viewBox="0 0 10 10"><use href="#arrow"/></svg>
</body></html>"##,
);
let svg = tree.query_selector("#icon").unwrap().unwrap();
let mut markup = serialize_svg(&tree, svg);
let mut cache = std::collections::HashMap::new();
let mut sprite_cache = std::collections::HashMap::new();
inject_external_sprites(&tree, svg, None, &mut markup, &mut cache, &mut sprite_cache);
assert!(
markup.contains(r#"<defs><symbol id="arrow""#),
"document-level symbol must be copied into target SVG: {markup}"
);
assert!(
markup.contains(r##"<use href="#arrow""##),
"local use reference must remain intact: {markup}"
);
}
#[test] #[test]
fn font_face_parser_selects_ascii_subset_and_preserves_functional_src() { fn font_face_parser_selects_ascii_subset_and_preserves_functional_src() {
let css = r#" let css = r#"