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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz MartensandClaude Opus 5.5 committed 2026-11-07 12:37:35 +01:00
1 parent 69e26167e4
commit 3bf6c6f027
2 files changed
+44 -3

No files matched your search

+8 -3
View File
@@ -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))?;
+36
View File
@@ -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]