Accessibility check asks for a manual check where page content ran past the decode budget

This commit is contained in:
aiongg committed 2026-10-22 19:51:47 +00:00
1 parent 089882f372
commit 9d5bcc0745
3 files changed
+59 -2

No files matched your search

+19 -2
View File
@@ -20,6 +20,8 @@ pub(crate) struct Scan {
pub untagged: BTreeMap<usize, usize>,
/// Page → fonts used.
pub fonts: BTreeMap<usize, BTreeSet<ObjRef>>,
/// Pages read only in part: content or forms past the page's decode budget are missing.
pub incomplete: BTreeSet<usize>,
}
const MAX_FORM_DEPTH: usize = 12;
@@ -32,6 +34,9 @@ impl Scan {
let res = page.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
let mut budget = DecodeBudget::page();
let data = crate::alt::page_content_within(doc, &page.dict, &mut budget);
if budget.remaining() == 0 {
s.incomplete.insert(p);
}
let mut w = Walk { doc, scan: &mut s, page: p, forms: HashSet::new(), budget };
w.content(&data, &res, false, 0);
}
@@ -100,7 +105,20 @@ impl Walk<'_> {
self.scan.has_images = true;
untagged += usize::from(!inside);
}
Some(b"Form") if depth < MAX_FORM_DEPTH && !self.forms.contains(&r) && self.budget.enter_form() => {
Some(b"Form") if depth < MAX_FORM_DEPTH && !self.forms.contains(&r) => {
if !self.budget.enter_form() {
self.scan.incomplete.insert(self.page);
continue;
}
let data = match self.budget.decode(x) {
Ok(data) => data,
Err(_) => {
if self.budget.exhausted() {
self.scan.incomplete.insert(self.page);
}
continue;
}
};
self.forms.insert(r);
let inner = x
.dict
@@ -108,7 +126,6 @@ impl Walk<'_> {
.map(|o| self.doc.resolve(o))
.and_then(|o| o.as_dict().cloned())
.unwrap_or_else(|| res.clone());
let data = self.budget.decode(x).unwrap_or_default();
self.content(&data, &inner, inside, depth + 1);
self.forms.remove(&r);
}
+12
View File
@@ -681,6 +681,18 @@ pub fn check(doc: &Document, options: &Options) -> Report {
}
}
}
// A page read only in part may hold what the scan didn't find: text that makes the
// document more than images, untagged content, or a font without Unicode mappings.
let unsure = !scan.incomplete.is_empty()
&& match rule {
Rule::ImageOnly => !scan.has_text,
Rule::TaggedContent | Rule::CharacterEncoding => findings.is_empty(),
_ => false,
};
if unsure {
results.push(RuleResult { rule, status: Status::Manual, findings: Vec::new() });
continue;
}
if total > MAX_FINDINGS {
let more = total - MAX_FINDINGS;
findings.truncate(MAX_FINDINGS);
+28
View File
@@ -1278,3 +1278,31 @@ fn marking_decorative_refuses_content_past_the_decode_budget() {
assert!(mark_decorative(&mut doc, ObjRef::new(7, 0)).is_err());
assert_eq!(figures(&doc).len(), 2, "the figure stays");
}
/// Text past the page's decode budget is unread, so a page that shows images and no text so far
/// can't be called image-only, nor its content all tagged.
#[test]
fn content_past_the_decode_budget_needs_a_manual_check() {
use pdfcraft_cos::{Dict, Object, ObjRef, Stream};
let mut doc = pdf(
&[
"<< /Type /Catalog /Pages 2 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R /Resources << /XObject << /Im 5 0 R >> /Font << /F1 6 0 R >> >> >>".into(),
stream("/Artifact BMC q 200 0 0 200 0 0 cm /Im Do Q EMC"),
"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\n\u{0}\nendstream"
.into(),
FONT.into(),
],
"",
);
let filler = doc.add(Stream::flate(Dict::new(), &vec![b' '; 1 << 20]));
let text = doc.add(Stream::from_raw(Dict::new(), b"BT /F1 12 Tf 10 10 Td (Hello) Tj ET".to_vec()));
let mut contents = vec![Object::Ref(ObjRef::new(4, 0))];
contents.extend(std::iter::repeat_n(Object::Ref(filler), 256));
contents.push(Object::Ref(text));
doc.update_dict(ObjRef::new(3, 0), |p| p.set(b"Contents".to_vec(), Object::Array(contents))).unwrap();
let report = check(&doc, &Options::default());
assert_eq!(status(&report, Rule::ImageOnly), Status::Manual);
assert_eq!(status(&report, Rule::TaggedContent), Status::Manual);
}