mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
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:
commit
4f90d2295f
2 files changed
+72
-6
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
Reference in new issue
Block a user