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:
mtlcl410andClaude Opus 5.5 committed 2026-11-05 10:54:12 +08:00
1 parent 19da2752cf
commit c35d8441d9
3 files changed
+99 -11

No files matched your search

+10 -8
View File
@@ -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);
}
}
}
+21 -1
View File
@@ -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());
}
+68 -2
View File
@@ -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 {