diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index 1aa571a6ed..22511ce8ab 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -188,7 +188,7 @@ pub(crate) fn import_finding( symbol_suffixes: HashMap::new(), pattern_suffixes: HashMap::new(), blends: fx::blends(&xml, &scan.markers), - filter_sources: fx::sources(&xml, &scan.markers), + filter_sources: fx::sources(&xml, &scan.markers, |h| files.data_href(h)), files, non_scaling: found.non_scaling, data: found.data, diff --git a/crates/svg/src/import/files.rs b/crates/svg/src/import/files.rs index fe3bf05e8f..8899eccdee 100644 --- a/crates/svg/src/import/files.rs +++ b/crates/svg/src/import/files.rs @@ -102,6 +102,8 @@ struct DataUrls { numbers: HashMap, usize>, /// Each href: a data URL, then what it decoded to, or a link. urls: Vec, + /// Each href as it was written, for the filters kept as written ([`Files::data_href`]). + hrefs: Vec>, } enum DataUrl { @@ -322,6 +324,7 @@ impl Files { let href: Arc = Arc::from(a.value()); let n = data.urls.len(); data.numbers.insert(href.clone(), n); + data.hrefs.push(href.clone()); data.urls.push(if url { DataUrl::Encoded(href) } else { DataUrl::Link(href) }); n } @@ -601,6 +604,13 @@ impl Files { (known && h.len() == DATA_URL_KEY.len() + n.to_string().len()).then_some(n) } + /// The href whose key is `h`, as it was written: a filter kept as an effect holds its + /// `` hrefs as written, not their keys ([`super::fx::sources`]). + pub(in crate::import) fn data_href(&self, h: &str) -> Option> { + let n = self.data_number(h)?; + self.data.lock().ok()?.hrefs.get(n).cloned() + } + /// The image of data URL `n`: decoded the first time ([`Files::decode`]), then kept; the link /// when href `n` is one. fn data_url(&self, n: usize, opts: &usvg::Options) -> Result, Arc> { diff --git a/crates/svg/src/import/fx.rs b/crates/svg/src/import/fx.rs index b760b1b119..4a549552d4 100644 --- a/crates/svg/src/import/fx.rs +++ b/crates/svg/src/import/fx.rs @@ -61,8 +61,10 @@ impl Source { /// Each `` element with an id, as written, by id, with the document's style sheets read /// once for all of them. A source is checked and read by usvg only when art uses it, once. -/// `markers` is the text with its data URLs left out ([`super::depth::prescan`]). -pub(super) fn sources(xml: &roxmltree::Document, markers: &str) -> HashMap { +/// `markers` is the text with its data URLs left out ([`super::depth::prescan`]). `href` gives the +/// href, as written, of each key the prepass wrote in place of an `` href +/// ([`super::files::Files::data_href`]): the source holds that href. +pub(super) fn sources(xml: &roxmltree::Document, markers: &str, href: impl Fn(&str) -> Option>) -> HashMap { if !markers.contains(" HashMap` href that `href` gives a written href for put back as written, XML-escaped. +fn hrefs_as_written(n: roxmltree::Node, mut xml: String, href: &impl Fn(&str) -> Option>) -> String { + // `element_xml` adds namespace declarations to the start tag only, ahead of every child. + let (start, end) = (n.range().start, n.range().end); + let Some(shift) = xml.len().checked_sub(end - start) else { return xml }; + let mut written: Vec<_> = n + .descendants() + .filter(|p| p.is_element() && p.tag_name().name() == "feImage") + .flat_map(|p| p.attributes()) + .filter(|a| a.name() == "href" && matches!(a.namespace(), None | Some("http://www.w3.org/1999/xlink"))) + .filter_map(|a| { + let r = a.range_value(); + // A value written through an entity lies outside the element's text: it stays. + (r.start > start && r.end < end).then_some(())?; + Some((r.start - start + shift..r.end - start + shift, a.value(), href(a.value())?)) + }) + .collect(); + written.sort_by_key(|(r, ..)| std::cmp::Reverse(r.start)); + for (r, key, h) in written { + if xml.get(r.clone()) == Some(key) { + xml.replace_range(r, &crate::xml_escape(&h)); + } + } + xml +} + /// The element name of primitive `k` (`feGaussianBlur`). fn element_name(k: &Kind) -> String { let debug = format!("{k:?}");