mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
fix(a11y): auto-tagging bounds the content it plans across all pages (#6090)
This commit is contained in:
1 parent
14ae96a2ac
commit
3a900b8bd2
2 files changed
+27
No files matched your search
@@ -9,6 +9,10 @@ use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object, Stream};
|
||||
const MAX_PAGES: usize = 1_000;
|
||||
const MAX_PAGE_BYTES: usize = 16 * 1024 * 1024;
|
||||
const MAX_OPERATORS: usize = 100_000;
|
||||
/// What all pages together may decode and keep: pages can share one stream, so the per-page limits
|
||||
/// alone let a small file retain a plan for every page. Real documents stay far under these.
|
||||
const MAX_TOTAL_BYTES: usize = 128 * 1024 * 1024;
|
||||
const MAX_TOTAL_OPERATORS: usize = 2_000_000;
|
||||
const MAX_TEXT_OBJECTS: usize = 4_096;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -186,6 +190,7 @@ fn plan(doc: &Document) -> Result<Vec<Plan>, AutoTagError> {
|
||||
}
|
||||
let mut out = Vec::with_capacity(pages.len());
|
||||
let mut total = 0usize;
|
||||
let (mut total_bytes, mut total_ops) = (0usize, 0usize);
|
||||
for (index, page) in pages.iter().enumerate() {
|
||||
if page.dict.contains(b"StructParents") {
|
||||
return Err(unsupported(index, "page already has a structure parent key"));
|
||||
@@ -197,10 +202,15 @@ fn plan(doc: &Document) -> Result<Vec<Plan>, AutoTagError> {
|
||||
return Err(unsupported(index, "annotations need separate structure tags"));
|
||||
}
|
||||
let bytes = content_bytes(doc, page, index)?;
|
||||
total_bytes = total_bytes.saturating_add(bytes.len());
|
||||
let parsed = parse(&bytes);
|
||||
if parsed.skipped > 0 || parsed.ops.len() > MAX_OPERATORS {
|
||||
return Err(unsupported(index, "content has malformed or too many operators"));
|
||||
}
|
||||
total_ops = total_ops.saturating_add(parsed.ops.len());
|
||||
if total_bytes > MAX_TOTAL_BYTES || total_ops > MAX_TOTAL_OPERATORS {
|
||||
return Err(unsupported(index, "the document has too much content to auto-tag in one edit"));
|
||||
}
|
||||
let spans = text_spans(&parsed.ops, index)?;
|
||||
total = total.saturating_add(spans.len());
|
||||
out.push(Plan { page: page.obj, ops: parsed.ops, spans });
|
||||
|
||||
@@ -1763,3 +1763,20 @@ 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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autotag_text_refuses_pages_that_share_content_past_the_total_operator_allowance() {
|
||||
// Each page is just under the per-page limit; sharing one stream makes the total 21 times it.
|
||||
let content = format!("BT (a) Tj ET {}", "q Q ".repeat(49_990));
|
||||
let pages = 21;
|
||||
let kids: Vec<String> = (0..pages).map(|i| format!("{} 0 R", 4 + i)).collect();
|
||||
let mut bodies = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||
format!("<< /Type /Pages /Kids [{}] /Count {pages} /MediaBox [0 0 200 200] >>", kids.join(" ")),
|
||||
stream(&content),
|
||||
];
|
||||
bodies.extend((0..pages).map(|_| "<< /Type /Page /Parent 2 0 R /Contents 3 0 R >>".to_string()));
|
||||
let mut doc = pdf(&bodies, "");
|
||||
let Err(AutoTagError::Unsupported { reason, .. }) = autotag_text(&mut doc) else { panic!("expected a refusal") };
|
||||
assert!(reason.contains("too much content"), "{reason}");
|
||||
}
|
||||
Reference in new issue
Block a user