diff --git a/crates/preflight/src/lib.rs b/crates/preflight/src/lib.rs index 7ab1a23b34..21a232abc8 100644 --- a/crates/preflight/src/lib.rs +++ b/crates/preflight/src/lib.rs @@ -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("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(); diff --git a/crates/preflight/src/spot.rs b/crates/preflight/src/spot.rs new file mode 100644 index 0000000000..8367d8d28a --- /dev/null +++ b/crates/preflight/src/spot.rs @@ -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, + /// Spot colorants a DeviceN space uses without a `/Colorants` entry, in name order. + pub(crate) undefined: BTreeSet, +} + +/// The 6.2.4.4 findings over the indirect objects `objs`. +pub(crate) fn check(doc: &Document, objs: &[(ObjRef, std::sync::Arc)]) -> 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, (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> = 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, + } + } +} diff --git a/crates/preflight/src/tests.rs b/crates/preflight/src/tests.rs index ffb94ca06b..8729f42aa7 100644 --- a/crates/preflight/src/tests.rs +++ b/crates/preflight/src/tests.rs @@ -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 { + 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 { + 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 { + (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 { + 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/" "#, diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 94e56ee1ef..e747bf0026 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -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"