From abb409bac63e8f6820b4ccda3ed46374c2002b8a Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Thu, 5 Nov 2026 01:25:45 +0800 Subject: [PATCH] M11: a font named through a chain of references is the font PDF/A checks A /Font entry can point at an object that is nothing but a reference to another (ISO 32000-1 7.3.10). The fonts something draws with were recorded by the entry's own reference, so the font dictionary at the end of such a chain went unchecked. Follow the chain, as resolve does, up to 32 references. (CodeRabbit review on #6172.) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn --- crates/preflight/src/lib.rs | 15 ++++++++++++++- crates/preflight/src/tests.rs | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/preflight/src/lib.rs b/crates/preflight/src/lib.rs index 614240c023..799942fb85 100644 --- a/crates/preflight/src/lib.rs +++ b/crates/preflight/src/lib.rs @@ -341,12 +341,25 @@ fn drawing_fonts(doc: &Document, objs: &[(ObjRef, std::sync::Arc)]) -> s 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())); + 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. diff --git a/crates/preflight/src/tests.rs b/crates/preflight/src/tests.rs index 6747fdd62a..53f29e677e 100644 --- a/crates/preflight/src/tests.rs +++ b/crates/preflight/src/tests.rs @@ -507,12 +507,16 @@ fn only_fonts_something_draws_with_must_be_embedded() { "<< /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"]); }