Merge #6202: M11: PDF/A verify reports integers, reals, strings, names, CIDs and page boundaries past the implementation limits (optimize.pdfa-validate)

# Conflicts:
#	crates/preflight/src/lib.rs
#	parity/acrobat-features.toml
This commit is contained in:
bflatastic committed 2026-11-09 08:04:54 +07:00
commit 25ac77c360
4 files changed
+389 -2

No files matched your search

+29
View File
@@ -22,6 +22,7 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod icc;
mod limits;
mod xmp;
mod xmp_extension;
mod xmp_types;
@@ -164,6 +165,12 @@ pub const WORDINGS: &[&str] = &[
"Metadata stream {object} writes the namespace {namespace} with the prefix {written}, not {prefix}",
// The document as a whole.
"The document is encrypted",
"An integer is outside the 32-bit range",
"A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸",
"A CMap maps a character code to a CID above 65535",
"A string is longer than 32767 bytes",
"A name is longer than 127 bytes",
"Page {} has a boundary smaller than 3 or larger than 14400 units",
"Device colour is used, but there is no PDF/A output intent",
"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",
@@ -714,6 +721,28 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
}
let drawing = drawing_fonts(doc, &objs);
check_objects(doc, level, &objs, &drawing, &page_of, &mut issue);
// 6.1.13: the implementation limits of ISO 32000-1 Annex C.
let over = limits::exceeded(doc, &objs, &referenced_content(doc, &objs));
if over.int {
issue("6.1.13", "An integer is outside the 32-bit range", None, None, false);
}
if over.real {
issue("6.1.13", "A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸", None, None, false);
}
if over.string {
issue("6.1.13", "A string is longer than 32767 bytes", None, None, false);
}
if limits::cid_over_limit(doc, &objs) {
issue("6.1.13", "A CMap maps a character code to a CID above 65535", None, None, false);
}
if over.name {
issue("6.1.13", "A name is longer than 127 bytes", None, None, false);
}
for (i, p) in pages.iter().enumerate() {
if limits::page_box_out_of_range(doc, &p.dict) {
issue("6.1.13", "Page {} has a boundary smaller than 3 or larger than 14400 units", Some((i + 1).to_string()), Some(i), 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();
+227
View File
@@ -0,0 +1,227 @@
//! PDF/A-2 6.1.13: a file stays within the implementation limits of ISO 32000-1 Annex C (Table C.1
//! and the page size limits after it): integers that fit in 32 bits, strings of at most 32767
//! bytes, names of at most 127 bytes, and page boundaries between 3 and 14400 units wide and high.
//!
//! The values are read from the trailer, the indirect objects and the content streams, as parsed:
//! a name written with `#xx` escapes counts its decoded bytes. A page's `/Contents` streams are read
//! joined, as one content stream (an operator's operands may end one stream and the operator start
//! the next, ISO 32000-1 7.8.2). Content is decoded within one allowance for the whole document.
//! Real numbers beyond ±3.403 × 10³⁸ are reported, and so are nonzero ones nearer zero than
//! ±1.175 × 10⁻³⁸ (ISO 32000-1 Table C.1 lets a reader round them to zero; PDF/A-2 does not allow
//! them, PDF Association TechNote 0010 A018), and so is an embedded CMap of a Type 0 font
//! (or one it uses through `/UseCMap`) that maps a code to a CID above 65535. An integer token too
//! long even for a real reads as one past that range, and is reported as a real.
use std::collections::HashSet;
use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object};
/// Table C.1: the smallest and largest integer values.
const MIN_INT: i64 = i32::MIN as i64;
const MAX_INT: i64 = i32::MAX as i64;
/// Table C.1: the largest real value (approximate).
const MAX_REAL: f64 = 3.403e38;
/// Table C.1: the nonzero real value closest to zero (approximate).
const MIN_REAL: f64 = 1.175e-38;
/// Table C.1: the largest character identifier.
const MAX_CID: u64 = 65535;
/// The decoded CMap data, across all the document's embedded CMaps, the scan reads.
const MAX_CMAP_BYTES: usize = 16 << 20;
/// How many CMaps a `/UseCMap` chain is followed through.
const MAX_CMAP_CHAIN: usize = 8;
/// Table C.1: the longest string, in bytes.
const MAX_STRING: usize = 32767;
/// Table C.1: the longest name, in bytes.
const MAX_NAME: usize = 127;
/// The smallest and largest page boundary, in default user space units.
const MIN_PAGE: f64 = 3.0;
const MAX_PAGE: f64 = 14400.0;
/// 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;
/// The decoded content, across all the document's content streams, the scan reads.
const MAX_CONTENT_BYTES: usize = 256 << 20;
/// The page boundaries, of which only the media and crop boxes are inherited.
const BOXES: [&[u8]; 5] = [b"MediaBox", b"CropBox", b"BleedBox", b"TrimBox", b"ArtBox"];
/// Which limits a value went past.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Exceeded {
pub(crate) int: bool,
pub(crate) real: bool,
pub(crate) string: bool,
pub(crate) name: bool,
}
impl Exceeded {
fn all(self) -> bool {
self.int && self.real && self.string && self.name
}
fn name(&mut self, n: &[u8]) {
self.name |= n.len() > MAX_NAME;
}
/// One value and, within [`MAX_DEPTH`], what it holds: a dictionary's keys count as names.
fn value(&mut self, o: &Object, depth: u8) {
if self.all() || depth > MAX_DEPTH {
return;
}
match o {
Object::Int(i) => self.int |= !(MIN_INT..=MAX_INT).contains(i),
Object::Real(r) => self.real |= !r.is_finite() || r.abs() > MAX_REAL || (*r != 0.0 && r.abs() < MIN_REAL),
Object::String(s) => self.string |= s.bytes.len() > MAX_STRING,
Object::Name(n) => self.name(n),
Object::Array(a) => {
for v in a {
self.value(v, depth.saturating_add(1));
}
}
Object::Dict(d) => self.dict(d, depth),
Object::Stream(s) => self.dict(&s.dict, depth),
Object::Null | Object::Bool(_) | Object::Ref(_) => {}
}
}
/// The operands of one content stream, inline images' dictionaries included.
fn content(&mut self, data: &[u8]) {
for op in pdfcraft_content::ops(data) {
if self.all() {
return;
}
for v in &op.operands {
self.value(v, 0);
}
if let Some((image, _)) = &op.inline {
self.dict(image, 0);
}
}
}
fn dict(&mut self, d: &Dict, depth: u8) {
for (k, v) in d.iter() {
self.name(k);
self.value(v, depth.saturating_add(1));
}
}
}
/// The limits that the trailer, the indirect objects `objs` and the content streams go past.
/// `content` holds the content streams only a reference identifies (a page's `/Contents`, Type 3
/// glyph procedures), besides form XObjects and appearance streams, which say so themselves.
pub(crate) fn exceeded(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)], content: &HashSet<ObjRef>) -> Exceeded {
let mut found = Exceeded::default();
found.dict(doc.trailer(), 0);
for (_, o) in objs {
if found.all() {
return found;
}
found.value(o, 0);
}
let mut budget = DecodeBudget::new(MAX_CONTENT_BYTES, 0);
let mut read: HashSet<ObjRef> = HashSet::new();
for page in pdfcraft_model::pages(doc) {
if found.all() || budget.remaining() == 0 {
return found;
}
let refs = contents(doc, &page.dict);
let mut joined = Vec::new();
for r in &refs {
if let Object::Stream(s) = &*doc.get(*r)
&& let Ok(data) = budget.decode(s)
{
joined.extend_from_slice(&data);
joined.push(b' ');
}
}
read.extend(refs);
found.content(&joined);
}
for (r, o) in objs {
if found.all() || budget.remaining() == 0 {
break;
}
if let Object::Stream(s) = &**o
&& !read.contains(r)
&& (content.contains(r) || crate::is_content(&s.dict))
&& let Ok(data) = budget.decode(s)
{
found.content(&data);
}
}
found
}
/// A page's `/Contents` streams, in order.
fn contents(doc: &Document, page: &Dict) -> Vec<ObjRef> {
match page.get(b"Contents") {
Some(Object::Ref(r)) => match &*doc.get(*r) {
Object::Array(a) => a.iter().filter_map(Object::as_ref).collect(),
_ => vec![*r],
},
Some(Object::Array(a)) => a.iter().filter_map(Object::as_ref).collect(),
_ => Vec::new(),
}
}
/// Whether one of a page's boundaries (with the media and crop boxes it inherits) is narrower or
/// lower than 3 units, or wider or higher than 14400.
pub(crate) fn page_box_out_of_range(doc: &Document, page: &Dict) -> bool {
BOXES.iter().filter_map(|k| page.get(k)).any(|b| {
let b = doc.resolve(b);
let v: Vec<f64> = b.as_array().map(|a| a.iter().filter_map(|x| doc.resolve(x).as_f64()).collect()).unwrap_or_default();
let [x0, y0, x1, y1] = v.as_slice() else { return false };
let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs());
// A size past `f64` (corners near ±1e308) is as much too large as any other.
[w, h].iter().any(|s| !(MIN_PAGE..=MAX_PAGE).contains(s))
})
}
/// Whether an embedded CMap of a Type 0 font among `objs`, or one it uses through `/UseCMap`, maps
/// a code to a CID above 65535: a `begincidrange` entry `<low> <high> cid` reaches
/// `cid + high − low`, a `begincidchar` entry `<code> cid` reaches `cid` (ISO 32000-1 9.7.5.4). A
/// CMap named rather than embedded is a predefined one, within the limit. The CMaps are decoded
/// within one allowance for the document.
pub(crate) fn cid_over_limit(doc: &Document, objs: &[(ObjRef, std::sync::Arc<Object>)]) -> bool {
let mut budget = DecodeBudget::new(MAX_CMAP_BYTES, 0);
let mut read: HashSet<ObjRef> = HashSet::new();
for (_, o) in objs {
let Some(d) = o.as_dict() else { continue };
if d.name(b"Subtype") != Some(b"Type0") {
continue;
}
let mut next = d.get(b"Encoding").and_then(Object::as_ref);
for _ in 0..MAX_CMAP_CHAIN {
let Some(r) = next.filter(|r| read.insert(*r)) else { break };
if budget.remaining() == 0 {
return false;
}
let Object::Stream(cmap) = &*doc.get(r) else { break };
if budget.decode(cmap).is_ok_and(|data| maps_past_limit(&data)) {
return true;
}
next = cmap.dict.get(b"UseCMap").and_then(Object::as_ref);
}
}
false
}
/// Whether the CMap program `data` maps a code to a CID above [`MAX_CID`]. The entries of a
/// `begincidrange` … `endcidrange` block are the operands of its `endcidrange`, read as content is.
fn maps_past_limit(data: &[u8]) -> bool {
let value = |o: &Object| -> Option<u64> {
match o {
Object::String(s) if s.bytes.len() <= 4 => Some(s.bytes.iter().fold(0u64, |v, b| (v << 8) | u64::from(*b))),
Object::Int(i) => u64::try_from(*i).ok(),
_ => None,
}
};
pdfcraft_content::ops(data).any(|op| match op.op.as_slice() {
b"endcidrange" => op.operands.as_chunks::<3>().0.iter().any(|[low, high, cid]| match (value(low), value(high), value(cid)) {
(Some(low), Some(high), Some(cid)) => cid.saturating_add(high.saturating_sub(low)) > MAX_CID,
_ => false,
}),
b"endcidchar" => op.operands.as_chunks::<2>().0.iter().any(|[_, cid]| value(cid).is_some_and(|cid| cid > MAX_CID)),
_ => false,
})
}
+131
View File
@@ -491,6 +491,137 @@ fn an_output_intent_profile_header_is_read_within_a_size_cap() {
}
}
/// PDF/A-2 6.1.13 holds a file to the implementation limits of ISO 32000-1 Annex C (Table C.1):
/// an integer past 32 bits, a string over 32767 bytes or a name over 127 bytes (decoded: a name
/// written with `#xx` escapes counts its bytes once read), in an object or in content; and a page
/// boundary under 3 or over 14400 units, read after inheritance, so a page that overrides its
/// parent's box is judged by its own.
#[test]
fn implementation_limits_are_reported() {
let limits =
|doc: &Document| -> Vec<String> { verify(doc, Level::A2b).into_iter().filter(|i| i.clause == "6.1.13").map(|i| i.message).collect() };
let doc = |extra: &str, content: &str, parent_box: &str, page_box: &str| {
let objects = [
format!("<< /Type /Catalog /Pages 2 0 R {extra} >>"),
format!("<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox {parent_box} >>"),
format!("<< /Type /Page /Parent 2 0 R /Contents 4 0 R {page_box} >>"),
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
];
let refs: Vec<&str> = objects.iter().map(String::as_str).collect();
build(&refs, "")
};
let ok = "[0 0 200 200]";
assert!(limits(&doc("", "", ok, "")).is_empty(), "{:?}", limits(&doc("", "", ok, "")));
assert_eq!(limits(&doc("/Big 2147483648", "", ok, "")), ["An integer is outside the 32-bit range"]);
assert_eq!(limits(&doc("/Small -2147483649", "", ok, "")), ["An integer is outside the 32-bit range"]);
assert!(limits(&doc("/Edge -2147483648 /Top 2147483647", "", ok, "")).is_empty());
assert_eq!(limits(&doc("", "2147483648 0 Td", ok, "")), ["An integer is outside the 32-bit range"]);
let long = "x".repeat(32768);
assert_eq!(limits(&doc(&format!("/S ({long})"), "", ok, "")), ["A string is longer than 32767 bytes"]);
assert_eq!(limits(&doc("", &format!("BT ({long}) Tj ET"), ok, "")), ["A string is longer than 32767 bytes"]);
assert!(limits(&doc(&format!("/S ({})", "x".repeat(32767)), "", ok, "")).is_empty());
let name = "n".repeat(128);
assert_eq!(limits(&doc(&format!("/V /{name}"), "", ok, "")), ["A name is longer than 127 bytes"]);
assert_eq!(limits(&doc(&format!("/{name} 1"), "", ok, "")), ["A name is longer than 127 bytes"]);
assert_eq!(limits(&doc("", &format!("/{name} Do"), ok, "")), ["A name is longer than 127 bytes"]);
// 200 bytes as written, 66 once the escapes are read.
let escaped = format!("A{}", "#41".repeat(65) + &"B".repeat(4));
assert!(limits(&doc(&format!("/V /{escaped}"), "", ok, "")).is_empty());
assert_eq!(limits(&doc("", "", "[0 0 2 200]", "")), ["Page 1 has a boundary smaller than 3 or larger than 14400 units"]);
assert_eq!(limits(&doc("", "", ok, "/CropBox [0 0 14401 200]")), ["Page 1 has a boundary smaller than 3 or larger than 14400 units"]);
assert!(limits(&doc("", "", "[0 0 2 2]", "/MediaBox [0 0 3 14400]")).is_empty(), "the page's own box overrides its parent's");
}
/// 6.1.13 also reads the trailer, a page's `/Contents` streams joined (operands may end one
/// stream and their operator start the next, ISO 32000-1 7.8.2), values nested as deep as the
/// parser allows, and a page box whose size overflows to infinity.
#[test]
fn implementation_limits_reach_the_trailer_split_content_deep_values_and_huge_boxes() {
let limits =
|doc: &Document| -> Vec<String> { verify(doc, Level::A2b).into_iter().filter(|i| i.clause == "6.1.13").map(|i| i.message).collect() };
let stream = |data: &str| {
format!(
"<< /Length {} >>
stream
{data}
endstream",
data.len()
)
};
let page = |contents: &str, catalog_extra: &str, media: &str, trailer: &str| {
let (first, second) = contents.split_once('|').unwrap_or((contents, ""));
build(
&[
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
&format!("<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox {media} >>"),
"<< /Type /Page /Parent 2 0 R /Contents [4 0 R 5 0 R] >>",
&stream(first),
&stream(second),
],
trailer,
)
};
let ok = "[0 0 200 200]";
assert!(limits(&page("|", "", ok, "")).is_empty());
assert_eq!(limits(&page("|", "", ok, "/Custom 2147483648")), ["An integer is outside the 32-bit range"]);
let long = "x".repeat(32768);
assert_eq!(limits(&page(&format!("BT ({long})|Tj ET"), "", ok, "")), ["A string is longer than 32767 bytes"]);
let deep = format!("/Deep {}2147483648{}", "[".repeat(40), "]".repeat(40));
assert_eq!(limits(&page("|", &deep, ok, "")), ["An integer is outside the 32-bit range"]);
// Corners near ±1e308 are reals past the limit too, and the width overflows to infinity.
assert_eq!(
limits(&page("|", "", "[-1e308 0 1e308 200]", "")),
[
"A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸",
"Page 1 has a boundary smaller than 3 or larger than 14400 units"
]
);
}
/// 6.1.13's two remaining limits of ISO 32000-1 Annex C: a real number beyond ±3.403 × 10³⁸ (or
/// nonzero and nearer zero than ±1.175 × 10⁻³⁸), and
/// a CID above 65535 that an embedded CMap of a Type 0 font maps to (a `begincidrange` reaching
/// past it, a `begincidchar` naming one, or a CMap it uses through `/UseCMap`).
#[test]
fn implementation_limits_cover_reals_and_cids() {
let limits =
|doc: &Document| -> Vec<String> { verify(doc, Level::A2b).into_iter().filter(|i| i.clause == "6.1.13").map(|i| i.message).collect() };
let stream = |dict: &str, data: &str| format!("<< {dict} /Length {} >>\nstream\n{data}\nendstream", data.len());
let doc = |catalog_extra: &str, cmap: &str, used: &str| {
let first = stream("/Type /CMap /CMapName /Test-A /UseCMap 6 0 R", cmap);
let second = stream("/Type /CMap /CMapName /Test-B", used);
build(
&[
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >>",
"<< /Type /Page /Parent 2 0 R /Resources << /Font << /F 4 0 R >> >> >>",
"<< /Type /Font /Subtype /Type0 /BaseFont /Test /Encoding 5 0 R /DescendantFonts [] >>",
&first,
&second,
],
"",
)
};
let range = |low: &str, high: &str, cid: u32| format!("1 begincidrange <{low}> <{high}> {cid} endcidrange");
let near = format!("34{}.0", "0".repeat(37));
let far = format!("4{}.0", "0".repeat(38));
assert!(limits(&doc(&format!("/R {near} /S -{near}"), &range("0000", "00ff", 65280), "")).is_empty(), "both at their limits");
assert_eq!(limits(&doc(&format!("/R {far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
assert_eq!(limits(&doc(&format!("/R -{far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
// A nonzero real nearer zero than ±1.175 × 10⁻³⁸ is not allowed either; zero and the
// smallest allowed magnitude are.
let tiny = format!("0.{}1", "0".repeat(39));
let smallest = format!("0.{}1175", "0".repeat(37));
assert!(limits(&doc(&format!("/Z 0.0 /M -0.0 /S {smallest} /T -{smallest}"), "", "")).is_empty(), "zero and the smallest real");
assert_eq!(limits(&doc(&format!("/R {tiny}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
assert_eq!(limits(&doc(&format!("/R -{tiny}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
let cid = ["A CMap maps a character code to a CID above 65535"];
assert_eq!(limits(&doc("", &range("3f00", "3fff", 65536), "")), cid);
assert_eq!(limits(&doc("", &range("0000", "0100", 65280), "")), cid, "the range reaches 65536");
assert_eq!(limits(&doc("", "1 begincidchar <01> 70000 endcidchar", "")), cid);
assert_eq!(limits(&doc("", &range("0000", "00ff", 0), &range("3f00", "3fff", 65536))), cid, "through /UseCMap");
}
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/" "#,
+2 -2
View File
@@ -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"]
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. 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"]
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."
[[feature]]
id = "optimize.pdfa-convert"