From 62d0f86d12d72da995e43d9bdda3df90c3dcac09 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 15:32:07 +0000 Subject: [PATCH] fix(xfa): index saved-state names once and bound existing-image discovery Saved-state matching scanned every template sibling for each saved one, quadratic in a wide form. earlier_images rescanned shared page resources and counted only accepted images, not the entries it examined. --- crates/xfa/src/parse.rs | 11 +++++++++-- crates/xfa/src/pdf.rs | 24 +++++++++++++++++++++--- crates/xfa/src/tests.rs | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index 9ca6a7f3cd..ef258e81ee 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -793,6 +793,14 @@ fn apply_saved_state(nodes: &mut [Node], saved: XmlNode, depth: usize) { return; } let mut seen: std::collections::HashMap<&str, usize> = std::collections::HashMap::new(); + // The template siblings of each name, in order, indexed once: scanning every sibling for each + // saved one is quadratic in a wide form. + let mut by_name: std::collections::HashMap> = std::collections::HashMap::new(); + for (i, n) in nodes.iter().enumerate() { + if let Some(name) = n.common().name.as_deref() { + by_name.entry(name.to_owned()).or_default().push(i); + } + } for c in saved.children().filter(|c| c.is_element()) { if !matches!(c.tag_name().name(), "subform" | "subformSet" | "area" | "field" | "draw" | "exclGroup") { continue; @@ -803,9 +811,8 @@ fn apply_saved_state(nodes: &mut [Node], saved: XmlNode, depth: usize) { *k += 1; *k - 1 }; - let same: Vec = nodes.iter().enumerate().filter(|(_, n)| n.common().name.as_deref() == Some(name)).map(|(i, _)| i).collect(); // More saved objects of the name than the template has: further instances. - let Some(target) = same.get(nth).copied().and_then(|i| nodes.get_mut(i)) else { continue }; + let Some(target) = by_name.get(name).and_then(|same| same.get(nth)).copied().and_then(|i| nodes.get_mut(i)) else { continue }; let presence = c.attribute("presence").map(|p| Presence::parse(Some(p))); match target { Node::Subform(s) => { diff --git a/crates/xfa/src/pdf.rs b/crates/xfa/src/pdf.rs index 8e9b30c00d..d935b80701 100644 --- a/crates/xfa/src/pdf.rs +++ b/crates/xfa/src/pdf.rs @@ -62,6 +62,8 @@ const IMAGE_SHA_KEY: &[u8] = b"PCXfaSHA256"; const MAX_PAGES_SEARCHED: usize = 10_000; /// Most pictures remembered. const MAX_REMEMBERED_IMAGES: usize = 1000; +/// Most XObject entries read while looking for those images, reusable or not. +const MAX_IMAGES_EXAMINED: usize = 100_000; struct Emitter<'a> { doc: &'a mut Document, @@ -885,12 +887,28 @@ fn has_host(after: &str) -> bool { fn earlier_images(doc: &Document, pages: ObjRef) -> HashMap<[u8; 32], ObjRef> { let mut out = HashMap::new(); let kids = doc.get(pages).as_dict().and_then(|d| d.get(b"Kids").map(|k| doc.resolve(k))).and_then(|k| k.as_array().cloned()).unwrap_or_default(); + // A page, resource or XObject dictionary listed again is read once, and every entry read + // counts, whether or not it is a reusable image. + let mut seen = std::collections::HashSet::new(); + let mut examined = 0usize; for page in kids.iter().filter_map(Object::as_ref).take(MAX_PAGES_SEARCHED) { + if !seen.insert(page) { + continue; + } let page = doc.get(page); - let Some(resources) = page.as_dict().and_then(|p| p.get(b"Resources")).map(|r| doc.resolve(r)) else { continue }; - let Some(xobjects) = resources.as_dict().and_then(|r| r.get(b"XObject")).map(|x| doc.resolve(x)) else { continue }; + let Some(res_entry) = page.as_dict().and_then(|p| p.get(b"Resources")) else { continue }; + if res_entry.as_ref().is_some_and(|r| !seen.insert(r)) { + continue; + } + let resources = doc.resolve(res_entry); + let Some(xo_entry) = resources.as_dict().and_then(|r| r.get(b"XObject")) else { continue }; + if xo_entry.as_ref().is_some_and(|r| !seen.insert(r)) { + continue; + } + let xobjects = doc.resolve(xo_entry); for r in xobjects.as_dict().into_iter().flat_map(|x| x.iter()).filter_map(|(_, v)| v.as_ref()) { - if out.len() >= MAX_REMEMBERED_IMAGES { + examined += 1; + if out.len() >= MAX_REMEMBERED_IMAGES || examined > MAX_IMAGES_EXAMINED { return out; } if let Object::Stream(s) = &*doc.get(r) diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 6d93e1bc99..6a8fd7fd34 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4531,3 +4531,22 @@ fn a_container_is_as_wide_as_a_turned_draw_stands() { assert!((got - right).abs() < 0.01, "{rotate}: the neighbour stands {got} right of the anchor, not {right}"); } } + +/// The saved state of a wide form is matched through one index of the template's names: 20,000 +/// same-named fields used to be scanned once for each of their 20,000 saved states. +#[test] +fn saved_state_of_a_wide_form_is_matched_by_index() { + const N: usize = 20_000; + let fields = "".repeat(N); + let saved = format!("{}", "".repeat(N - 1)); + let xml = format!( + "
{saved}
" + ); + let start = std::time::Instant::now(); + let (t, _) = parse(&xml).unwrap(); + assert!(start.elapsed() < std::time::Duration::from_secs(10), "{:?}", start.elapsed()); + let presence: Vec = t.root.children.iter().map(|n| n.common().presence).collect(); + assert_eq!(presence.len(), N); + assert_eq!(presence.last(), Some(&Presence::Hidden), "the last saved state reaches the last field"); + assert!(presence.iter().take(N - 1).all(|p| *p == Presence::Visible)); +}