From abf0f94e787560c1bd0b8353df856aca2369e03e Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Thu, 5 Nov 2026 16:56:37 +0800 Subject: [PATCH] M11: PDF/A verify allows only XMP's value forms: one rdf:value, parseType Resource, empty rdf:resource (6.6.2.3.1) A qualified value has exactly one rdf:value; a parse type other than Resource, which XMP doesn't use (ISO 16684-1 7.9.2.5), breaks the value; and an rdf:resource element holds no element content in any form. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn --- crates/preflight/src/tests.rs | 18 ++++++++++++++++++ crates/preflight/src/xmp_types.rs | 16 ++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/preflight/src/tests.rs b/crates/preflight/src/tests.rs index 8eea6afeb1..23677867a2 100644 --- a/crates/preflight/src/tests.rs +++ b/crates/preflight/src/tests.rs @@ -708,3 +708,21 @@ fn xmp_value_types_are_reported_by_verify() { ); assert_eq!(issues(&doc).len(), 1); } + +/// PDF/A-2 6.6.2.3.1, the RDF/XML value forms XMP allows (ISO 16684-1 7.9.2.5): one `rdf:value` +/// in a qualified value, no parse type but `Resource`, and an `rdf:resource` element left empty. +#[test] +fn xmp_value_forms_are_those_xmp_allows() { + // A qualified value has exactly one rdf:value. + assert_eq!( + mistyped(r#">5five"#), + ["crs:Contrast"] + ); + // Only parseType="Resource" has a place in XMP; an rdf:resource element holds no element. + assert_eq!(mistyped(r#">5"#), ["crs:Contrast"]); + assert_eq!(mistyped(r#">"#), ["xmpMM:DerivedFrom"]); + assert_eq!( + mistyped(r#">"#), + ["xmpMM:DerivedFrom"] + ); +} diff --git a/crates/preflight/src/xmp_types.rs b/crates/preflight/src/xmp_types.rs index 7ef694ba2f..84f3ce6d9b 100644 --- a/crates/preflight/src/xmp_types.rs +++ b/crates/preflight/src/xmp_types.rs @@ -535,6 +535,11 @@ fn fits(ty: Ty, e: &Node<'_, '_>, depth: usize) -> bool { let children: Vec> = e.children().filter(Node::is_element).collect(); // A qualified value: `rdf:value` holds the value itself (ISO 16684-1 7.9.2.5). let resource = e.attribute((RDF, "parseType")) == Some("Resource"); + // `parseType="Resource"` is the only parse type XMP has (ISO 16684-1 7.9.2.5); an + // `rdf:resource` element is empty. + if e.attribute((RDF, "parseType")).is_some_and(|p| p != "Resource") || e.attribute((RDF, "resource")).is_some() && !children.is_empty() { + return false; + } let description = match children.as_slice() { [d] if is(d, RDF, "Description") => Some(*d), _ => None, @@ -543,10 +548,13 @@ fn fits(ty: Ty, e: &Node<'_, '_>, depth: usize) -> bool { // attribute of the description or of an empty property element. Its qualifiers that are // predefined properties keep their types too. let node = if resource { Some(*e) } else { description }; - if let Some(n) = node - && let Some(v) = n.children().find(|c| is(c, RDF, "value")) - { - return blank(e) && blank(&n) && fits(ty, &v, deeper) && predefined_ok(&n, deeper); + if let Some(n) = node { + // A qualified value has one `rdf:value`. + match n.children().filter(|c| is(c, RDF, "value")).collect::>().as_slice() { + [] => {} + [v] => return blank(e) && blank(&n) && fits(ty, v, deeper) && predefined_ok(&n, deeper), + _ => return false, + } } let attribute_node = description.or_else(|| (children.is_empty() && !resource).then_some(*e)); if let Some(n) = attribute_node