mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
Merge #6203: M11: PDF/A verify reports Separation spaces of one name that differ and DeviceN spot colours missing from /Colorants (optimize.pdfa-validate)
# Conflicts: # crates/preflight/src/lib.rs # crates/preflight/src/tests.rs # parity/acrobat-features.toml
This commit is contained in:
commit
fd204ced66
4 files changed
+360
-2
No files matched your search
@@ -23,6 +23,7 @@
|
||||
|
||||
pub mod icc;
|
||||
mod limits;
|
||||
mod spot;
|
||||
mod xmp;
|
||||
mod xmp_extension;
|
||||
mod xmp_types;
|
||||
@@ -175,6 +176,8 @@ pub const WORDINGS: &[&str] = &[
|
||||
"DeviceCMYK is used: it needs a CMYK output intent (not added automatically)",
|
||||
"DeviceCMYK is used, but the PDF/A output intent isn't a CMYK profile",
|
||||
"DeviceRGB is used, but the PDF/A output intent isn't an RGB profile",
|
||||
"Separation colour spaces named {} differ in their alternate space or tint transform",
|
||||
"The spot colour {} of a DeviceN colour space isn't in its Colorants dictionary",
|
||||
"The document has additional actions (/AA)",
|
||||
"The document has document-level JavaScript",
|
||||
"The form asks viewers to make appearances (NeedAppearances)",
|
||||
@@ -743,6 +746,14 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
issue("6.1.13", "Page {} has a boundary smaller than 3 or larger than 14400 units", Some((i + 1).to_string()), Some(i), false);
|
||||
}
|
||||
}
|
||||
// 6.2.4.4: Separation and DeviceN colour spaces.
|
||||
let spots = spot::check(doc, &objs);
|
||||
for name in spots.differing {
|
||||
issue("6.2.4.4", "Separation colour spaces named {} differ in their alternate space or tint transform", Some(name), None, false);
|
||||
}
|
||||
for name in spots.undefined {
|
||||
issue("6.2.4.4", "The spot colour {} of a DeviceN colour space isn't in its Colorants dictionary", Some(name), None, false);
|
||||
}
|
||||
// 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();
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
//! 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. 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); 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.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
|
||||
use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object, Stream};
|
||||
|
||||
/// 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;
|
||||
/// How many object pairs the comparisons of one document visit in all. Past it, a comparison
|
||||
/// claims no difference.
|
||||
const MAX_COMPARE_STEPS: u32 = 1 << 20;
|
||||
/// The decoded bytes the stream comparisons of one document may spend in all.
|
||||
const MAX_COMPARE_BYTES: usize = 64 << 20;
|
||||
/// The stream entries that say how the data is stored, not what it is.
|
||||
const ENCODING: [&[u8]; 3] = [b"Length", b"Filter", b"DecodeParms"];
|
||||
/// 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"];
|
||||
|
||||
/// What the colour spaces of a document break of 6.2.4.4.
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct Findings {
|
||||
/// Colorant names whose Separation spaces differ, in name order.
|
||||
pub(crate) differing: BTreeSet<String>,
|
||||
/// Spot colorants a DeviceN space uses without a `/Colorants` entry, in name order.
|
||||
pub(crate) undefined: BTreeSet<String>,
|
||||
}
|
||||
|
||||
/// The 6.2.4.4 findings over the indirect objects `objs`.
|
||||
pub(crate) fn check(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)]) -> Findings {
|
||||
let compare = Compare { doc, equal: HashSet::new(), steps: MAX_COMPARE_STEPS, budget: DecodeBudget::new(MAX_COMPARE_BYTES, 0) };
|
||||
let mut scan = Scan { doc, compare, separations: HashMap::new(), findings: Findings::default() };
|
||||
for (_, o) in objs {
|
||||
scan.walk(o, 0);
|
||||
}
|
||||
scan.findings
|
||||
}
|
||||
|
||||
struct Scan<'a> {
|
||||
doc: &'a Document,
|
||||
compare: Compare<'a>,
|
||||
/// Each colorant name's first Separation space: its alternate space and tint transform.
|
||||
separations: HashMap<Vec<u8>, (Object, Object)>,
|
||||
findings: Findings,
|
||||
}
|
||||
|
||||
impl Scan<'_> {
|
||||
fn walk(&mut self, o: &Object, depth: u8) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
match o {
|
||||
Object::Array(a) => {
|
||||
match a.first().and_then(Object::as_name) {
|
||||
Some(b"Separation") => self.separation(a),
|
||||
Some(b"DeviceN") => self.devicen(a),
|
||||
_ => {}
|
||||
}
|
||||
for v in a {
|
||||
self.walk(v, depth.saturating_add(1));
|
||||
}
|
||||
}
|
||||
Object::Dict(d) => {
|
||||
for (_, v) in d.iter() {
|
||||
self.walk(v, depth.saturating_add(1));
|
||||
}
|
||||
}
|
||||
Object::Stream(s) => {
|
||||
for (_, v) in s.dict.iter() {
|
||||
self.walk(v, depth.saturating_add(1));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[/Separation name alternateSpace tintTransform]`: the same name must keep the same
|
||||
/// 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 };
|
||||
let this = (alternate.clone(), tint.clone());
|
||||
match self.separations.get(name) {
|
||||
Some((alt, fun)) => {
|
||||
if !self.compare.same(alt, &this.0, 0) || !self.compare.same(fun, &this.1, 0) {
|
||||
self.findings.differing.insert(String::from_utf8_lossy(name).into_owned());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.separations.insert(name.to_vec(), this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[/DeviceN names alternateSpace tintTransform attributes]`: each spot colorant in `names`
|
||||
/// needs an entry in the attributes' `/Colorants`.
|
||||
fn devicen(&mut self, a: &[Object]) {
|
||||
let Some(names) = a.get(1).map(|n| self.doc.resolve(n)) else { return };
|
||||
let Some(names) = names.as_array() else { return };
|
||||
let attributes = a.get(4).map(|d| self.doc.resolve(d));
|
||||
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| self.doc.resolve(n).as_name().map(<[u8]>::to_vec)).collect())
|
||||
.unwrap_or_default();
|
||||
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);
|
||||
// Each entry is a Separation space for the colorant it is keyed by (ISO 32000-1 8.6.6.5,
|
||||
// Table 71); a null entry, or one referring to no object, defines nothing (7.3.9).
|
||||
let defined = colorants.and_then(|c| c.get(n)).is_some_and(|e| self.separation_named(e, n));
|
||||
if spot && !defined {
|
||||
self.findings.undefined.insert(String::from_utf8_lossy(n).into_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `o` is `[/Separation name ...]` for the colorant `name`.
|
||||
fn separation_named(&self, o: &Object, name: &[u8]) -> bool {
|
||||
let o = self.doc.resolve(o);
|
||||
let Some([kind, n, ..]) = o.as_array().map(Vec::as_slice) else { return false };
|
||||
kind.as_name() == Some(b"Separation") && self.doc.resolve(n).as_name() == Some(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares definitions, remembering which reference pairs it found equal, so a structure shared
|
||||
/// by many definitions is compared once, within [`MAX_COMPARE_STEPS`] and [`MAX_COMPARE_BYTES`].
|
||||
struct Compare<'a> {
|
||||
doc: &'a Document,
|
||||
/// Reference pairs compared equal. A depth cut-off only ever answers "different", so an
|
||||
/// equal answer holds at any depth.
|
||||
equal: HashSet<(ObjRef, ObjRef)>,
|
||||
steps: u32,
|
||||
budget: DecodeBudget,
|
||||
}
|
||||
|
||||
impl Compare<'_> {
|
||||
/// 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, and streams compare by their decoded data. Once the steps or the
|
||||
/// decode allowance run out, nothing more is called different.
|
||||
fn same(&mut self, a: &Object, b: &Object, depth: u8) -> bool {
|
||||
let pair = match (a, b) {
|
||||
(Object::Ref(x), Object::Ref(y)) if x == y || self.equal.contains(&(*x, *y)) => return true,
|
||||
(Object::Ref(x), Object::Ref(y)) => Some((*x, *y)),
|
||||
_ => None,
|
||||
};
|
||||
let Some(steps) = self.steps.checked_sub(1) else { return true };
|
||||
self.steps = steps;
|
||||
if depth > MAX_COMPARE_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let (a, b) = (self.doc.resolve(a), self.doc.resolve(b));
|
||||
let deeper = depth.saturating_add(1);
|
||||
let same = match (&*a, &*b) {
|
||||
(Object::Array(x), Object::Array(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| self.same(p, q, deeper)),
|
||||
(Object::Dict(x), Object::Dict(y)) => self.same_dict(x, y, &[], deeper),
|
||||
(Object::Stream(x), Object::Stream(y)) => self.same_data(x, y, deeper) && self.same_dict(&x.dict, &y.dict, &ENCODING, deeper),
|
||||
(x, y) => x == y,
|
||||
};
|
||||
if same && let Some(pair) = pair {
|
||||
self.equal.insert(pair);
|
||||
}
|
||||
same
|
||||
}
|
||||
|
||||
/// Whether two dictionaries hold the same entries, those keyed in `skip` aside.
|
||||
fn same_dict(&mut self, a: &Dict, b: &Dict, skip: &[&[u8]], depth: u8) -> bool {
|
||||
let kept = |d: &Dict| d.iter().filter(|(k, _)| !skip.contains(&k.as_slice())).count();
|
||||
kept(a) == kept(b) && a.iter().filter(|(k, _)| !skip.contains(&k.as_slice())).all(|(k, v)| b.get(k).is_some_and(|w| self.same(v, w, depth)))
|
||||
}
|
||||
|
||||
/// Whether two streams hold the same data, however each is encoded. Data that can't be
|
||||
/// decoded within the allowance is not called different.
|
||||
fn same_data(&mut self, a: &Stream, b: &Stream, depth: u8) -> bool {
|
||||
let stored_alike = |c: &mut Self, k: &[u8]| match (a.dict.get(k), b.dict.get(k)) {
|
||||
(None, None) => true,
|
||||
(Some(x), Some(y)) => c.same(x, y, depth),
|
||||
_ => false,
|
||||
};
|
||||
if a.raw == b.raw && stored_alike(self, b"Filter") && stored_alike(self, b"DecodeParms") {
|
||||
return true;
|
||||
}
|
||||
match (self.budget.decode(a), self.budget.decode(b)) {
|
||||
(Ok(x), Ok(y)) => x == y,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -622,6 +622,141 @@ fn implementation_limits_cover_reals_and_cids() {
|
||||
assert_eq!(limits(&doc("", &range("0000", "00ff", 0), &range("3f00", "3fff", 65536))), cid, "through /UseCMap");
|
||||
}
|
||||
|
||||
/// PDF/A-2 6.2.4.4: Separation spaces of one colorant name agree on their alternate space and tint
|
||||
/// transform, also inside a DeviceN space's `/Colorants`; and a DeviceN space names each spot
|
||||
/// colorant it uses in its `/Colorants` (ISO 32000-1 8.6.6.5, Table 71). Process colorants (the
|
||||
/// CMYK four, and an NChannel space's `/Process` components) and `None` need no entry.
|
||||
#[test]
|
||||
fn separation_and_devicen_spaces_are_checked() {
|
||||
let spots = |spaces: &str| -> Vec<String> {
|
||||
let page = format!("<< /Type /Page /Parent 2 0 R /Resources << /ColorSpace << {spaces} >> >> >>");
|
||||
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 >>",
|
||||
"<< /FunctionType 2 /Domain [0 1] /C0 [1 1 1] /C1 [1 0 0] /N 1 >>",
|
||||
],
|
||||
"",
|
||||
);
|
||||
verify(&doc, Level::A2b).into_iter().filter(|i| i.clause == "6.2.4.4").map(|i| i.message).collect()
|
||||
};
|
||||
let red = |alt: &str, f: u32| format!("[/Separation /Red /{alt} {f} 0 R]");
|
||||
// The same name, the same alternate and an identical tint transform (another object): fine.
|
||||
assert!(spots(&format!("/A {} /B {}", red("DeviceRGB", 4), red("DeviceRGB", 6))).is_empty());
|
||||
assert_eq!(
|
||||
spots(&format!("/A {} /B {}", red("DeviceRGB", 4), red("DeviceRGB", 5))),
|
||||
["Separation colour spaces named Red differ in their alternate space or tint transform"]
|
||||
);
|
||||
assert_eq!(
|
||||
spots(&format!("/A {} /B {}", red("DeviceRGB", 4), red("DeviceGray", 4))),
|
||||
["Separation colour spaces named Red differ in their alternate space or tint transform"]
|
||||
);
|
||||
// Inside two DeviceN spaces' Colorants.
|
||||
let nchannel = |f: u32| format!("[/DeviceN [/Red] /DeviceRGB 4 0 R << /Subtype /NChannel /Colorants << /Red {} >> >>]", red("DeviceRGB", f));
|
||||
assert_eq!(
|
||||
spots(&format!("/A {} /B {}", nchannel(4), nchannel(5))),
|
||||
["Separation colour spaces named Red differ in their alternate space or tint transform"]
|
||||
);
|
||||
// Spot colorants need a Colorants entry; process colorants and None don't.
|
||||
assert_eq!(
|
||||
spots("/A [/DeviceN [/Spot1 /Cyan /None] /DeviceCMYK 4 0 R]"),
|
||||
["The spot colour Spot1 of a DeviceN colour space isn't in its Colorants dictionary"]
|
||||
);
|
||||
assert_eq!(
|
||||
spots("/A [/DeviceN [/Spot1] /DeviceRGB 4 0 R << /Colorants << >> >>]"),
|
||||
["The spot colour Spot1 of a DeviceN colour space isn't in its Colorants dictionary"]
|
||||
);
|
||||
assert!(spots(&format!("/A [/DeviceN [/Red /Black] /DeviceRGB 4 0 R << /Colorants << /Red {} >> >>]", red("DeviceRGB", 4))).is_empty());
|
||||
assert!(
|
||||
spots("/A [/DeviceN [/PrCyan /Black] /DeviceCMYK 4 0 R << /Subtype /NChannel /Process << /ColorSpace /DeviceCMYK /Components [/PrCyan /M /Y /Black] >> >>]")
|
||||
.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"));
|
||||
}
|
||||
|
||||
/// From the second review of the 6.2.4.4 checks: a `/Colorants` entry is a Separation space of
|
||||
/// its own colorant, tint-transform streams compare by decoded data, and a structure shared by
|
||||
/// many definitions is compared once rather than once per path through it.
|
||||
#[test]
|
||||
fn separation_checks_match_colorant_entries_decode_streams_and_share_comparisons() {
|
||||
// Two copies of a chain of arrays, each level naming the next twice: comparing them path by
|
||||
// path takes about 2^19 steps.
|
||||
const LEVELS: usize = 18;
|
||||
let chain = |first: usize| -> Vec<String> {
|
||||
(0..LEVELS).map(|k| if k + 1 < LEVELS { format!("[{n} 0 R {n} 0 R]", n = first + k + 1) } else { "[1 2 3]".into() }).collect()
|
||||
};
|
||||
let (a, b) = (5, 5 + LEVELS);
|
||||
let spots = |spaces: &str| -> Vec<String> {
|
||||
let page = format!("<< /Type /Page /Parent 2 0 R /Resources << /ColorSpace << {spaces} >> >> >>");
|
||||
let mut objects = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>".into(),
|
||||
page,
|
||||
"<< /FunctionType 2 /Domain [0 1] /C0 [1 1 1] /C1 [1 0 0] /N 1 >>".into(),
|
||||
];
|
||||
objects.extend(chain(a));
|
||||
objects.extend(chain(b));
|
||||
objects.push("<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Length 15 >>\nstream\n{ dup dup pop }\nendstream".into());
|
||||
objects.push("<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Filter /ASCIIHexDecode /Length 31 >>\nstream\n7B206475702064757020706F70207D>\nendstream".into());
|
||||
objects.push("<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Length 15 >>\nstream\n{ dup pop dup }\nendstream".into());
|
||||
let objects: Vec<&str> = objects.iter().map(String::as_str).collect();
|
||||
let doc = build(&objects, "");
|
||||
verify(&doc, Level::A2b).into_iter().filter(|i| i.clause == "6.2.4.4").map(|i| i.message).collect()
|
||||
};
|
||||
let differ = vec!["Separation colour spaces named Red differ in their alternate space or tint transform".to_string()];
|
||||
let undefined = vec!["The spot colour Spot of a DeviceN colour space isn't in its Colorants dictionary".to_string()];
|
||||
// A Colorants entry that isn't a Separation space of the same colorant defines nothing.
|
||||
assert_eq!(spots("/A [/DeviceN [/Spot] /DeviceRGB 4 0 R << /Colorants << /Spot /DeviceRGB >> >>]"), undefined);
|
||||
assert_eq!(spots("/A [/DeviceN [/Spot] /DeviceRGB 4 0 R << /Colorants << /Spot [/Separation /Other /DeviceRGB 4 0 R] >> >>]"), undefined);
|
||||
assert!(spots("/A [/DeviceN [/Spot] /DeviceRGB 4 0 R << /Colorants << /Spot [/Separation /Spot /DeviceRGB 4 0 R] >> >>]").is_empty());
|
||||
// The same PostScript function, once plain and once hex-encoded, is the same tint transform.
|
||||
let (plain, hex, other) = (5 + 2 * LEVELS, 6 + 2 * LEVELS, 7 + 2 * LEVELS);
|
||||
assert!(spots(&format!("/A [/Separation /Red /DeviceRGB {plain} 0 R] /B [/Separation /Red /DeviceRGB {hex} 0 R]")).is_empty());
|
||||
assert_eq!(spots(&format!("/A [/Separation /Red /DeviceRGB {plain} 0 R] /B [/Separation /Red /DeviceRGB {other} 0 R]")), differ);
|
||||
// Equal copies compared again and again still leave room to find the one that differs.
|
||||
let sep = |f: usize| format!("[/Separation /Red /DeviceRGB {f} 0 R]");
|
||||
assert_eq!(spots(&format!("/A {} /B {} /C {} /D {} /E {}", sep(a), sep(b), sep(b), sep(b), sep(4))), differ);
|
||||
}
|
||||
|
||||
const XMP_NS: &str = concat!(
|
||||
r#"xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" "#,
|
||||
r#"xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" "#,
|
||||
|
||||
@@ -8017,8 +8017,8 @@ milestone = "M11"
|
||||
status = "partial"
|
||||
commands = ["standards.pdfa"]
|
||||
tools = ["pdfa_verify"]
|
||||
evidence = ["crates/preflight/src/tests.rs::verify_reports_what_breaks_pdfa", "crates/preflight/src/tests.rs::standards_identification_uses_namespace_not_prefix", "crates/preflight/src/tests.rs::convert_fixes_all_but_fonts", "crates/preflight/src/tests.rs::verify_reports_an_output_intent_that_does_not_match_device_colour", "crates/preflight/src/tests.rs::xmp_simple_values_have_their_lexical_forms", "crates/preflight/src/tests.rs::xmp_arrays_have_their_kind_and_item_types", "crates/preflight/src/tests.rs::xmp_structures_have_their_field_types", "crates/preflight/src/tests.rs::xmp_value_types_are_reported_by_verify", "crates/preflight/src/tests.rs::xmp_exif_schemas_have_their_value_types", "crates/preflight/src/tests.rs::xmp_exif_properties_are_known_by_namespace_uri", "crates/preflight/src/tests.rs::xmp_extension_schema_descriptions_are_complete", "crates/preflight/src/tests.rs::xmp_extension_findings_say_where_they_are", "crates/preflight/src/tests.rs::xmp_extension_naming_entries_are_not_empty", "crates/preflight/src/tests.rs::xmp_mistyped_properties_name_their_type", "crates/preflight/src/tests.rs::xmp_properties_are_known_by_namespace_uri_and_named_as_written", "crates/automation/tests/it/automation.rs::pdfa_through_tools", "crates/ui-egui/tests/it/compare.rs::pdfa_dialog_verifies_and_converts", "crates/preflight/src/tests.rs::implementation_limits_are_reported", "crates/preflight/src/tests.rs::implementation_limits_reach_the_trailer_split_content_deep_values_and_huge_boxes", "crates/preflight/src/tests.rs::implementation_limits_cover_reals_and_cids"]
|
||||
notes = "PDF/A-2b/3b structural rules: identification, the value types of the predefined XMP schemas (6.6.2.3.1: dc, xmp, xmpRights, xmpMM, xmpBJ, xmpTPg, xmpDM, pdf, photoshop, crs, and tiff, exif and aux from the September 2005 XMP Specification, with their structures), XMP extension schema descriptions (6.6.2.3.3, after PDF Association TechNote 0009 section 4: required entries of the schema, property, value type and field descriptions, none of the naming entries empty, and the five required prefixes; that a property's custom value type has its own pdfaSchema:valueType description is not yet checked), encryption, output intent (present, and RGB/CMYK to match DeviceRGB/DeviceCMYK use), fonts embedded, annotations, actions, images, embedded files, and the implementation limits of 6.1.13 (integers, reals, strings, names, CIDs and page boundaries, ISO 32000-1 Annex C). Not yet: content-stream operator checks, transparency details, PDF/A-1 and -4, a veraPDF comparison."
|
||||
evidence = ["crates/preflight/src/tests.rs::verify_reports_what_breaks_pdfa", "crates/preflight/src/tests.rs::standards_identification_uses_namespace_not_prefix", "crates/preflight/src/tests.rs::convert_fixes_all_but_fonts", "crates/preflight/src/tests.rs::verify_reports_an_output_intent_that_does_not_match_device_colour", "crates/preflight/src/tests.rs::xmp_simple_values_have_their_lexical_forms", "crates/preflight/src/tests.rs::xmp_arrays_have_their_kind_and_item_types", "crates/preflight/src/tests.rs::xmp_structures_have_their_field_types", "crates/preflight/src/tests.rs::xmp_value_types_are_reported_by_verify", "crates/preflight/src/tests.rs::xmp_exif_schemas_have_their_value_types", "crates/preflight/src/tests.rs::xmp_exif_properties_are_known_by_namespace_uri", "crates/preflight/src/tests.rs::xmp_extension_schema_descriptions_are_complete", "crates/preflight/src/tests.rs::xmp_extension_findings_say_where_they_are", "crates/preflight/src/tests.rs::xmp_extension_naming_entries_are_not_empty", "crates/preflight/src/tests.rs::xmp_mistyped_properties_name_their_type", "crates/preflight/src/tests.rs::xmp_properties_are_known_by_namespace_uri_and_named_as_written", "crates/automation/tests/it/automation.rs::pdfa_through_tools", "crates/ui-egui/tests/it/compare.rs::pdfa_dialog_verifies_and_converts", "crates/preflight/src/tests.rs::implementation_limits_are_reported", "crates/preflight/src/tests.rs::implementation_limits_reach_the_trailer_split_content_deep_values_and_huge_boxes", "crates/preflight/src/tests.rs::implementation_limits_cover_reals_and_cids", "crates/preflight/src/tests.rs::separation_and_devicen_spaces_are_checked", "crates/preflight/src/tests.rs::separation_and_devicen_checks_follow_references_depth_and_nchannel"]
|
||||
notes = "PDF/A-2b/3b structural rules: identification, the value types of the predefined XMP schemas (6.6.2.3.1: dc, xmp, xmpRights, xmpMM, xmpBJ, xmpTPg, xmpDM, pdf, photoshop, crs, and tiff, exif and aux from the September 2005 XMP Specification, with their structures), XMP extension schema descriptions (6.6.2.3.3, after PDF Association TechNote 0009 section 4: required entries of the schema, property, value type and field descriptions, none of the naming entries empty, and the five required prefixes; that a property's custom value type has its own pdfaSchema:valueType description is not yet checked), encryption, output intent (present, and RGB/CMYK to match DeviceRGB/DeviceCMYK use), fonts embedded, annotations, actions, images, embedded files, the implementation limits of 6.1.13 (integers, reals, strings, names, CIDs and page boundaries, ISO 32000-1 Annex C), and Separation and DeviceN spaces (6.2.4.4: one definition per colorant name, spot colorants in /Colorants; a device alternate without a matching output intent is not yet reported). Not yet: content-stream operator checks, transparency details, PDF/A-1 and -4, a veraPDF comparison."
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.pdfa-convert"
|
||||
|
||||
Reference in new issue
Block a user