From 3bf6c6f027f5bad86a4a16fccf955b87d449266e Mon Sep 17 00:00:00 2001 From: Grzegorz Martens Date: Thu, 5 Nov 2026 08:44:13 +0100 Subject: [PATCH] M2.6: marking content as an artifact refuses what may be past the damage A tag's content not found before the damage in a damaged stream may be past it. Marking a figure decorative took it for absent: it succeeded, took the figure out of the structure tree and left its content tagged. Marking a tag as an artifact said its content wasn't on the page. Both now refuse with `Damaged`, as when the content is before the damage, and change nothing. Clearing the parent tree read the page's `/StructParents` only when direct, so with it given by reference (ISO 32000-2 lets it be) the tree went on pointing the content at the retired tag. It is read through the reference. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY --- crates/a11y/src/alt.rs | 11 ++++++++--- crates/a11y/src/tests.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index dd4b43a641..bdc72d2e33 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -417,7 +417,7 @@ enum Marked { /// None of the content is on the page, or with `require_all` not all of it: nothing changed. NotFound, /// Some of it is in a stream damaged partway, which can't be rewritten without losing what - /// follows the damage: nothing changed. + /// follows the damage, or may be past the damage: nothing changed. Damaged, } @@ -442,6 +442,11 @@ fn artifact_marked_content( edits.push((op.span, b"/Artifact BMC".to_vec())); } } + // Content not found may be past the damage in a damaged stream: whether it is on the page + // isn't known. + if found != *ids && streams.iter().any(|(_, s)| s.as_ref().is_some_and(|(_, _, damaged)| *damaged)) { + return Ok(Marked::Damaged); + } if if require_all { found != *ids } else { found.is_empty() } { return Ok(Marked::NotFound); } @@ -494,7 +499,7 @@ pub fn mark_decorative(doc: &mut Document, obj: ObjRef) -> Result<(), AltError> match artifact_marked_content(doc, page, streams, &ids, false)? { // The parent tree no longer points those ids at the figure. Marked::Done => { - if let Some(key) = page.dict.get(b"StructParents").and_then(Object::as_int) { + if let Some(key) = page.dict.get(b"StructParents").and_then(|o| doc.resolve(o).as_int()) { clear_parent_tree(doc, key, obj)?; } } @@ -545,7 +550,7 @@ pub fn mark_tag_artifact(doc: &mut Document, obj: ObjRef) -> Result<(), Artifact Marked::NotFound => return Err(ArtifactError::ContentNotFound), Marked::Damaged => return Err(ArtifactError::Damaged), } - if let Some(key) = page.dict.get(b"StructParents").and_then(Object::as_int) { + if let Some(key) = page.dict.get(b"StructParents").and_then(|o| doc.resolve(o).as_int()) { clear_parent_tree(doc, key, obj).map_err(|e| ArtifactError::ParentTree(e.to_string()))?; } doc.update_dict(parent, |p| p.set(b"K".to_vec(), replacement))?; diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index a129f207cd..78a6d96589 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -1129,6 +1129,42 @@ fn a_tags_content_in_a_stream_damaged_partway_is_not_rewritten() { assert_ne!(list.get(1), Some(&Object::Ref(ObjRef::new(4, 0))), "the tag's stream is rewritten"); } +/// A tag's content not found before the damage in a damaged stream may be past it: it isn't known +/// to be off the page, so neither marking changes anything. +#[test] +fn a_tags_content_past_the_damage_is_not_taken_for_absent() { + use pdfcraft_cos::ObjRef; + let damaged = + damaged_flate(b"/Photo << /MCID 1 >> BDC 100 100 50 30 re f EMC\n", b"/Figure << /MCID 0 >> BDC q 40 0 0 20 10 150 cm /Im Do Q EMC"); + let mut doc = figures_doc(); + doc.set(ObjRef::new(4, 0), damaged); + let contents = |doc: &Document| doc.get(ObjRef::new(3, 0)).as_dict().unwrap().get(b"Contents").cloned(); + let before = (contents(&doc), doc.get(ObjRef::new(9, 0)), doc.get(ObjRef::new(6, 0))); + assert_eq!(mark_decorative(&mut doc, ObjRef::new(7, 0)), Err(AltError::Damaged)); + assert_eq!(mark_tag_artifact(&mut doc, ObjRef::new(7, 0)), Err(ArtifactError::Damaged)); + assert_eq!((contents(&doc), doc.get(ObjRef::new(9, 0)), doc.get(ObjRef::new(6, 0))), before); +} + +/// A page's `/StructParents` given by reference is read through it: the parent tree no longer +/// points the marked content at the tag. +#[test] +fn struct_parents_given_by_reference_are_read() { + use pdfcraft_cos::{ObjRef, Object}; + let by_reference = || { + let mut doc = figures_doc(); + let key = doc.add(Object::Int(0)); + doc.update_dict(ObjRef::new(3, 0), |p| p.set(b"StructParents".to_vec(), Object::Ref(key))).unwrap(); + doc + }; + let nums = |doc: &Document| doc.get(ObjRef::new(9, 0)).as_dict().and_then(|d| d.get(b"Nums").cloned()); + let mut doc = by_reference(); + mark_decorative(&mut doc, ObjRef::new(7, 0)).unwrap(); + assert_eq!(nums(&doc), Some(Object::Array(vec![Object::Int(0), Object::Array(vec![Object::Null, Object::Ref(ObjRef::new(8, 0))])]))); + let mut doc = by_reference(); + mark_tag_artifact(&mut doc, ObjRef::new(8, 0)).unwrap(); + assert_eq!(nums(&doc), Some(Object::Array(vec![Object::Int(0), Object::Array(vec![Object::Ref(ObjRef::new(7, 0)), Object::Null])]))); +} + /// A content stream that doesn't decode at all is kept as stored, and a tag in the page's other /// streams is still marked: only running past the decode budget refuses. #[test]