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.
This commit is contained in:
Dylan M. Taylor
2026-11-03 15:32:07 +00:00
parent 14ae839a28
commit 62d0f86d12
3 changed files with 49 additions and 5 deletions
+9 -2
View File
@@ -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<String, Vec<usize>> = 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<usize> = 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) => {
+21 -3
View File
@@ -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)
+19
View File
@@ -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 = "<field name=\"f\"/>".repeat(N);
let saved = format!("{}<field name=\"f\" presence=\"hidden\"/>", "<field name=\"f\"/>".repeat(N - 1));
let xml = format!(
"<xdp:xdp xmlns:xdp=\"http://ns.adobe.com/xdp/\"><template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"form1\">{fields}</subform></template><form xmlns=\"http://www.xfa.org/schema/xfa-form/2.8/\"><subform name=\"form1\">{saved}</subform></form></xdp:xdp>"
);
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<Presence> = 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));
}