mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 02:33:51 +00:00
M11: 6.2.4.4 compares by content, resolves names, and has no All/None exemption
From the review of #6203: - Separation definitions are compared as objects, references followed at any depth (to 32), so identical streams under different numbers agree; - All and None are compared like any other colorant name; - colorant names and /Process components given by reference are resolved; - /Process exempts its components only in an NChannel space; - a null /Colorants entry, or one referring to no object, defines nothing; - colour spaces are found as deep as the parser nests them (128). Corpus results are unchanged (15 of 23 6.2.4.4 files right, no new false failure). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e001b9f4b0
commit
72bc2548fb
@@ -1,11 +1,12 @@
|
||||
//! PDF/A-2 6.2.4.4, Separation and DeviceN colour spaces:
|
||||
//!
|
||||
//! - Separation spaces of the same colorant name agree on their alternate space and tint
|
||||
//! transform, also where a DeviceN space's `/Colorants` dictionary holds them;
|
||||
//! transform, also where a DeviceN space's `/Colorants` dictionary holds them. They are compared
|
||||
//! as objects, whether stored directly or by reference, at any depth (ISO 32000-1 7.3.10);
|
||||
//! - a DeviceN space names each spot colorant it uses in the `/Colorants` dictionary of its
|
||||
//! attributes (ISO 32000-1 8.6.6.5, Table 71). The process colorants (`Cyan`, `Magenta`,
|
||||
//! `Yellow`, `Black`, and those an NChannel space's `/Process` lists) and `None` are not spot
|
||||
//! colorants.
|
||||
//! attributes (ISO 32000-1 8.6.6.5, Table 71); a null or missing entry defines nothing. The
|
||||
//! process colorants (`Cyan`, `Magenta`, `Yellow`, `Black`, and those an NChannel space's
|
||||
//! `/Process` lists) and `None` are not spot colorants.
|
||||
//!
|
||||
//! Colour spaces are found wherever they are written: in resource dictionaries, images, shadings
|
||||
//! and other colour spaces, direct or by reference.
|
||||
@@ -14,8 +15,11 @@ use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use pdfcraft_cos::{Document, ObjRef, Object};
|
||||
|
||||
/// How deep a direct object is followed (arrays and dictionaries inside one another).
|
||||
const MAX_DEPTH: u8 = 32;
|
||||
/// How deep a direct object is followed (arrays and dictionaries inside one another): as deep as
|
||||
/// the parser nests them.
|
||||
const MAX_DEPTH: u8 = 128;
|
||||
/// How deep two definitions are compared, references followed.
|
||||
const MAX_COMPARE_DEPTH: u8 = 32;
|
||||
/// The colorants of DeviceCMYK, which a DeviceN space may name without defining them.
|
||||
const PROCESS: [&[u8]; 4] = [b"Cyan", b"Magenta", b"Yellow", b"Black"];
|
||||
|
||||
@@ -75,19 +79,18 @@ impl Scan<'_> {
|
||||
}
|
||||
|
||||
/// `[/Separation name alternateSpace tintTransform]`: the same name must keep the same
|
||||
/// alternate space and tint transform. `All` and `None` name no colorant of their own.
|
||||
/// alternate space and tint transform.
|
||||
fn separation(&mut self, a: &[Object]) {
|
||||
let [_, name, alternate, tint, ..] = a else { return };
|
||||
let name = self.doc.resolve(name);
|
||||
let Some(name) = name.as_name() else { return };
|
||||
if name == b"All" || name == b"None" {
|
||||
return;
|
||||
}
|
||||
let this = ((*self.doc.resolve(alternate)).clone(), (*self.doc.resolve(tint)).clone());
|
||||
let this = (alternate.clone(), tint.clone());
|
||||
match self.separations.get(name) {
|
||||
Some(first) if *first != this => {
|
||||
self.findings.differing.insert(String::from_utf8_lossy(name).into_owned());
|
||||
Some((alt, fun)) => {
|
||||
if !same(self.doc, alt, &this.0, 0) || !same(self.doc, fun, &this.1, 0) {
|
||||
self.findings.differing.insert(String::from_utf8_lossy(name).into_owned());
|
||||
}
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
self.separations.insert(name.to_vec(), this);
|
||||
}
|
||||
@@ -103,20 +106,52 @@ impl Scan<'_> {
|
||||
let attributes = attributes.as_deref().and_then(Object::as_dict);
|
||||
let colorants = attributes.and_then(|d| d.get(b"Colorants")).map(|c| self.doc.resolve(c));
|
||||
let colorants = colorants.as_deref().and_then(Object::as_dict);
|
||||
let nchannel = attributes.and_then(|d| d.get(b"Subtype")).map(|t| self.doc.resolve(t)).is_some_and(|t| t.as_name() == Some(b"NChannel"));
|
||||
let process = attributes
|
||||
.filter(|_| nchannel)
|
||||
.and_then(|d| d.get(b"Process"))
|
||||
.map(|p| self.doc.resolve(p))
|
||||
.and_then(|p| p.as_dict().and_then(|p| p.get(b"Components")).map(|c| self.doc.resolve(c)));
|
||||
let process: Vec<Vec<u8>> = process
|
||||
.as_deref()
|
||||
.and_then(Object::as_array)
|
||||
.map(|c| c.iter().filter_map(|n| n.as_name().map(<[u8]>::to_vec)).collect())
|
||||
.map(|c| c.iter().filter_map(|n| self.doc.resolve(n).as_name().map(<[u8]>::to_vec)).collect())
|
||||
.unwrap_or_default();
|
||||
for n in names.iter().filter_map(Object::as_name) {
|
||||
for n in names.iter().map(|n| self.doc.resolve(n)) {
|
||||
let Some(n) = n.as_name() else { continue };
|
||||
let spot = n != b"None" && !PROCESS.contains(&n) && !process.iter().any(|p| p == n);
|
||||
if spot && !colorants.is_some_and(|c| c.get(n).is_some()) {
|
||||
// A null entry, or one referring to no object, defines nothing (ISO 32000-1 7.3.9).
|
||||
let defined = colorants.and_then(|c| c.get(n)).is_some_and(|e| !matches!(*self.doc.resolve(e), Object::Null));
|
||||
if spot && !defined {
|
||||
self.findings.undefined.insert(String::from_utf8_lossy(n).into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `a` and `b` are the same object, wherever their parts are stored: references are
|
||||
/// followed (to [`MAX_COMPARE_DEPTH`]), so two identical streams under different object numbers
|
||||
/// are the same.
|
||||
fn same(doc: &Document, a: &Object, b: &Object, depth: u8) -> bool {
|
||||
if let (Object::Ref(x), Object::Ref(y)) = (a, b)
|
||||
&& x == y
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if depth > MAX_COMPARE_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let (a, b) = (doc.resolve(a), doc.resolve(b));
|
||||
let deeper = depth.saturating_add(1);
|
||||
match (&*a, &*b) {
|
||||
(Object::Array(x), Object::Array(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| same(doc, p, q, deeper)),
|
||||
(Object::Dict(x), Object::Dict(y)) => same_dict(doc, x, y, deeper),
|
||||
(Object::Stream(x), Object::Stream(y)) => x.raw == y.raw && same_dict(doc, &x.dict, &y.dict, deeper),
|
||||
(x, y) => x == y,
|
||||
}
|
||||
}
|
||||
|
||||
fn same_dict(doc: &Document, a: &pdfcraft_cos::Dict, b: &pdfcraft_cos::Dict, depth: u8) -> bool {
|
||||
// `/Length` may be written directly in one and by reference in the other: `same` resolves it.
|
||||
a.len() == b.len() && a.iter().all(|(k, v)| b.get(k).is_some_and(|w| same(doc, v, w, depth)))
|
||||
}
|
||||
|
||||
@@ -542,3 +542,40 @@ fn separation_and_devicen_spaces_are_checked() {
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
/// From the review of the 6.2.4.4 checks: nested parts compare by content, `All` and `None` get
|
||||
/// no exemption, names may be given by reference, `/Process` exempts only in an NChannel space, a
|
||||
/// null `/Colorants` entry defines nothing, and a colour space nested deep is still found.
|
||||
#[test]
|
||||
fn separation_and_devicen_checks_follow_references_depth_and_nchannel() {
|
||||
let spots = |spaces: &str| -> Vec<String> {
|
||||
let page = format!("<< /Type /Page /Parent 2 0 R /Resources << /ColorSpace << {spaces} >> >> >>");
|
||||
let icc = "<< /N 3 /Length 4 >>\nstream\nabcd\nendstream";
|
||||
let doc = build(
|
||||
&[
|
||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
|
||||
&page,
|
||||
"<< /FunctionType 2 /Domain [0 1] /C0 [1 1 1] /C1 [1 0 0] /N 1 >>",
|
||||
"<< /FunctionType 2 /Domain [0 1] /C0 [1 1 1] /C1 [0 1 0] /N 1 >>",
|
||||
icc,
|
||||
icc,
|
||||
"/Red",
|
||||
"null",
|
||||
],
|
||||
"",
|
||||
);
|
||||
verify(&doc, Level::A2b).into_iter().filter(|i| i.clause == "6.2.4.4").map(|i| i.message).collect()
|
||||
};
|
||||
let differ = |n: &str| vec![format!("Separation colour spaces named {n} differ in their alternate space or tint transform")];
|
||||
let undefined = |n: &str| vec![format!("The spot colour {n} of a DeviceN colour space isn't in its Colorants dictionary")];
|
||||
// Identical ICC streams under different numbers are the same alternate space.
|
||||
assert!(spots("/A [/Separation /Red [/ICCBased 6 0 R] 4 0 R] /B [/Separation /Red [/ICCBased 7 0 R] 4 0 R]").is_empty());
|
||||
assert_eq!(spots("/A [/Separation /All /DeviceRGB 4 0 R] /B [/Separation /All /DeviceRGB 5 0 R]"), differ("All"));
|
||||
assert_eq!(spots("/A [/Separation 8 0 R /DeviceRGB 4 0 R] /B [/Separation /Red /DeviceRGB 5 0 R]"), differ("Red"));
|
||||
assert_eq!(spots("/A [/DeviceN [/Spot] /DeviceRGB 4 0 R << /Process << /Components [/Spot] >> >>]"), undefined("Spot"));
|
||||
assert_eq!(spots("/A [/DeviceN [/Spot] /DeviceRGB 4 0 R << /Colorants << /Spot null >> >>]"), undefined("Spot"));
|
||||
assert_eq!(spots("/A [/DeviceN [/Spot] /DeviceRGB 4 0 R << /Colorants << /Spot 9 0 R >> >>]"), undefined("Spot"));
|
||||
let deep = format!("/A {}[/DeviceN [/Spot] /DeviceRGB 4 0 R]{}", "[".repeat(40), "]".repeat(40));
|
||||
assert_eq!(spots(&deep), undefined("Spot"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user