Tagged PDF: resolve /MarkInfo /Marked when it is an indirect reference

Any value in a PDF may be written as an indirect reference, the tagged
flag included: `/MarkInfo << /Marked 9 0 R >>` with object 9 holding
`true` is valid, and veraPDF's corpus has pass files that write it that
way. Both readers of the flag tested the reference itself, so such a
document was reported as untagged:

    doc_info                     "tagged": false
    Accessibility Checker        "The document isn't marked as tagged
                                  (MarkInfo Marked)"

Both now resolve the value first. On a pair of otherwise identical
tagged pages, one with `/Marked true` and one with `/Marked 8 0 R`:

    before   direct: tagged true    indirect: tagged false
    after    direct: tagged true    indirect: tagged true

and the checker reports the finding for neither.

A document with no flag at all still fails the rule, which the test
asserts alongside, so this does not turn the rule into one that always
passes.

Fixes #3671.
This commit is contained in:
Hamza
2026-10-20 20:28:27 +05:00
parent 6fb6b7cf6f
commit bfc7487384
3 changed files with 28 additions and 2 deletions
+5 -2
View File
@@ -167,12 +167,15 @@ pub(crate) fn document_rule(doc: &Document, rule: Rule, pages: &[Page], scan: &S
}
}
Rule::TaggedPdf => {
let marked = cat.get(b"MarkInfo").map(|m| doc.resolve(m)).and_then(|m| m.as_dict().and_then(|d| d.get(b"Marked").cloned()));
// `/Marked` may be an indirect reference like any other value, so it is resolved
// before the test below, which looks for the boolean itself (#3671).
let marked =
cat.get(b"MarkInfo").map(|m| doc.resolve(m)).and_then(|m| m.as_dict().and_then(|d| d.get(b"Marked").map(|v| doc.resolve(v))));
let mut out = Vec::new();
if !cat.contains(b"StructTreeRoot") {
out.push(Finding::plain(None, "The document has no tags (structure tree)"));
}
if !matches!(marked, Some(Object::Bool(true))) {
if !matches!(marked.as_deref(), Some(Object::Bool(true))) {
out.push(Finding::plain(None, "The document isn't marked as tagged (MarkInfo Marked)"));
}
out
+20
View File
@@ -192,6 +192,26 @@ fn an_untagged_document_fails_the_document_rules() {
assert_eq!((only.count(Status::Failed), only.count(Status::Skipped)), (1, 31));
}
/// `/MarkInfo /Marked` may be written as an indirect reference, like any value. The rule read the
/// reference itself and so reported a tagged document as untagged (#3671).
#[test]
fn marked_as_an_indirect_reference_still_counts_as_tagged() {
let mut bodies = good();
// The catalog is object 1; point /Marked at a new object holding `true`.
let last = bodies.len() + 1;
bodies[0] = bodies[0].replace("/MarkInfo << /Marked true >>", &format!("/MarkInfo << /Marked {last} 0 R >>"));
bodies.push("true".into());
let doc = pdf(&bodies, "");
let report = check(&doc, &Options { rules: [Rule::TaggedPdf].into(), pages: None });
assert_eq!(status(&report, Rule::TaggedPdf), Status::Passed, "{:?}", failed(&report));
// The direct form is unchanged, and a document with no flag at all still fails.
assert_eq!(status(&check(&pdf(&good(), ""), &Options { rules: [Rule::TaggedPdf].into(), pages: None }), Rule::TaggedPdf), Status::Passed);
let mut none = good();
none[0] = none[0].replace("/MarkInfo << /Marked true >>", "");
assert_eq!(status(&check(&pdf(&none, ""), &Options { rules: [Rule::TaggedPdf].into(), pages: None }), Rule::TaggedPdf), Status::Failed);
}
/// A tagged document that passes every automatic rule.
fn good() -> Vec<String> {
vec![
+3
View File
@@ -643,6 +643,9 @@ impl<'a> Inspector<'a> {
.ok()
.and_then(|o| self.dict(o))
.and_then(|m| m.get(b"Marked").ok())
// Any value may be written as an indirect reference, `/Marked 9 0 R` included, so the
// flag is resolved before it is read as a boolean (#3671).
.map(|o| self.resolve(o))
.and_then(|o| o.as_bool().ok())
.unwrap_or(false);
match self.page_labels(catalog, &mut info.pages) {