mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
M11: PDF/A verify reports blend modes ISO 32000-1 doesn't define (6.2.10)
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
19da2752cf
commit
c35d8441d9
3 files changed
+99
-11
No files matched your search
@@ -117,7 +117,8 @@ pub const WORDINGS: &[&str] = &[
|
||||
// The document as a whole.
|
||||
"The document is encrypted",
|
||||
"Device colour is used, but there is no PDF/A output intent",
|
||||
"Page {} uses transparency, but there is no PDF/A output intent and its group names no blending colour space",
|
||||
"Page {} has transparency but no blending colour space or PDF/A output intent",
|
||||
"A graphics state uses the blend mode {}, which ISO 32000-1 doesn't define",
|
||||
"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",
|
||||
@@ -516,18 +517,19 @@ 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);
|
||||
}
|
||||
}
|
||||
// 6.2.10: blend modes are those ISO 32000-1 defines.
|
||||
for (_, o) in &objs {
|
||||
for name in transparency::unknown_blend_modes(doc, o) {
|
||||
issue("6.2.10", "A graphics state uses the blend mode {}, which ISO 32000-1 doesn't define", Some(name), None, false);
|
||||
}
|
||||
}
|
||||
// 6.2.10: without a PDF/A output intent, a page with transparency names its blending colour
|
||||
// space.
|
||||
if !has_pdfa_intent(doc) {
|
||||
for (i, p) in pages.iter().enumerate() {
|
||||
if transparency::page_has_transparency(doc, &p.dict) && !transparency::page_names_blending_space(doc, &p.dict) {
|
||||
issue(
|
||||
"6.2.10",
|
||||
"Page {} uses transparency, but there is no PDF/A output intent and its group names no blending colour space",
|
||||
Some((i + 1).to_string()),
|
||||
Some(i),
|
||||
false,
|
||||
);
|
||||
let n = Some((i + 1).to_string());
|
||||
issue("6.2.10", "Page {} has transparency but no blending colour space or PDF/A output intent", n, Some(i), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ endstream",
|
||||
}
|
||||
verify(&build(&objs, ""), Level::A2b).into_iter().filter(|i| i.clause == "6.2.10").map(|i| i.message).collect()
|
||||
};
|
||||
let said = ["Page 1 uses transparency, but there is no PDF/A output intent and its group names no blending colour space"];
|
||||
let said = ["Page 1 has transparency but no blending colour space or PDF/A output intent"];
|
||||
let form = |dict: &str| format!("<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] {dict} /Length 0 >>\nstream\n\nendstream");
|
||||
let gs = |entries: &str| format!("/ExtGState << /G << {entries} >> >>");
|
||||
for (what, resources, extra) in [
|
||||
@@ -588,3 +588,23 @@ endstream",
|
||||
assert!(blending(false, "", &resources, &extra).is_empty(), "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 6.2.10: a graphics state's blend mode, or each name of a blend mode array, is one ISO 32000-1
|
||||
/// 11.3.5 defines, whether the graphics state is an object of its own or written in a page's
|
||||
/// resources.
|
||||
#[test]
|
||||
fn blend_modes_are_the_ones_iso_32000_1_defines() {
|
||||
let modes = |resources: &str, extra: &[&str]| -> Vec<String> {
|
||||
let page = format!("<< /Type /Page /Parent 2 0 R /Group << /S /Transparency /CS /DeviceRGB >> /Resources << {resources} >> >>");
|
||||
let mut objs: Vec<&str> =
|
||||
vec!["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>", &page];
|
||||
objs.extend_from_slice(extra);
|
||||
verify(&build(&objs, ""), Level::A2b).into_iter().filter(|i| i.clause == "6.2.10").map(|i| i.message).collect()
|
||||
};
|
||||
let custom = ["A graphics state uses the blend mode Custom, which ISO 32000-1 doesn't define"];
|
||||
assert_eq!(modes("/ExtGState << /G 4 0 R >>", &["<< /BM /Custom >>"]), custom, "an object of its own");
|
||||
assert_eq!(modes("/ExtGState << /G << /BM /Custom >> >>", &[]), custom, "written in the resources");
|
||||
assert_eq!(modes("/ExtGState << /G << /BM [/Multiply /Custom] >> >>", &[]), custom, "in an array");
|
||||
let all = "/Normal /Compatible /Multiply /Screen /Overlay /Darken /Lighten /ColorDodge /ColorBurn /HardLight /SoftLight /Difference /Exclusion /Hue /Saturation /Color /Luminosity";
|
||||
assert!(modes(&format!("/ExtGState << /G << /BM [{all}] >> >>"), &[]).is_empty());
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! PDF/A-2 6.2.10: a page that contains transparency, in a file without a PDF/A output intent,
|
||||
//! names its blending colour space: its `/Group` dictionary has a `/CS` entry.
|
||||
//! PDF/A-2 6.2.10: a graphics state's blend mode is one ISO 32000-1 defines, and a page that
|
||||
//! contains transparency, in a file without a PDF/A output intent, names its blending colour
|
||||
//! space: its `/Group` dictionary has a `/CS` entry.
|
||||
//!
|
||||
//! A page contains transparency when something its resources reach can paint transparently (ISO
|
||||
//! 32000-1 11.6.4.4 and 11.7.4): a graphics state with a constant alpha (`/CA`, `/ca`) below 1,
|
||||
@@ -147,6 +148,71 @@ impl Walk<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The blend modes of ISO 32000-1 11.3.5 (Tables 136 and 137), and `Compatible` (Table 58).
|
||||
const BLEND_MODES: &[&[u8]] = &[
|
||||
b"Normal",
|
||||
b"Compatible",
|
||||
b"Multiply",
|
||||
b"Screen",
|
||||
b"Overlay",
|
||||
b"Darken",
|
||||
b"Lighten",
|
||||
b"ColorDodge",
|
||||
b"ColorBurn",
|
||||
b"HardLight",
|
||||
b"SoftLight",
|
||||
b"Difference",
|
||||
b"Exclusion",
|
||||
b"Hue",
|
||||
b"Saturation",
|
||||
b"Color",
|
||||
b"Luminosity",
|
||||
];
|
||||
|
||||
/// The blend modes ISO 32000-1 doesn't define that the object `o` names in a `/BM` entry: its own,
|
||||
/// or one of a dictionary written inside it (a page's resources hold their graphics states
|
||||
/// directly), within [`MAX_DEPTH`].
|
||||
pub(crate) fn unknown_blend_modes(doc: &Document, o: &Object) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
blend_modes_within(doc, o, 0, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn blend_modes_within(doc: &Document, o: &Object, depth: usize, out: &mut Vec<String>) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
let d = match o {
|
||||
Object::Dict(d) => d,
|
||||
Object::Stream(s) => &s.dict,
|
||||
Object::Array(a) => {
|
||||
for v in a {
|
||||
blend_modes_within(doc, v, depth + 1, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
out.extend(unknown_in(doc, d));
|
||||
for (k, v) in d.iter() {
|
||||
if k != b"BM" {
|
||||
blend_modes_within(doc, v, depth + 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The names in a graphics state's `/BM` (a name, or an array of them) that are not blend modes
|
||||
/// ISO 32000-1 defines.
|
||||
fn unknown_in(doc: &Document, gs: &Dict) -> Vec<String> {
|
||||
let Some(bm) = gs.get(b"BM").map(|b| doc.resolve(b)) else { return Vec::new() };
|
||||
let names: Vec<Vec<u8>> = match &*bm {
|
||||
Object::Name(n) => vec![n.clone()],
|
||||
Object::Array(a) => a.iter().filter_map(|m| doc.resolve(m).as_name().map(<[u8]>::to_vec)).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
names.into_iter().filter(|n| !BLEND_MODES.contains(&n.as_slice())).map(|n| String::from_utf8_lossy(&n).into_owned()).collect()
|
||||
}
|
||||
|
||||
/// Whether a `/BM` value blends: a name other than `Normal` or `Compatible`, or an array whose
|
||||
/// first name is not one of those.
|
||||
fn blends(doc: &Document, b: &Object) -> bool {
|
||||
|
||||
Reference in new issue
Block a user