diff --git a/crates/preflight/src/gstate.rs b/crates/preflight/src/gstate.rs new file mode 100644 index 0000000000..64c19de116 --- /dev/null +++ b/crates/preflight/src/gstate.rs @@ -0,0 +1,235 @@ +//! PDF/A-2 6.2.5, the extended graphics state: device-dependent settings a conforming file may not +//! rely on. +//! +//! - no transfer function: no `/TR` entry, and `/TR2` only as `/Default` (ISO 32000-1 10.4); +//! - no `/HTP` (halftone phase) entry, a PDF 1.2 key ISO 32000-1 no longer defines; +//! - halftones of type 1 or 5 only, without a `/HalftoneName` (ISO 32000-1 10.5.5); the halftones +//! a type 5 halftone holds for its colorants are checked the same way; +//! - a halftone's `/TransferFunction` only where one is needed: a type 5 halftone's component for +//! a non-primary colorant has one, and no other halftone does. The primary colour components +//! are those of the standard device spaces: Gray; Red, Green and Blue; Cyan, Magenta, Yellow +//! and Black (ISO 32000-1 10.5.5.6; ISO 32000-2 10.6.5.6). The `Default` component is left as +//! it is (ISO 32000-1 Table 130); +//! - a rendering intent (`/RI`) that is one of the four of ISO 32000-1 8.6.5.8, Table 70. +//! +//! Graphics state dictionaries are found as the `/ExtGState` entries of resource dictionaries +//! (`/Resources` of pages, forms, Type 3 fonts, patterns and annotation appearances, and an +//! interactive form's `/DR`), and as the `/ExtGState` of a shading pattern (ISO 32000-1 8.7.4.3, +//! Table 76). They are looked for in what the document reaches from its trailer, following +//! references, but not into private application data (`/PieceInfo`), so an object only that data +//! refers to isn't read. A map shared by reference is read once. + +use std::collections::{BTreeSet, HashSet, VecDeque}; + +use pdfcraft_cos::{Dict, Document, ObjRef, Object}; + +/// How deep a direct object is followed: as deep as the parser nests them. +const MAX_DEPTH: u8 = 128; +/// The rendering intents of ISO 32000-1 Table 70. +const INTENTS: [&[u8]; 4] = [b"AbsoluteColorimetric", b"RelativeColorimetric", b"Saturation", b"Perceptual"]; +/// The entries of a type 5 halftone that aren't colorant halftones. +const HALFTONE_KEYS: [&[u8]; 3] = [b"Type", b"HalftoneType", b"HalftoneName"]; +/// The primary colour components of DeviceGray, DeviceRGB and DeviceCMYK, whose halftones take +/// the graphics state's transfer function. +const PRIMARY: [&[u8]; 8] = [b"Gray", b"Red", b"Green", b"Blue", b"Cyan", b"Magenta", b"Yellow", b"Black"]; +/// The entries that hold resource dictionaries. +const RESOURCES: [&[u8]; 2] = [b"Resources", b"DR"]; + +/// What the graphics states of a document break of 6.2.5. +#[derive(Debug, Default, PartialEq, Eq)] +pub(crate) struct Findings { + /// A graphics state has a `/TR` entry. + pub(crate) transfer: bool, + /// A graphics state has a `/TR2` other than `/Default`. + pub(crate) transfer2: bool, + /// A graphics state has an `/HTP`. + pub(crate) phase: bool, + /// Halftone types other than 1 and 5, in order. + pub(crate) halftone_types: BTreeSet, + /// A halftone's `/HalftoneType` is missing or isn't an integer. + pub(crate) untyped: bool, + /// A halftone has a `/HalftoneName`. + pub(crate) halftone_name: bool, + /// A halftone that may not have a `/TransferFunction` has one. + pub(crate) halftone_transfer: bool, + /// Non-primary colorants whose halftone has no `/TransferFunction`, in name order. + pub(crate) missing_transfer: BTreeSet, + /// Rendering intents outside Table 70, in name order. + pub(crate) intents: BTreeSet, + /// A rendering intent isn't a name. + pub(crate) intent_not_name: bool, +} + +/// The 6.2.5 findings over what `doc` reaches from its trailer. +pub(crate) fn check(doc: &Document) -> Findings { + scan(doc).findings +} + +/// How many objects [`check`] reads. +#[cfg(test)] +pub(crate) fn reached(doc: &Document) -> usize { + scan(doc).reached.len() +} + +fn scan(doc: &Document) -> Scan<'_> { + let mut scan = Scan { doc, read: HashSet::new(), reached: HashSet::new(), queue: VecDeque::new(), findings: Findings::default() }; + scan.walk(&Object::Dict(doc.trailer().clone()), 0); + // References are followed from a queue, not by recursion: a chain of them can be as long as + // the file. + while let Some(r) = scan.queue.pop_front() { + let o = doc.get(r); + scan.walk(&o, 0); + } + scan +} + +struct Scan<'a> { + doc: &'a Document, + /// Resource dictionaries, `/ExtGState` maps and graphics states read so far, by object number. + read: HashSet, + /// Object numbers reached so far, queued or walked. Both sets go by number, as + /// [`Document::get`] does: references differing only in generation reach the same object. + reached: HashSet, + queue: VecDeque, + findings: Findings, +} + +impl Scan<'_> { + fn walk(&mut self, o: &Object, depth: u8) { + if depth > MAX_DEPTH { + return; + } + let d = match o { + Object::Ref(r) => { + if self.reached.insert(r.num) { + self.queue.push_back(*r); + } + return; + } + Object::Array(a) => { + for v in a { + self.walk(v, depth.saturating_add(1)); + } + return; + } + Object::Dict(d) => d, + Object::Stream(s) => &s.dict, + _ => return, + }; + self.holder(d); + for (k, v) in d.iter() { + if k.as_slice() != b"PieceInfo" { + self.walk(v, depth.saturating_add(1)); + } + } + } + + /// Whether `o` is a reference read before; marks it read. + fn read_before(&mut self, o: &Object) -> bool { + matches!(o, Object::Ref(r) if !self.read.insert(r.num)) + } + + /// A dictionary that holds graphics states: through its resources, or as a shading pattern. + fn holder(&mut self, d: &Dict) { + for key in RESOURCES { + if let Some(resources) = d.get(key) { + self.resources(resources); + } + } + if d.get(b"PatternType").is_some() + && let Some(gs) = d.get(b"ExtGState") + && !self.read_before(gs) + && let Some(gs) = self.doc.resolve(gs).as_dict() + { + self.gstate(gs); + } + } + + fn resources(&mut self, resources: &Object) { + if self.read_before(resources) { + return; + } + let resources = self.doc.resolve(resources); + let Some(states) = resources.as_dict().and_then(|r| r.get(b"ExtGState")) else { return }; + if self.read_before(states) { + return; + } + let states = self.doc.resolve(states); + let Some(states) = states.as_dict() else { return }; + for (_, gs) in states.iter() { + if !self.read_before(gs) + && let Some(gs) = self.doc.resolve(gs).as_dict() + { + self.gstate(gs); + } + } + } + + /// `key` of `d`, resolved, unless it is missing or null (ISO 32000-1 7.3.9). + fn present(&self, d: &Dict, key: &[u8]) -> Option> { + d.get(key).map(|v| self.doc.resolve(v)).filter(|v| !matches!(**v, Object::Null)) + } + + fn gstate(&mut self, gs: &Dict) { + if self.present(gs, b"TR").is_some() { + self.findings.transfer = true; + } + if self.present(gs, b"TR2").is_some_and(|f| f.as_name() != Some(b"Default")) { + self.findings.transfer2 = true; + } + if self.present(gs, b"HTP").is_some() { + self.findings.phase = true; + } + if let Some(ht) = self.present(gs, b"HT") + && let Some(ht) = ht.as_dict() + { + self.halftone(ht, None); + } + if let Some(ri) = self.present(gs, b"RI") { + match ri.as_name() { + Some(ri) if !INTENTS.contains(&ri) => { + self.findings.intents.insert(String::from_utf8_lossy(ri).into_owned()); + } + Some(_) => {} + None => self.findings.intent_not_name = true, + } + } + } + + /// A halftone dictionary or stream: the graphics state's own (`colorant` is `None`), or the + /// one a type 5 halftone holds for `colorant`. + fn halftone(&mut self, ht: &Dict, colorant: Option<&[u8]>) { + // An integer (ISO 32000-1 Table 128), not a real of the same value. + let kind = self.present(ht, b"HalftoneType").and_then(|t| if let Object::Int(t) = *t { Some(t) } else { None }); + match kind { + None => self.findings.untyped = true, + Some(1 | 5) => {} + Some(kind) => { + self.findings.halftone_types.insert(kind); + } + } + if self.present(ht, b"HalftoneName").is_some() { + self.findings.halftone_name = true; + } + let transfer = self.present(ht, b"TransferFunction").is_some(); + match colorant { + Some(b"Default") => {} + Some(c) if !PRIMARY.contains(&c) => { + if !transfer { + self.findings.missing_transfer.insert(String::from_utf8_lossy(c).into_owned()); + } + } + _ => self.findings.halftone_transfer |= transfer, + } + if colorant.is_none() && kind == Some(5) { + for (k, v) in ht.iter() { + if HALFTONE_KEYS.contains(&k.as_slice()) { + continue; + } + if let Some(component) = self.doc.resolve(v).as_dict() { + self.halftone(component, Some(k)); + } + } + } + } +} diff --git a/crates/preflight/src/lib.rs b/crates/preflight/src/lib.rs index ad2d790e20..fd609d96e0 100644 --- a/crates/preflight/src/lib.rs +++ b/crates/preflight/src/lib.rs @@ -21,6 +21,7 @@ #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +mod gstate; mod header; pub mod icc; mod limits; @@ -183,6 +184,16 @@ pub const WORDINGS: &[&str] = &[ "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", + "A graphics state has a transfer function (TR)", + "A graphics state has a transfer function (TR2) other than Default", + "A graphics state has a halftone phase (HTP)", + "A halftone has type {}: only types 1 and 5 are allowed", + "A halftone's HalftoneType is missing or isn't an integer", + "A halftone has a HalftoneName", + "A halftone has a transfer function, which only a non-primary colorant's halftone may have", + "The halftone for the colorant {} has no transfer function", + "A graphics state has the rendering intent {}, which isn't a standard one", + "A graphics state has a rendering intent that isn't a name", "The form asks viewers to make appearances (NeedAppearances)", // Pages, actions, annotations and fields. "Page {} has additional actions", @@ -765,6 +776,38 @@ pub fn verify(doc: &Document, level: Level) -> Vec { 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); } + // 6.2.5: the extended graphics state. + let gs = gstate::check(doc); + if gs.transfer { + issue("6.2.5", "A graphics state has a transfer function (TR)", None, None, false); + } + if gs.transfer2 { + issue("6.2.5", "A graphics state has a transfer function (TR2) other than Default", None, None, false); + } + if gs.phase { + issue("6.2.5", "A graphics state has a halftone phase (HTP)", None, None, false); + } + for kind in gs.halftone_types { + issue("6.2.5", "A halftone has type {}: only types 1 and 5 are allowed", Some(kind.to_string()), None, false); + } + if gs.untyped { + issue("6.2.5", "A halftone's HalftoneType is missing or isn't an integer", None, None, false); + } + if gs.halftone_name { + issue("6.2.5", "A halftone has a HalftoneName", None, None, false); + } + if gs.halftone_transfer { + issue("6.2.5", "A halftone has a transfer function, which only a non-primary colorant's halftone may have", None, None, false); + } + for colorant in gs.missing_transfer { + issue("6.2.5", "The halftone for the colorant {} has no transfer function", Some(colorant), None, false); + } + for intent in gs.intents { + issue("6.2.5", "A graphics state has the rendering intent {}, which isn't a standard one", Some(intent), None, false); + } + if gs.intent_not_name { + issue("6.2.5", "A graphics state has a rendering intent that isn't a name", None, 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/tests.rs b/crates/preflight/src/tests.rs index a475e935f2..ecc37e8c84 100644 --- a/crates/preflight/src/tests.rs +++ b/crates/preflight/src/tests.rs @@ -1589,3 +1589,91 @@ fn file_header_is_checked_and_rewritten() { let report = convert(&mut doc, Level::A2b).unwrap(); assert!(!report.fixed.iter().any(|f| f == "Rewrote the file header")); } + +/// PDF/A-2 6.2.5: a graphics state has no transfer function, halftones of type 1 or 5 without a +/// name, and a standard rendering intent, wherever it is kept. +#[test] +fn extended_graphics_states_are_checked() { + let gs = |states: &str| -> Vec { + let page = format!("<< /Type /Page /Parent 2 0 R /Resources << /ExtGState << {states} >> >> >>"); + 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 [0] /C1 [1] /N 1 >>", + "<< /HalftoneType 1 /Frequency 60 /Angle 45 /SpotFunction /Round >>", + "<< /TR 4 0 R >>", + "<< /Type /Halftone /HalftoneType 6 /Width 2 /Height 2 /Length 4 >>\nstream\nabcd\nendstream", + "<< /Resources << /ExtGState << /X << /TR 4 0 R >> >> >> >>", + ], + "", + ); + let issues = verify(&doc, Level::A2b).into_iter().filter(|i| i.clause == "6.2.5"); + issues.inspect(|i| assert!(!i.fixable, "{}", i.message)).map(|i| i.message).collect() + }; + let tr = "A graphics state has a transfer function (TR)"; + let tr2 = "A graphics state has a transfer function (TR2) other than Default"; + let name = "A halftone has a HalftoneName"; + // What 6.2.5 allows. + assert!(gs("/A << /TR2 /Default /HT 5 0 R /RI /Perceptual >> /B << /HT /Default /RI /AbsoluteColorimetric >>").is_empty()); + assert!(gs("/A << /HT << /HalftoneType 5 /Default 5 0 R /Cyan 5 0 R >> /RI /Saturation >> /B << /TR null >>").is_empty()); + // Transfer functions. + assert_eq!(gs("/A << /TR 4 0 R >>"), [tr]); + assert_eq!(gs("/A << /TR /Identity >>"), [tr]); + assert_eq!(gs("/A 6 0 R"), [tr]); + assert_eq!(gs("/A << /TR2 4 0 R >>"), [tr2]); + assert_eq!(gs("/A << /TR2 /Identity >>"), [tr2]); + assert_eq!(gs("/A << /HTP [0 1] >>"), ["A graphics state has a halftone phase (HTP)"]); + // Halftones: the type, also of a type 5 halftone's colorants and of a stream, and the name. + assert_eq!(gs("/A << /HT << /HalftoneType 6 >> >>"), ["A halftone has type 6: only types 1 and 5 are allowed"]); + assert_eq!(gs("/A << /HT 7 0 R >>"), ["A halftone has type 6: only types 1 and 5 are allowed"]); + assert_eq!( + gs("/A << /HT << /HalftoneType 5 /Default 5 0 R /Magenta << /HalftoneType 10 >> >> >>"), + ["A halftone has type 10: only types 1 and 5 are allowed"] + ); + assert_eq!(gs("/A << /HT << /HalftoneType 1 /HalftoneName (Screen) >> >>"), [name]); + assert_eq!(gs("/A << /HT << /HalftoneType 5 /Default << /HalftoneType 1 /HalftoneName (S) >> >> >>"), [name]); + // Rendering intents. + assert_eq!(gs("/A << /RI /Custom >>"), ["A graphics state has the rendering intent Custom, which isn't a standard one"]); + assert_eq!(gs("/A << /RI (Perceptual) >>"), ["A graphics state has a rendering intent that isn't a name"]); + // A halftone's type is required; a null name is no name. + let untyped = ["A halftone's HalftoneType is missing or isn't an integer"]; + assert_eq!(gs("/A << /HT << /Frequency 60 >> >>"), untyped); + assert_eq!(gs("/A << /HT << /HalftoneType 1.0 >> >>"), untyped); + assert!(gs("/A << /HT << /HalftoneType 1 /HalftoneName null >> >>").is_empty()); + // Transfer functions in halftones: only, and always, for a non-primary colorant. + let in_halftone = "A halftone has a transfer function, which only a non-primary colorant's halftone may have"; + assert_eq!(gs("/A << /HT << /HalftoneType 1 /TransferFunction 4 0 R >> >>"), [in_halftone]); + assert_eq!(gs("/A << /HT << /HalftoneType 5 /Default 5 0 R /Cyan << /HalftoneType 1 /TransferFunction /Identity >> >> >>"), [in_halftone]); + // The primary components are those of DeviceGray, DeviceRGB and DeviceCMYK (ISO 32000-1 10.5.5.6). + assert_eq!(gs("/A << /HT << /HalftoneType 5 /Default 5 0 R /Gray << /HalftoneType 1 /TransferFunction /Identity >> >> >>"), [in_halftone]); + assert_eq!( + gs("/A << /HT << /HalftoneType 5 /Default 5 0 R /Spot 5 0 R /Red 5 0 R /Gray 5 0 R >> >>"), + ["The halftone for the colorant Spot has no transfer function"] + ); + assert!(gs("/A << /HT << /HalftoneType 5 /Default 5 0 R /Black 5 0 R /Spot << /HalftoneType 1 /TransferFunction 4 0 R >> >> >>").is_empty()); + // Private application data isn't graphics resources. + assert!( + gs("/A << /RI /Perceptual >> >> /PieceInfo << /App << /Private << /Resources << /ExtGState << /X << /TR 4 0 R >> >> >> >> >>").is_empty() + ); + // Nor is an object (8) only private data refers to; one the page refers to is read. + assert!(gs("/A << /RI /Perceptual >> >> /PieceInfo << /App << /Private 8 0 R >> >> /Z <<").is_empty()); + assert_eq!(gs("/A << /RI /Perceptual >> >> /Other 8 0 R /Z <<"), [tr]); + // A shading pattern holds one graphics state, not a dictionary of them. + let pattern = |gs: &str| format!("/A << /RI /Perceptual >> >> /Pattern << /P << /PatternType 2 /Shading << /ShadingType 2 >> /ExtGState {gs} >>"); + assert_eq!(gs(&pattern("<< /TR 4 0 R >>")), [tr]); + assert!(gs(&pattern("<< /HT << /HalftoneType 1 >> >>")).is_empty()); +} + +/// From the review of the 6.2.5 check: references that differ only in generation reach the same +/// object, which is read once. +#[test] +fn graphics_state_scan_reads_each_object_once_whatever_the_generation() { + let gens: String = (0..10).map(|g| format!("4 {g} R ")).collect(); + let page = format!("<< /Type /Page /Parent 2 0 R /Same [{gens}] >>"); + let doc = + build(&["<< /Type /Catalog /Pages 2 0 R >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>", &page, "[1 2 3]"], ""); + // The catalog, the page tree, the page and object 4. + assert_eq!(gstate::reached(&doc), 4); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index ee74ee3e26..6bd4ea64cb 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", "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", "crates/preflight/src/tests.rs::file_header_is_checked_and_rewritten"] -notes = "PDF/A-2b/3b structural rules: the file header (6.1.2), 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." +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", "crates/preflight/src/tests.rs::file_header_is_checked_and_rewritten", "crates/preflight/src/tests.rs::extended_graphics_states_are_checked", "crates/preflight/src/tests.rs::graphics_state_scan_reads_each_object_once_whatever_the_generation"] +notes = "PDF/A-2b/3b structural rules: the file header (6.1.2), 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, graphics states (6.2.5: TR, TR2, HTP, halftone types, names and transfer functions with the primary components of ISO 32000-1 10.5.5.6, rendering intents), 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"