From d9f55b0a692b0acfada1d1e68390bcab593a2e7a Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 9 Nov 2026 09:03:01 +0700 Subject: [PATCH] Split PDF/A verify into one helper per group of clause checks verify() went past clippy's cognitive complexity limit once every check was in it. The checks move as they are into helpers, run in the same order, so the issues and their order don't change. --- crates/preflight/src/lib.rs | 138 ++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 38 deletions(-) diff --git a/crates/preflight/src/lib.rs b/crates/preflight/src/lib.rs index 64ca00311e..d5a6d80b04 100644 --- a/crates/preflight/src/lib.rs +++ b/crates/preflight/src/lib.rs @@ -586,7 +586,48 @@ pub fn verify(doc: &Document, level: Level) -> Vec { let message = fill(pattern, None, &values); push(Issue { clause, message, pattern, value: None, values, page: None, object: Some(object), fixable }); }; - let cat = catalog(doc); + check_identification(doc, level, &mut issue); + let objs = objects(doc); + check_xmp_packets(&objs, &located_issue); + check_output_intent(doc, &objs, &mut issue); + check_catalog_actions(doc, &mut issue); + // Pages: annotations and page actions. + let pages = pdfcraft_model::pages(doc); + // Each annotation's page: the first page that lists it. + let mut annot_pages: std::collections::HashMap = 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); + } + } + check_transparency(doc, &pages, &mut issue); + let drawing = drawing_fonts(doc, &objs); + check_objects(doc, level, &objs, &drawing, &page_of, &mut issue); + check_limits(doc, &objs, &pages, &mut issue); + check_spots(doc, &objs, &mut issue); + check_graphics_states(doc, &mut issue); + // One message per kind of problem is enough for repeated ones (as preflight summaries do). + let mut out = out.into_inner(); + let mut seen = std::collections::HashSet::new(); + out.retain(|i| seen.insert((i.clause, i.message.clone(), i.page))); + out +} + +/// 6.1.2, 6.1.3 and 6.6: identification and metadata, the file header, the file identifier and +/// encryption. +fn check_identification(doc: &Document, level: Level, issue: &mut impl FnMut(&'static str, &'static str, Option, Option, bool)) { // 6.1.3 / 6.6: identification and metadata. match metadata(doc) { None => issue("6.6.2.1", "The document has no XMP metadata", None, None, true), @@ -622,12 +663,16 @@ pub fn verify(doc: &Document, level: Level) -> Vec { if doc.trailer().get(b"Encrypt").is_some() { issue("6.1.3", "The document is encrypted", None, None, true); } - // 6.2.2: output intent when device colour is used. - let objs = objects(doc); - // 6.6.2.3.1 and 6.6.2.3.3: predefined XMP value types and extension schema descriptions, in - // every metadata stream. +} + +/// 6.6.2.3.1 and 6.6.2.3.3: predefined XMP value types and extension schema descriptions, in +/// every metadata stream. +fn check_xmp_packets( + objs: &[(ObjRef, std::sync::Arc)], + located_issue: &impl Fn(&'static str, &'static str, Vec<(&'static str, String)>, ObjRef, bool), +) { let (mut mistyped, mut extension) = (Vec::new(), xmp_extension::Findings::default()); - xmp_types::for_each_packet(&objs, |object, packet| { + xmp_types::for_each_packet(objs, |object, packet| { mistyped.extend(xmp_types::mistyped(packet).into_iter().map(|((name, written), ty)| (object, name, written, ty))); extension.add(object, packet); }); @@ -652,6 +697,14 @@ pub fn verify(doc: &Document, level: Level) -> Vec { ); } } + report_xmp_extension(extension, located_issue); +} + +/// 6.6.2.3.3: what the extension schema descriptions of the metadata streams lack. +fn report_xmp_extension( + extension: xmp_extension::Findings, + located_issue: &impl Fn(&'static str, &'static str, Vec<(&'static str, String)>, ObjRef, bool), +) { for m in extension.missing { let mut values = vec![("{schema}", m.schema), ("{object}", m.object.to_string()), ("{entry}", m.entry)]; match (m.item, m.empty) { @@ -733,7 +786,15 @@ pub fn verify(doc: &Document, level: Level) -> Vec { ); } } - let used = uses_device_colour(doc, &objs); +} + +/// 6.2.3 and 6.2.4.3: the output intent device colour needs. +fn check_output_intent( + doc: &Document, + objs: &[(ObjRef, std::sync::Arc)], + issue: &mut impl FnMut(&'static str, &'static str, Option, Option, bool), +) { + let used = uses_device_colour(doc, objs); match pdfa_intent_profile(doc) { None if used.any() => { issue("6.2.3", "Device colour is used, but there is no PDF/A output intent", None, None, !used.cmyk); @@ -752,7 +813,11 @@ pub fn verify(doc: &Document, level: Level) -> Vec { } None => {} } +} + +fn check_catalog_actions(doc: &Document, issue: &mut impl FnMut(&'static str, &'static str, Option, Option, bool)) { // 6.1.3 / 6.6.1: catalog actions. + let cat = catalog(doc); if cat.get(b"AA").is_some() { issue("6.5.2", "The document has additional actions (/AA)", None, None, true); } @@ -766,27 +831,13 @@ pub fn verify(doc: &Document, level: Level) -> Vec { { issue("6.4.1", "The form asks viewers to make appearances (NeedAppearances)", None, None, true); } - // Pages: annotations and page actions. - let pages = pdfcraft_model::pages(doc); - // Each annotation's page: the first page that lists it. - let mut annot_pages: std::collections::HashMap = 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); - } - } +} + +fn check_transparency( + doc: &Document, + pages: &[pdfcraft_model::Page], + issue: &mut impl FnMut(&'static str, &'static str, Option, Option, bool), +) { // 6.2.10: blend modes are those ISO 32000-1 defines, and without a PDF/A output intent (one with // a profile) a page with transparency names a blending colour space. let intent = pdfa_intent_has_profile(doc); @@ -802,10 +853,16 @@ pub fn verify(doc: &Document, level: Level) -> Vec { issue("6.2.10", "Page {} has transparency but no device-independent blending colour space or PDF/A output intent", n, Some(i), false); } } - let drawing = drawing_fonts(doc, &objs); - check_objects(doc, level, &objs, &drawing, &page_of, &mut issue); +} + +fn check_limits( + doc: &Document, + objs: &[(ObjRef, std::sync::Arc)], + pages: &[pdfcraft_model::Page], + issue: &mut impl FnMut(&'static str, &'static str, Option, Option, bool), +) { // 6.1.13: the implementation limits of ISO 32000-1 Annex C. - let over = limits::exceeded(doc, &objs, &referenced_content(doc, &objs)); + let over = limits::exceeded(doc, objs, &referenced_content(doc, objs)); if over.int { issue("6.1.13", "An integer is outside the 32-bit range", None, None, false); } @@ -815,7 +872,7 @@ pub fn verify(doc: &Document, level: Level) -> Vec { if over.string { issue("6.1.13", "A string is longer than 32767 bytes", None, None, false); } - if limits::cid_over_limit(doc, &objs) { + if limits::cid_over_limit(doc, objs) { issue("6.1.13", "A CMap maps a character code to a CID above 65535", None, None, false); } if over.name { @@ -826,14 +883,24 @@ pub fn verify(doc: &Document, level: Level) -> Vec { issue("6.1.13", "Page {} has a boundary smaller than 3 or larger than 14400 units", Some((i + 1).to_string()), Some(i), false); } } +} + +fn check_spots( + doc: &Document, + objs: &[(ObjRef, std::sync::Arc)], + issue: &mut impl FnMut(&'static str, &'static str, Option, Option, bool), +) { // 6.2.4.4: Separation and DeviceN colour spaces. - let spots = spot::check(doc, &objs); + let spots = spot::check(doc, objs); for name in spots.differing { issue("6.2.4.4", "Separation colour spaces named {} differ in their alternate space or tint transform", Some(name), None, false); } for name in spots.undefined { issue("6.2.4.4", "The spot colour {} of a DeviceN colour space isn't in its Colorants dictionary", Some(name), None, false); } +} + +fn check_graphics_states(doc: &Document, issue: &mut impl FnMut(&'static str, &'static str, Option, Option, bool)) { // 6.2.5: the extended graphics state. let gs = gstate::check(doc); if gs.transfer { @@ -866,11 +933,6 @@ pub fn verify(doc: &Document, level: Level) -> Vec { if gs.intent_not_name { issue("6.2.5", "A graphics state has a rendering intent that isn't a name", None, None, false); } - // One message per kind of problem is enough for repeated ones (as preflight summaries do). - let mut out = out.into_inner(); - let mut seen = std::collections::HashSet::new(); - out.retain(|i| seen.insert((i.clause, i.message.clone(), i.page))); - out } fn check_objects(