mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 14:08:23 -06:00
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:
+20
-11
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user