mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
M11: PDF/A verify reads graphics states from what the trailer reaches, and needs an integer HalftoneType (6.2.5)
Objects are followed from the trailer through a queue of references, never into /PieceInfo, so an object only private data refers to is not read as a graphics state holder. /HalftoneType must be an integer (ISO 32000-1 Table 128); a real of the same value is reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
1 parent
7e16d79284
commit
c6b5e61270
3 files changed
+35
-14
No files matched your search
@@ -13,10 +13,11 @@
|
||||
//! Graphics state dictionaries are found as the `/ExtGState` entries of resource dictionaries
|
||||
//! (`/Resources` of pages, forms, Type 3 fonts, patterns and annotation appearances, and an
|
||||
//! interactive form's `/DR`), and as the `/ExtGState` of a shading pattern (ISO 32000-1 8.7.4.3,
|
||||
//! Table 76). Private application data (`/PieceInfo`) isn't read. A map shared by reference is
|
||||
//! read once.
|
||||
//! Table 76). They are looked for in what the document reaches from its trailer, following
|
||||
//! references, but not into private application data (`/PieceInfo`), so an object only that data
|
||||
//! refers to isn't read. A map shared by reference is read once.
|
||||
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::collections::{BTreeSet, HashSet, VecDeque};
|
||||
|
||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object};
|
||||
|
||||
@@ -42,7 +43,7 @@ pub(crate) struct Findings {
|
||||
pub(crate) phase: bool,
|
||||
/// Halftone types other than 1 and 5, in order.
|
||||
pub(crate) halftone_types: BTreeSet<i64>,
|
||||
/// A halftone has no `/HalftoneType` integer.
|
||||
/// A halftone's `/HalftoneType` is missing or isn't an integer.
|
||||
pub(crate) untyped: bool,
|
||||
/// A halftone has a `/HalftoneName`.
|
||||
pub(crate) halftone_name: bool,
|
||||
@@ -56,11 +57,15 @@ pub(crate) struct Findings {
|
||||
pub(crate) intent_not_name: bool,
|
||||
}
|
||||
|
||||
/// The 6.2.5 findings over the indirect objects `objs`.
|
||||
pub(crate) fn check(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)]) -> Findings {
|
||||
let mut scan = Scan { doc, read: HashSet::new(), findings: Findings::default() };
|
||||
for (_, o) in objs {
|
||||
scan.walk(o, 0);
|
||||
/// The 6.2.5 findings over what `doc` reaches from its trailer.
|
||||
pub(crate) fn check(doc: &Document) -> Findings {
|
||||
let mut scan = Scan { doc, read: HashSet::new(), reached: HashSet::new(), queue: VecDeque::new(), findings: Findings::default() };
|
||||
scan.walk(&Object::Dict(doc.trailer().clone()), 0);
|
||||
// References are followed from a queue, not by recursion: a chain of them can be as long as
|
||||
// the file.
|
||||
while let Some(r) = scan.queue.pop_front() {
|
||||
let o = doc.get(r);
|
||||
scan.walk(&o, 0);
|
||||
}
|
||||
scan.findings
|
||||
}
|
||||
@@ -69,6 +74,9 @@ struct Scan<'a> {
|
||||
doc: &'a Document,
|
||||
/// Resource dictionaries, `/ExtGState` maps and graphics states read so far, by reference.
|
||||
read: HashSet<ObjRef>,
|
||||
/// Objects reached so far, queued or walked.
|
||||
reached: HashSet<ObjRef>,
|
||||
queue: VecDeque<ObjRef>,
|
||||
findings: Findings,
|
||||
}
|
||||
|
||||
@@ -78,6 +86,12 @@ impl Scan<'_> {
|
||||
return;
|
||||
}
|
||||
let d = match o {
|
||||
Object::Ref(r) => {
|
||||
if self.reached.insert(*r) {
|
||||
self.queue.push_back(*r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Object::Array(a) => {
|
||||
for v in a {
|
||||
self.walk(v, depth.saturating_add(1));
|
||||
@@ -171,7 +185,8 @@ impl Scan<'_> {
|
||||
/// A halftone dictionary or stream: the graphics state's own (`colorant` is `None`), or the
|
||||
/// one a type 5 halftone holds for `colorant`.
|
||||
fn halftone(&mut self, ht: &Dict, colorant: Option<&[u8]>) {
|
||||
let kind = self.present(ht, b"HalftoneType").and_then(|t| t.as_int());
|
||||
// An integer (ISO 32000-1 Table 128), not a real of the same value.
|
||||
let kind = self.present(ht, b"HalftoneType").and_then(|t| if let Object::Int(t) = *t { Some(t) } else { None });
|
||||
match kind {
|
||||
None => self.findings.untyped = true,
|
||||
Some(1 | 5) => {}
|
||||
|
||||
@@ -126,7 +126,7 @@ pub const WORDINGS: &[&str] = &[
|
||||
"A graphics state has a transfer function (TR2) other than Default",
|
||||
"A graphics state has a halftone phase (HTP)",
|
||||
"A halftone has type {}: only types 1 and 5 are allowed",
|
||||
"A halftone has no HalftoneType",
|
||||
"A halftone's HalftoneType is missing or isn't an integer",
|
||||
"A halftone has a HalftoneName",
|
||||
"A halftone has a transfer function, which only a non-primary colorant's halftone may have",
|
||||
"The halftone for the colorant {} has no transfer function",
|
||||
@@ -527,7 +527,7 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
}
|
||||
check_objects(doc, level, &objs, &page_of, &mut issue);
|
||||
// 6.2.5: the extended graphics state.
|
||||
let gs = gstate::check(doc, &objs);
|
||||
let gs = gstate::check(doc);
|
||||
if gs.transfer {
|
||||
issue("6.2.5", "A graphics state has a transfer function (TR)", None, None, false);
|
||||
}
|
||||
@@ -541,7 +541,7 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
issue("6.2.5", "A halftone has type {}: only types 1 and 5 are allowed", Some(kind.to_string()), None, false);
|
||||
}
|
||||
if gs.untyped {
|
||||
issue("6.2.5", "A halftone has no HalftoneType", None, None, false);
|
||||
issue("6.2.5", "A halftone's HalftoneType is missing or isn't an integer", None, None, false);
|
||||
}
|
||||
if gs.halftone_name {
|
||||
issue("6.2.5", "A halftone has a HalftoneName", None, None, false);
|
||||
|
||||
@@ -504,6 +504,7 @@ fn extended_graphics_states_are_checked() {
|
||||
"<< /HalftoneType 1 /Frequency 60 /Angle 45 /SpotFunction /Round >>",
|
||||
"<< /TR 4 0 R >>",
|
||||
"<< /Type /Halftone /HalftoneType 6 /Width 2 /Height 2 /Length 4 >>\nstream\nabcd\nendstream",
|
||||
"<< /Resources << /ExtGState << /X << /TR 4 0 R >> >> >> >>",
|
||||
],
|
||||
"",
|
||||
);
|
||||
@@ -536,7 +537,9 @@ fn extended_graphics_states_are_checked() {
|
||||
assert_eq!(gs("/A << /RI /Custom >>"), ["A graphics state has the rendering intent Custom, which isn't a standard one"]);
|
||||
assert_eq!(gs("/A << /RI (Perceptual) >>"), ["A graphics state has a rendering intent that isn't a name"]);
|
||||
// A halftone's type is required; a null name is no name.
|
||||
assert_eq!(gs("/A << /HT << /Frequency 60 >> >>"), ["A halftone has no HalftoneType"]);
|
||||
let untyped = ["A halftone's HalftoneType is missing or isn't an integer"];
|
||||
assert_eq!(gs("/A << /HT << /Frequency 60 >> >>"), untyped);
|
||||
assert_eq!(gs("/A << /HT << /HalftoneType 1.0 >> >>"), untyped);
|
||||
assert!(gs("/A << /HT << /HalftoneType 1 /HalftoneName null >> >>").is_empty());
|
||||
// Transfer functions in halftones: only, and always, for a non-primary colorant.
|
||||
let in_halftone = "A halftone has a transfer function, which only a non-primary colorant's halftone may have";
|
||||
@@ -551,6 +554,9 @@ fn extended_graphics_states_are_checked() {
|
||||
assert!(
|
||||
gs("/A << /RI /Perceptual >> >> /PieceInfo << /App << /Private << /Resources << /ExtGState << /X << /TR 4 0 R >> >> >> >> >>").is_empty()
|
||||
);
|
||||
// Nor is an object (8) only private data refers to; one the page refers to is read.
|
||||
assert!(gs("/A << /RI /Perceptual >> >> /PieceInfo << /App << /Private 8 0 R >> >> /Z <<").is_empty());
|
||||
assert_eq!(gs("/A << /RI /Perceptual >> >> /Other 8 0 R /Z <<"), [tr]);
|
||||
// A shading pattern holds one graphics state, not a dictionary of them.
|
||||
let pattern = |gs: &str| format!("/A << /RI /Perceptual >> >> /Pattern << /P << /PatternType 2 /Shading << /ShadingType 2 >> /ExtGState {gs} >>");
|
||||
assert_eq!(gs(&pattern("<< /TR 4 0 R >>")), [tr]);
|
||||
|
||||
Reference in new issue
Block a user