From 7f381eb494875e34ac200b44361bb45c1fb46694 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 16:48:21 +0000 Subject: [PATCH] fix(a11y): collect a structure element once whatever generation references it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The structure collector deduplicated by full reference while object lookup goes by number, so 3 0 R, 3 1 R, 3 2 R… each cloned the same element and its children again. --- crates/a11y/src/alt.rs | 4 +++- crates/a11y/src/tests.rs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index fde1048e47..efa96c4dfe 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -99,7 +99,9 @@ pub(crate) fn elements(doc: &Document) -> Vec { match k { Object::Array(a) => stack.extend(a.into_iter().rev().map(|x| (x, pg, parent, depth))), Object::Ref(r) => { - if !seen.insert(r) { + // By number: objects are looked up by number, so `4 1 R` is `4 0 R` again, and + // must not be collected (with its children) once per generation written. + if !seen.insert(r.num) { continue; } let Some(d) = doc.get(r).as_dict().cloned() else { continue }; diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index 3024319ec3..6e74e97e92 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -1504,3 +1504,19 @@ fn marking_a_tag_artifact_refuses_content_past_the_decode_budget() { assert_eq!(mark_tag_artifact(&mut doc, ObjRef::new(7, 0)), Err(ArtifactError::TooLarge)); assert_eq!(doc.get(ObjRef::new(3, 0)).as_dict().unwrap().get(b"Contents"), Some(&contents), "the page keeps its content"); } + +/// One structure element referenced under several generations is collected once: lookup goes by +/// object number, so each alias would otherwise copy the element and its children again. +#[test] +fn a_structure_element_under_many_generations_is_collected_once() { + let aliases: String = (0..200).map(|g| format!(" 3 {g} R")).collect(); + let doc = pdf( + &[ + "<< /Type /Catalog /StructTreeRoot 2 0 R >>".into(), + format!("<< /Type /StructTreeRoot /K [{aliases}] >>"), + format!("<< /S /Figure /Alt (x) /K [{aliases}] >>"), + ], + "", + ); + assert_eq!(crate::alt::elements(&doc).len(), 1); +}