fix(a11y): collect a structure element once whatever generation references it

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.
This commit is contained in:
Dylan M. Taylor committed 2026-11-03 16:48:21 +00:00
1 parent 14ae839a28
commit 7f381eb494
2 files changed
+19 -1

No files matched your search

+3 -1
View File
@@ -99,7 +99,9 @@ pub(crate) fn elements(doc: &Document) -> Vec<Element> {
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 };
+16
View File
@@ -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);
}