Merge #6172: M11: PDF/A verify asks only the fonts something draws with to be embedded (optimize.pdfa-validate)

# Conflicts:
#	crates/preflight/src/tests.rs
This commit is contained in:
bflatastic committed 2026-11-09 08:03:56 +07:00
commit 4f90d2295f
2 files changed
+72 -6

No files matched your search

+40 -6
View File
@@ -378,6 +378,37 @@ fn referenced_content(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)])
out
}
/// The fonts the document draws with: those a resource dictionary names (`/Resources`, of a page or
/// a page tree node, a form XObject or an annotation's appearance stream, a pattern or a Type 3
/// font). PDF/A asks that every font "used for rendering" be embedded (ISO 19005-2, 6.2.11.4.1). A
/// font that only the interactive form's default resources hold (`/AcroForm /DR`, ISO 32000-1
/// 12.7.2) draws nothing, so it needn't be: an appearance stream that does draw with it names it in
/// its own `/Resources`.
fn drawing_fonts(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)]) -> std::collections::HashSet<ObjRef> {
let mut out = std::collections::HashSet::new();
for (_, o) in objs {
let Some(resources) = dict_of(o).and_then(|d| d.get(b"Resources")).map(|r| doc.resolve(r)) else { continue };
let Some(fonts) = resources.as_dict().and_then(|r| r.get(b"Font")).map(|f| doc.resolve(f)) else { continue };
if let Some(fonts) = fonts.as_dict() {
out.extend(fonts.iter().filter_map(|(_, f)| f.as_ref()).map(|r| font_object(doc, r)));
}
}
out
}
/// How many references [`font_object`] follows, as the document's own `resolve` does.
const MAX_REFERENCE_HOPS: usize = 32;
/// The object a font entry's reference `r` ends at: an object can be nothing but a reference to
/// another (ISO 32000-1 7.3.10), and the font dictionary is the last one in that chain.
fn font_object(doc: &Document, mut r: ObjRef) -> ObjRef {
for _ in 0..MAX_REFERENCE_HOPS {
let Object::Ref(next) = &*doc.get(r) else { break };
r = *next;
}
r
}
/// Whether a stream's own dictionary says it holds content: a form XObject, or a stream with a
/// `/BBox` and no `/Subtype` (a tiling pattern, or an appearance stream that leaves out
/// `/Subtype /Form`). Mesh shadings may carry a `/BBox` too, but their data isn't content.
@@ -681,7 +712,8 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
issue("6.5.2", "Page {} has additional actions", Some((i + 1).to_string()), Some(i), true);
}
}
check_objects(doc, level, &objs, &page_of, &mut issue);
let drawing = drawing_fonts(doc, &objs);
check_objects(doc, level, &objs, &drawing, &page_of, &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();
@@ -693,28 +725,29 @@ fn check_objects(
doc: &Document,
level: Level,
objects: &[(ObjRef, std::sync::Arc<Object>)],
drawing: &std::collections::HashSet<ObjRef>,
page_of: &impl Fn(ObjRef) -> Option<usize>,
issue: &mut impl FnMut(&'static str, &'static str, Option<String>, Option<usize>, bool),
) {
for (r, o) in objects {
let Some(d) = dict_of(o) else { continue };
check_one_object(doc, level, *r, d, o, page_of, issue);
check_one_object(doc, level, d, o, drawing.contains(r), page_of(*r), issue);
}
}
fn check_one_object(
doc: &Document,
level: Level,
reference: ObjRef,
dict: &Dict,
object: &Object,
page_of: &impl Fn(ObjRef) -> Option<usize>,
draws: bool,
page: Option<usize>,
issue: &mut impl FnMut(&'static str, &'static str, Option<String>, Option<usize>, bool),
) {
let ty = dict.name(b"Type");
let subtype = dict.name(b"Subtype");
check_actions(doc, dict, issue);
if is_annotation(dict, ty, subtype) && check_annotation(doc, dict, subtype, page_of(reference), issue) {
if is_annotation(dict, ty, subtype) && check_annotation(doc, dict, subtype, page, issue) {
// A forbidden annotation stops every later check for its object, as the scan did before
// it was split into helpers: the rest of its own checks and the form field, font, stream
// and embedded file below all came after that `continue`.
@@ -723,7 +756,8 @@ fn check_one_object(
if dict.get(b"FT").is_some() && dict.get(b"AA").is_some() && ty != Some(b"Annot") {
issue("6.5.2", "A form field has additional actions", None, None, true);
}
if ty == Some(b"Font") || dict.get(b"BaseFont").is_some() && subtype.is_some() {
// A Type 0 font's descendant is checked through it.
if draws && (ty == Some(b"Font") || dict.get(b"BaseFont").is_some() && subtype.is_some()) {
check_font(doc, dict, issue);
}
if let Object::Stream(stream) = object {
+32
View File
@@ -1232,3 +1232,35 @@ fn xmp_value_forms_are_those_xmp_allows() {
["xmpMM:DerivedFrom"]
);
}
/// PDF/A asks for the fonts "used for rendering" to be embedded (ISO 19005-2, 6.2.11.4.1). A font
/// that only the form's default resources hold (`/AcroForm /DR`, ISO 32000-1 12.7.2) draws
/// nothing, as in veraPDF's 6-4-1-t02-pass-a; one that a widget's appearance stream draws with is
/// named in that stream's `/Resources`, and still has to be embedded.
#[test]
fn only_fonts_something_draws_with_must_be_embedded() {
let fonts =
|doc: &Document| -> Vec<String> { verify(doc, Level::A2b).into_iter().filter(|i| i.clause == "6.2.11.4").map(|i| i.message).collect() };
let form = |appearance_fonts: &str| {
build(
&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 4 0 R /ZaDb 5 0 R >> >> >> >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
"<< /Type /Page /Parent 2 0 R /Resources << >> /Annots [6 0 R] >>",
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
"<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>",
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /F 4 /Rect [10 10 90 30] /P 3 0 R /AP << /N 7 0 R >> >>",
&format!("<< /Type /XObject /Subtype /Form /BBox [0 0 80 20] /Resources << {appearance_fonts} >> /Length 0 >>\nstream\n\nendstream"),
// An object that is nothing but a reference to the Helvetica font (ISO 32000-1 7.3.10).
"4 0 R",
],
"",
)
};
assert!(fonts(&form("")).is_empty(), "{:?}", fonts(&form("")));
assert_eq!(fonts(&form("/Font << /Helv 4 0 R >>")), ["The font Helvetica isn't embedded"]);
// Named through a reference to it, it is still the font something draws with.
assert_eq!(fonts(&form("/Font << /Helv 8 0 R >>")), ["The font Helvetica isn't embedded"]);
// A page's own font is checked as before.
assert_eq!(fonts(&messy()), ["The font Helvetica isn't embedded"]);
}