mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 04:51:37 +00:00
M11: PDF/A verify finds empty naming entries in XMP extension schema descriptions (6.6.2.3.3)
A namespace URI, prefix, name, value type, type or category entry that is there but empty names nothing; it is now reported with its own wording. Descriptions stay free to be empty Text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
1 parent
1820ba241e
commit
8ca675b25d
4 files changed
+129
-10
No files matched your search
@@ -155,6 +155,8 @@ pub const WORDINGS: &[&str] = &[
|
||||
"The XMP property {property} in metadata stream {object} should have the value type {type}",
|
||||
"The XMP extension schema {schema} in metadata stream {object} has no {entry} entry",
|
||||
"The XMP extension schema {schema} in metadata stream {object} describes {item} without a {entry} entry",
|
||||
"The XMP extension schema {schema} in metadata stream {object} has an empty {entry} entry",
|
||||
"The XMP extension schema {schema} in metadata stream {object} describes {item} with an empty {entry} entry",
|
||||
"The XMP extension schema container in metadata stream {object} holds no rdf:Bag",
|
||||
"The XMP extension schema container in metadata stream {object} holds {found}, not one rdf:Bag",
|
||||
"Metadata stream {object} writes the namespace {namespace} without a prefix, not with {prefix}",
|
||||
@@ -534,15 +536,22 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
}
|
||||
for m in extension.missing {
|
||||
let mut values = vec![("{schema}", m.schema), ("{object}", m.object.to_string()), ("{entry}", m.entry)];
|
||||
match m.item {
|
||||
None => located_issue(
|
||||
match (m.item, m.empty) {
|
||||
(None, false) => located_issue(
|
||||
"6.6.2.3.3",
|
||||
"The XMP extension schema {schema} in metadata stream {object} has no {entry} entry",
|
||||
values,
|
||||
m.object,
|
||||
false,
|
||||
),
|
||||
Some(item) => {
|
||||
(None, true) => located_issue(
|
||||
"6.6.2.3.3",
|
||||
"The XMP extension schema {schema} in metadata stream {object} has an empty {entry} entry",
|
||||
values,
|
||||
m.object,
|
||||
false,
|
||||
),
|
||||
(Some(item), false) => {
|
||||
values.push(("{item}", item));
|
||||
located_issue(
|
||||
"6.6.2.3.3",
|
||||
@@ -552,6 +561,16 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
false,
|
||||
);
|
||||
}
|
||||
(Some(item), true) => {
|
||||
values.push(("{item}", item));
|
||||
located_issue(
|
||||
"6.6.2.3.3",
|
||||
"The XMP extension schema {schema} in metadata stream {object} describes {item} with an empty {entry} entry",
|
||||
values,
|
||||
m.object,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in extension.not_bag {
|
||||
|
||||
@@ -924,6 +924,76 @@ fn xmp_extension_schema_descriptions_are_complete() {
|
||||
assert!(!issues[0].fixable);
|
||||
}
|
||||
|
||||
/// PDF/A-2 6.6.2.3.3: an entry that names something is incomplete when it is empty, while the
|
||||
/// human-readable descriptions may be empty (TechNote 0009, 4.3 to 4.6).
|
||||
#[test]
|
||||
fn xmp_extension_naming_entries_are_not_empty() {
|
||||
let complete = schema_without("");
|
||||
let emptied = |name: &str, value: &str, with: &str| {
|
||||
let element = format!("<{name}>{value}</{name}>");
|
||||
assert_eq!(complete.matches(&element).count(), 1, "{name}");
|
||||
complete.replace(&element, &format!("<{name}>{with}</{name}>"))
|
||||
};
|
||||
let said = |schema: &str| -> Vec<(String, bool)> { extension_findings(schema, OWN).missing.into_iter().map(|m| (m.entry, m.empty)).collect() };
|
||||
for (name, value) in [
|
||||
("pdfaSchema:namespaceURI", "http://example.com/ns/"),
|
||||
("pdfaSchema:prefix", "ex"),
|
||||
("pdfaProperty:name", "x"),
|
||||
("pdfaProperty:valueType", "x"),
|
||||
("pdfaProperty:category", "x"),
|
||||
("pdfaType:type", "x"),
|
||||
("pdfaType:namespaceURI", "x"),
|
||||
("pdfaType:prefix", "x"),
|
||||
("pdfaField:name", "x"),
|
||||
("pdfaField:valueType", "x"),
|
||||
] {
|
||||
assert_eq!(said(&emptied(name, value, "")), [(name.to_owned(), true)], "{name}");
|
||||
assert_eq!(said(&emptied(name, value, " \n\t ")), [(name.to_owned(), true)], "{name} blank");
|
||||
}
|
||||
for (name, value) in
|
||||
[("pdfaSchema:schema", "Some schema"), ("pdfaProperty:description", "x"), ("pdfaType:description", "x"), ("pdfaField:description", "x")]
|
||||
{
|
||||
assert_eq!(said(&emptied(name, value, "")), [], "{name}");
|
||||
}
|
||||
// An empty element written shut, and an empty attribute, are empty too.
|
||||
let shut = complete.replace("<pdfaType:prefix>x</pdfaType:prefix>", "<pdfaType:prefix/>");
|
||||
assert_eq!(said(&shut), [("pdfaType:prefix".to_owned(), true)]);
|
||||
let attribute = r#"</rdf:li><rdf:li><rdf:Description pdfaSchema:schema="S" pdfaSchema:namespaceURI="http://e/" pdfaSchema:prefix=" "/>"#;
|
||||
assert_eq!(said(&format!("{complete}{attribute}")), [("pdfaSchema:prefix".to_owned(), true)]);
|
||||
// A URI given as an rdf:resource, or a value given as rdf:value, is a value.
|
||||
let resource =
|
||||
complete.replace("<pdfaType:namespaceURI>x</pdfaType:namespaceURI>", r#"<pdfaType:namespaceURI rdf:resource="http://t.example/"/>"#);
|
||||
assert_eq!(said(&resource), []);
|
||||
let value = complete.replace("<pdfaType:type>x</pdfaType:type>", r#"<pdfaType:type rdf:value="T"/>"#);
|
||||
assert_eq!(said(&value), []);
|
||||
// A comment is not a value.
|
||||
assert_eq!(said(&emptied("pdfaType:prefix", "x", "<!-- t -->")), [("pdfaType:prefix".to_owned(), true)]);
|
||||
|
||||
// Through verify, with their own wordings.
|
||||
let xmp = concat!(
|
||||
r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">"#,
|
||||
r#"<rdf:Description rdf:about="" xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" "#,
|
||||
r#"xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#" xmlns:pdfaType="http://www.aiim.org/pdfa/ns/type#">"#,
|
||||
r#"<pdfaExtension:schemas><rdf:Bag><rdf:li rdf:parseType="Resource">"#,
|
||||
r#"<pdfaSchema:schema>S</pdfaSchema:schema><pdfaSchema:namespaceURI>http://a.example/</pdfaSchema:namespaceURI>"#,
|
||||
r#"<pdfaSchema:prefix></pdfaSchema:prefix><pdfaSchema:valueType><rdf:Seq><rdf:li rdf:parseType="Resource">"#,
|
||||
r#"<pdfaType:type>T</pdfaType:type><pdfaType:namespaceURI>http://t.example/</pdfaType:namespaceURI>"#,
|
||||
r#"<pdfaType:prefix> </pdfaType:prefix><pdfaType:description>d</pdfaType:description>"#,
|
||||
r#"<pdfaType:field><rdf:Seq/></pdfaType:field></rdf:li></rdf:Seq></pdfaSchema:valueType>"#,
|
||||
r#"</rdf:li></rdf:Bag></pdfaExtension:schemas></rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
);
|
||||
let stream = format!("<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n{xmp}\nendstream", xmp.len());
|
||||
let doc = build(&["<< /Type /Catalog /Pages 2 0 R /Metadata 3 0 R >>", "<< /Type /Pages /Kids [] /Count 0 >>", &stream], "");
|
||||
let messages: Vec<String> = verify(&doc, Level::A2b).into_iter().filter(|i| i.clause == "6.6.2.3.3").map(|i| i.message).collect();
|
||||
assert_eq!(
|
||||
messages,
|
||||
[
|
||||
"The XMP extension schema http://a.example/ in metadata stream 3 0 R has an empty pdfaSchema:prefix entry",
|
||||
"The XMP extension schema http://a.example/ in metadata stream 3 0 R describes T with an empty pdfaType:prefix entry",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// PDF/A-2 6.6.2.3.3: each incomplete description is reported by itself, named by its schema's
|
||||
/// namespace URI, its own name and its metadata stream, and so is each misused prefix and container.
|
||||
#[test]
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
//! `pdfaProperty`, `pdfaType`, `pdfaField`): "Unlike predefined XMP schemas, the namespace
|
||||
//! prefixes are not only preferred, but required" (4.1).
|
||||
//!
|
||||
//! An entry counts as present written as an element or as an attribute of the description.
|
||||
//! An entry counts as present written as an element or as an attribute of the description. An
|
||||
//! entry that names something is empty when it holds no value: a namespace URI or prefix, a
|
||||
//! property, value type or field name, a value type reference, a property category. TechNote 0009
|
||||
//! asks for names that are valid XML names (4.4, 4.6) and a category that is internal or external
|
||||
//! (4.4), and an empty prefix or namespace binds nothing. The human-readable descriptions
|
||||
//! (`pdfaSchema:schema` and the `description` entries) are Text and may be empty.
|
||||
|
||||
use pdfcraft_cos::ObjRef;
|
||||
use roxmltree::{Document, Node};
|
||||
@@ -51,7 +56,7 @@ pub(crate) struct Findings {
|
||||
pub(crate) not_bag: Vec<NotBag>,
|
||||
}
|
||||
|
||||
/// A description without an entry it needs.
|
||||
/// A description without an entry it needs, or with the entry empty.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Missing {
|
||||
/// The metadata stream.
|
||||
@@ -63,8 +68,10 @@ pub(crate) struct Missing {
|
||||
/// description itself: its `pdfaProperty:name` or `pdfaType:type`, a field as `type/name`, or
|
||||
/// `#n` for the n-th of its list when it has no name.
|
||||
pub(crate) item: Option<String>,
|
||||
/// The entry it lacks, as `prefix:name`.
|
||||
/// The entry it lacks or holds empty, as `prefix:name`.
|
||||
pub(crate) entry: String,
|
||||
/// Whether the entry is there but empty, rather than absent.
|
||||
pub(crate) empty: bool,
|
||||
}
|
||||
|
||||
/// A description namespace written with another prefix than its own.
|
||||
@@ -132,12 +139,21 @@ impl Findings {
|
||||
self.missing.len().saturating_add(self.prefixes.len()).saturating_add(self.not_bag.len()) < MAX_FINDINGS
|
||||
}
|
||||
|
||||
/// Notes the entries of `ns` in `names` that the description `d`, at `at`, lacks.
|
||||
/// Notes the entries of `ns` in `names` that the description `d`, at `at`, lacks, or holds
|
||||
/// empty when they are entries that name something (see [`NAMING`]).
|
||||
fn need(&mut self, at: &Place, d: &Node<'_, '_>, ns: &str, prefix: &str, names: &[&str]) {
|
||||
for name in names {
|
||||
if entry(d, ns, name).is_none() && d.attribute((ns, *name)).is_none() && self.room() {
|
||||
let element = entry(d, ns, name);
|
||||
let attribute = d.attribute((ns, *name));
|
||||
let empty = match (element, attribute) {
|
||||
(None, None) => false,
|
||||
(Some(e), _) => NAMING.contains(name) && is_empty(&e),
|
||||
(None, Some(a)) => NAMING.contains(name) && a.trim().is_empty(),
|
||||
};
|
||||
if (empty || (element.is_none() && attribute.is_none())) && self.room() {
|
||||
let Place { object, schema, item } = at;
|
||||
self.missing.push(Missing { object: *object, schema: schema.clone(), item: item.clone(), entry: format!("{prefix}:{name}") });
|
||||
let entry = format!("{prefix}:{name}");
|
||||
self.missing.push(Missing { object: *object, schema: schema.clone(), item: item.clone(), entry, empty });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +179,20 @@ impl Findings {
|
||||
}
|
||||
}
|
||||
|
||||
/// The entries that name something, which an empty value leaves without a name: namespace URIs
|
||||
/// and prefixes, property, value type and field names, value type references and the property
|
||||
/// category.
|
||||
const NAMING: [&str; 6] = ["namespaceURI", "prefix", "name", "type", "valueType", "category"];
|
||||
|
||||
/// Whether the element entry `e` holds no value: no element, no text but white space, and no
|
||||
/// `rdf:resource` or `rdf:value` attribute with a value.
|
||||
fn is_empty(e: &Node<'_, '_>) -> bool {
|
||||
let blank = |v: Option<&str>| v.is_none_or(|v| v.trim().is_empty());
|
||||
!e.children().any(|c| c.is_element() || (c.is_text() && c.text().is_some_and(|t| !t.trim().is_empty())))
|
||||
&& blank(e.attribute((RDF, "resource")))
|
||||
&& blank(e.attribute((RDF, "value")))
|
||||
}
|
||||
|
||||
/// `#n` for the 0-based position `n`, counted from 1.
|
||||
fn position(n: usize) -> String {
|
||||
format!("#{}", n.saturating_add(1))
|
||||
|
||||
@@ -8004,7 +8004,7 @@ 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_mistyped_properties_name_their_type", "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, 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."
|
||||
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."
|
||||
|
||||
[[feature]]
|
||||
id = "optimize.pdfa-convert"
|
||||
|
||||
Reference in new issue
Block a user