SVG import: read the text once for the prepass's markers, with the data URLs left out

The prepass decided what to look for in the tree by a dozen substring searches over the whole
SVG text (`:not(`, the event attributes, `script`, `transform:` and nine more), and a separate
pass counted the elements' depth before roxmltree. With an embedded picture the text is mostly
its data URL: a 1.35 MB card SVG (a 968 KB PNG) paid about 7 ms of searches and 1 ms of depth
scan per import, on a 64 ms render, and a 54-page PDF export 54 times that.

`depth::prescan` is the one pass: it counts the depth as `xml_too_deep` did (the DOCTYPE entity
count included) and, in the same walk, notes every `data:` attribute value, answering the text
with those values left out, their quotes kept. The prepass and `fx::blends` search that text;
the edits are still made in the source. A data URL can hold no tag, attribute name, selector or
CSS declaration, and everything else, a `style="display:none"` included, is kept, so what is
found is what was found before.

`vectorcraft-cli perf --deep` gains a tracked row, "import SVG, one embedded PNG", beside the
export row it had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Drew Dibble
2026-10-23 01:01:27 +07:00
committed by bflatastic
co-authored by Claude Fable 5.1
parent 48792842ec
commit 5b919c0529
4 changed files with 131 additions and 33 deletions
+22 -2
View File
@@ -23,13 +23,16 @@ impl Rng {
}
}
/// The synthetic document's page, in points.
const SYNTHETIC_PAGE: (f64, f64) = (1600.0, 1200.0);
/// `n` mixed paths (ellipses with strokes, translucent rectangles, stroked stars) on a 1600×1200 page.
pub fn synthetic(n: usize) -> Result<Document, String> {
let mut d = Document::new(1600.0, 1200.0);
let mut d = Document::new(SYNTHETIC_PAGE.0, SYNTHETIC_PAGE.1);
let l = d.layers.first().map(|l| l.id);
let mut r = Rng(42);
for i in 0..n {
let (x, y, s) = (r.next() * 1600.0, r.next() * 1200.0, 3.0 + r.next() * 22.0);
let (x, y, s) = (r.next() * SYNTHETIC_PAGE.0, r.next() * SYNTHETIC_PAGE.1, 3.0 + r.next() * 22.0);
let c = Color::rgb(r.next() as f32, r.next() as f32, r.next() as f32);
let id = d.alloc_id();
let mut node = match i % 3 {
@@ -296,6 +299,23 @@ fn deep(n: usize) -> Result<(), String> {
rows.push(("render 4 embedded 24 MP photos with 3 renderers (cold)".into(), ms));
note("after rendering the photos with 3 renderers");
}
// An SVG with one embedded picture opened from its text, as picture-heavy SVGs come: the
// importer's passes over the text before usvg reads it, most of which is the data URL. The
// picture is the synthetic page drawn at 1.25x (about a megabyte as a PNG, the size at which
// the passes were measured); the hidden rectangle is a marker, so the prepass walks the tree
// as it does for a file that has one, instead of returning at its first check.
{
let k = 1.25;
let (w, h) = ((SYNTHETIC_PAGE.0 * k).round() as u32, (SYNTHETIC_PAGE.1 * k).round() as u32);
let photo = Renderer::new().render(&synthetic(3000)?, w, h, Affine::scale(k), &opts).to_png()?;
let data = vectorcraft_svg::base64_encode(&photo);
let svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}"><image width="{w}" height="{h}" href="data:image/png;base64,{data}"/><rect width="100" height="100" style="display:none"/></svg>"#
);
let ms = median_ms(5, || drop(vectorcraft_svg::import_with_report(&svg)));
rows.push((format!("import SVG, one embedded {} KB PNG", photo.len() / 1024), ms));
note("after the import of an embedded picture");
}
// Output hashes, to compare builds: the SVG export and a save → open → save round trip.
let fnv = |b: &[u8]| b.iter().fold(0xcbf2_9ce4_8422_2325u64, |h, &x| (h ^ u64::from(x)).wrapping_mul(0x100_0000_01b3));
let doc = synthetic(n)?;