fix(preflight): find annotation pages from one pass over the pages

verify found each annotation's page by copying and searching every page's
/Annots array again for each annotation, so a page with many annotations
cost quadratic time. The annotation-to-page map is built once (the first
page that lists an annotation keeps it, as before).
This commit is contained in:
aiongg
2026-10-22 17:57:53 +00:00
parent 0a30b4e081
commit adab150a45
2 changed files with 38 additions and 11 deletions
+20 -11
View File
@@ -416,6 +416,12 @@ fn uses_device_colour(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)])
used
}
#[cfg(test)]
thread_local! {
/// Page `/Annots` entries read while matching annotations to their pages.
static ANNOT_ENTRIES_READ: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Check `doc` against `level`.
pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
let mut out = Vec::new();
@@ -486,17 +492,20 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
}
// Pages: annotations and page actions.
let pages = pdfcraft_model::pages(doc);
let page_of = |r: ObjRef| {
pages.iter().position(|p| {
p.dict
.get(b"Annots")
.map(|a| doc.resolve(a))
.and_then(|a| a.as_array().cloned())
.unwrap_or_default()
.iter()
.any(|x| x.as_ref() == Some(r))
})
};
// Each annotation's page: the first page that lists it.
let mut annot_pages: std::collections::HashMap<ObjRef, usize> = std::collections::HashMap::new();
for (i, p) in pages.iter().enumerate() {
if let Some(a) = p.dict.get(b"Annots").map(|a| doc.resolve(a))
&& let Some(a) = a.as_array()
{
#[cfg(test)]
ANNOT_ENTRIES_READ.with(|n| n.set(n.get() + a.len()));
for r in a.iter().filter_map(Object::as_ref) {
annot_pages.entry(r).or_insert(i);
}
}
}
let page_of = |r: ObjRef| annot_pages.get(&r).copied();
for (i, p) in pages.iter().enumerate() {
if p.dict.get(b"AA").is_some() {
issue("6.5.2", "Page {} has additional actions", Some((i + 1).to_string()), Some(i), true);
+18
View File
@@ -430,3 +430,21 @@ fn content_colour_reads_operators_one_at_a_time_and_stops_when_all_are_found() {
assert_eq!(read, 3);
}
/// Each annotation's page is found from one pass over the pages' `/Annots`, not by searching
/// every page's list again for each annotation.
#[test]
fn annotation_pages_are_found_in_one_pass() {
let n = 2_000;
let annots: Vec<String> = (0..n).map(|i| format!("{} 0 R", i + 4)).collect();
let page = format!("<< /Type /Page /Parent 2 0 R /Annots [{}] >>", annots.join(" "));
let mut objs =
vec!["<< /Type /Catalog /Pages 2 0 R >>".to_string(), "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>".into(), page];
objs.extend((0..n).map(|_| "<< /Type /Annot /Subtype /Link /Rect [0 0 0 0] /P 3 0 R >>".to_string()));
let refs: Vec<&str> = objs.iter().map(String::as_str).collect();
let doc = build(&refs, "");
ANNOT_ENTRIES_READ.with(|c| c.set(0));
let issues = verify(&doc, Level::A2b);
assert!(issues.iter().any(|i| i.clause == "6.3.2" && i.page == Some(0)), "{issues:#?}");
let read = ANNOT_ENTRIES_READ.with(std::cell::Cell::get);
assert!(read <= 2 * n, "{read} entries read for {n} annotations");
}