mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
Merge #6254: M11: PDF/A verify checks that XMP extension schema descriptions are complete (optimize.pdfa-validate)
This commit is contained in:
commit
4dc55320bc
4 files changed
+665
-4
No files matched your search
@@ -23,6 +23,7 @@
|
||||
|
||||
pub mod icc;
|
||||
mod xmp;
|
||||
mod xmp_extension;
|
||||
mod xmp_types;
|
||||
|
||||
use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
|
||||
@@ -153,6 +154,14 @@ pub const WORDINGS: &[&str] = &[
|
||||
"The title in the metadata and the document information differ",
|
||||
"The XMP property {property} in metadata stream {object} should have the value type {type}",
|
||||
"The XMP property {property} in metadata stream {object}, written {written}, 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}",
|
||||
"Metadata stream {object} writes the namespace {namespace} with the prefix {written}, not {prefix}",
|
||||
// The document as a whole.
|
||||
"The document is encrypted",
|
||||
"Device colour is used, but there is no PDF/A output intent",
|
||||
@@ -509,10 +518,12 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
|
||||
}
|
||||
// 6.2.2: output intent when device colour is used.
|
||||
let objs = objects(doc);
|
||||
// 6.6.2.3.1: the value types of predefined XMP properties, in every metadata stream.
|
||||
let mut mistyped = Vec::new();
|
||||
// 6.6.2.3.1 and 6.6.2.3.3: predefined XMP value types and extension schema descriptions, in
|
||||
// every metadata stream.
|
||||
let (mut mistyped, mut extension) = (Vec::new(), xmp_extension::Findings::default());
|
||||
xmp_types::for_each_packet(&objs, |object, packet| {
|
||||
mistyped.extend(xmp_types::mistyped(packet).into_iter().map(|((name, written), ty)| (object, name, written, ty)));
|
||||
extension.add(object, packet);
|
||||
});
|
||||
for (object, name, written, ty) in mistyped {
|
||||
let mut values = vec![("{property}", name.clone()), ("{object}", object.to_string()), ("{type}", ty)];
|
||||
@@ -535,6 +546,87 @@ 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, 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,
|
||||
),
|
||||
(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",
|
||||
"The XMP extension schema {schema} in metadata stream {object} describes {item} without a {entry} entry",
|
||||
values,
|
||||
m.object,
|
||||
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 {
|
||||
let object = c.object;
|
||||
if c.found.is_empty() {
|
||||
located_issue(
|
||||
"6.6.2.3.3",
|
||||
"The XMP extension schema container in metadata stream {object} holds no rdf:Bag",
|
||||
vec![("{object}", object.to_string())],
|
||||
object,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
let values = vec![("{object}", object.to_string()), ("{found}", c.found)];
|
||||
located_issue(
|
||||
"6.6.2.3.3",
|
||||
"The XMP extension schema container in metadata stream {object} holds {found}, not one rdf:Bag",
|
||||
values,
|
||||
object,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
for p in extension.prefixes {
|
||||
let mut values = vec![("{object}", p.object.to_string()), ("{namespace}", p.namespace.to_owned()), ("{prefix}", p.own.to_owned())];
|
||||
if p.written.is_empty() {
|
||||
located_issue(
|
||||
"6.6.2.3.3",
|
||||
"Metadata stream {object} writes the namespace {namespace} without a prefix, not with {prefix}",
|
||||
values,
|
||||
p.object,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
values.push(("{written}", p.written));
|
||||
located_issue(
|
||||
"6.6.2.3.3",
|
||||
"Metadata stream {object} writes the namespace {namespace} with the prefix {written}, not {prefix}",
|
||||
values,
|
||||
p.object,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
let used = uses_device_colour(doc, &objs);
|
||||
match pdfa_intent_profile(doc) {
|
||||
None if used.any() => {
|
||||
|
||||
@@ -776,6 +776,324 @@ fn xmp_exif_schemas_have_their_value_types() {
|
||||
assert_eq!(element("aux:SerialNumber", "<rdf:Bag><rdf:li>1</rdf:li></rdf:Bag>"), ["aux:SerialNumber"]);
|
||||
}
|
||||
|
||||
/// The 6.6.2.3.3 findings of a packet whose extension schema bag holds `schema`, with the five
|
||||
/// description namespaces bound to the prefixes in `prefixes` (`pdfaSchema` and so on by default).
|
||||
fn extension(schema: &str, prefixes: [&str; 4]) -> (Vec<String>, Vec<&'static str>) {
|
||||
let found = extension_findings(schema, prefixes);
|
||||
(found.missing.into_iter().map(|m| m.entry).collect(), found.prefixes.into_iter().map(|p| p.own).collect())
|
||||
}
|
||||
|
||||
/// [`extension`]'s findings in full, as if from the metadata stream `3 0 R`.
|
||||
fn extension_findings(schema: &str, prefixes: [&str; 4]) -> xmp_extension::Findings {
|
||||
let [s, p, t, f] = prefixes;
|
||||
let xmp = format!(
|
||||
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:{s}="http://www.aiim.org/pdfa/ns/schema#" xmlns:{p}="http://www.aiim.org/pdfa/ns/property#" "#,
|
||||
r#"xmlns:{t}="http://www.aiim.org/pdfa/ns/type#" xmlns:{f}="http://www.aiim.org/pdfa/ns/field#">"#,
|
||||
r#"<pdfaExtension:schemas><rdf:Bag><rdf:li rdf:parseType="Resource">{schema}</rdf:li></rdf:Bag></pdfaExtension:schemas>"#,
|
||||
r#"</rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
),
|
||||
s = s,
|
||||
p = p,
|
||||
t = t,
|
||||
f = f,
|
||||
schema = schema
|
||||
);
|
||||
let parsed = xmp::parse(&xmp).expect("parses");
|
||||
let mut found = xmp_extension::Findings::default();
|
||||
found.add(ObjRef::new(3, 0), &parsed);
|
||||
found
|
||||
}
|
||||
|
||||
const OWN: [&str; 4] = ["pdfaSchema", "pdfaProperty", "pdfaType", "pdfaField"];
|
||||
|
||||
/// A complete schema description, with `drop` left out of it (`pdfaSchema:schema` and so on).
|
||||
fn schema_without(drop: &str) -> String {
|
||||
let entry = |name: &str, value: &str| if name == drop { String::new() } else { format!("<{name}>{value}</{name}>") };
|
||||
let property = ["pdfaProperty:name", "pdfaProperty:valueType", "pdfaProperty:category", "pdfaProperty:description"]
|
||||
.iter()
|
||||
.map(|n| entry(n, "x"))
|
||||
.collect::<String>();
|
||||
let field = ["pdfaField:name", "pdfaField:valueType", "pdfaField:description"].iter().map(|n| entry(n, "x")).collect::<String>();
|
||||
let field = entry("pdfaType:field", &format!(r#"<rdf:Seq><rdf:li rdf:parseType="Resource">{field}</rdf:li></rdf:Seq>"#));
|
||||
let value_type =
|
||||
["pdfaType:type", "pdfaType:namespaceURI", "pdfaType:prefix", "pdfaType:description"].iter().map(|n| entry(n, "x")).collect::<String>();
|
||||
[
|
||||
entry("pdfaSchema:schema", "Some schema"),
|
||||
entry("pdfaSchema:namespaceURI", "http://example.com/ns/"),
|
||||
entry("pdfaSchema:prefix", "ex"),
|
||||
entry("pdfaSchema:property", &format!(r#"<rdf:Seq><rdf:li rdf:parseType="Resource">{property}</rdf:li></rdf:Seq>"#)),
|
||||
entry("pdfaSchema:valueType", &format!(r#"<rdf:Seq><rdf:li rdf:parseType="Resource">{value_type}{field}</rdf:li></rdf:Seq>"#)),
|
||||
]
|
||||
.concat()
|
||||
}
|
||||
|
||||
/// PDF/A-2 6.6.2.3.3: every extension schema description has its required entries, in each of its
|
||||
/// four kinds, and writes each description namespace with its own prefix.
|
||||
#[test]
|
||||
fn xmp_extension_schema_descriptions_are_complete() {
|
||||
assert_eq!(extension(&schema_without(""), OWN), (vec![], vec![]));
|
||||
// The property and value type lists of a schema are optional.
|
||||
assert_eq!(extension(&schema_without("pdfaSchema:property"), OWN), (vec![], vec![]));
|
||||
assert_eq!(extension(&schema_without("pdfaSchema:valueType"), OWN), (vec![], vec![]));
|
||||
// Every other entry is required.
|
||||
for required in [
|
||||
"pdfaSchema:schema",
|
||||
"pdfaSchema:namespaceURI",
|
||||
"pdfaSchema:prefix",
|
||||
"pdfaProperty:name",
|
||||
"pdfaProperty:valueType",
|
||||
"pdfaProperty:category",
|
||||
"pdfaProperty:description",
|
||||
"pdfaType:type",
|
||||
"pdfaType:namespaceURI",
|
||||
"pdfaType:prefix",
|
||||
"pdfaType:description",
|
||||
"pdfaType:field",
|
||||
"pdfaField:name",
|
||||
"pdfaField:valueType",
|
||||
"pdfaField:description",
|
||||
] {
|
||||
assert_eq!(extension(&schema_without(required), OWN), (vec![required.to_string()], vec![]), "{required}");
|
||||
}
|
||||
// An entry may be an attribute of an rdf:Description item.
|
||||
let as_attribute = r#"</rdf:li><rdf:li><rdf:Description pdfaSchema:schema="S" pdfaSchema:namespaceURI="http://e/" pdfaSchema:prefix="e"/>"#;
|
||||
assert_eq!(extension(&format!("{}{as_attribute}", schema_without("")), OWN), (vec![], vec![]));
|
||||
let missing_attribute = r#"</rdf:li><rdf:li><rdf:Description pdfaSchema:schema="S" pdfaSchema:prefix="e"/>"#;
|
||||
assert_eq!(extension(&format!("{}{missing_attribute}", schema_without("")), OWN).0, ["pdfaSchema:namespaceURI"]);
|
||||
// Each namespace with its own prefix: the right URI under another prefix breaks the rule.
|
||||
for (i, own) in OWN.iter().enumerate() {
|
||||
let mut prefixes = OWN;
|
||||
prefixes[i] = "other";
|
||||
let schema = schema_without("").replace(&format!("{own}:"), "other:");
|
||||
assert_eq!(extension(&schema, prefixes), (vec![], vec![*own]), "{own}");
|
||||
}
|
||||
// A second property list is read as well as the first.
|
||||
let complete = schema_without("");
|
||||
let second = complete.replacen(
|
||||
"</pdfaSchema:property>",
|
||||
r#"</pdfaSchema:property><pdfaSchema:property><rdf:Seq><rdf:li rdf:parseType="Resource"><pdfaProperty:name>p</pdfaProperty:name></rdf:li></rdf:Seq></pdfaSchema:property>"#,
|
||||
1,
|
||||
);
|
||||
assert_eq!(extension(&second, OWN).0, ["pdfaProperty:valueType", "pdfaProperty:category", "pdfaProperty:description"]);
|
||||
// The container is an rdf:Bag (TechNote 0009, 4.2).
|
||||
let container = |inner: &str| {
|
||||
let xmp = format!(
|
||||
r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"><pdfaExtension:schemas>{inner}</pdfaExtension:schemas></rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
);
|
||||
let mut found = xmp_extension::Findings::default();
|
||||
found.add(ObjRef::new(3, 0), &xmp::parse(&xmp).expect("parses"));
|
||||
found.not_bag.into_iter().map(|c| c.found).collect::<Vec<_>>()
|
||||
};
|
||||
let schema = r#"<rdf:li rdf:parseType="Resource"><pdfaSchema:schema>S</pdfaSchema:schema><pdfaSchema:namespaceURI>http://e/</pdfaSchema:namespaceURI><pdfaSchema:prefix>e</pdfaSchema:prefix></rdf:li>"#;
|
||||
assert!(container(&format!("<rdf:Bag>{schema}</rdf:Bag>")).is_empty());
|
||||
// What it holds instead is named as the packet writes it.
|
||||
assert_eq!(container(&format!("<rdf:Seq>{schema}</rdf:Seq>")), ["rdf:Seq"]);
|
||||
assert_eq!(container(r#"<rdf:Description pdfaSchema:schema="S"/>"#), ["rdf:Description"]);
|
||||
assert_eq!(container(&format!("<rdf:Bag>{schema}</rdf:Bag><rdf:Bag/>")), ["rdf:Bag, rdf:Bag"]);
|
||||
assert_eq!(container(""), [""]);
|
||||
// The container itself is written as pdfaExtension too (TechNote 0009, 4.1).
|
||||
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:ext="http://www.aiim.org/pdfa/ns/extension/" "#,
|
||||
r#"xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"><ext:schemas><rdf:Bag><rdf:li rdf:parseType="Resource">"#,
|
||||
r#"<pdfaSchema:schema>S</pdfaSchema:schema><pdfaSchema:namespaceURI>http://e/</pdfaSchema:namespaceURI>"#,
|
||||
r#"<pdfaSchema:prefix>e</pdfaSchema:prefix></rdf:li></rdf:Bag></ext:schemas></rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
);
|
||||
let mut found = xmp_extension::Findings::default();
|
||||
found.add(ObjRef::new(3, 0), &xmp::parse(xmp).expect("parses"));
|
||||
assert!(found.missing.is_empty());
|
||||
let written: Vec<(&str, &str, &str)> = found.prefixes.iter().map(|p| (p.namespace, p.own, p.written.as_str())).collect();
|
||||
assert_eq!(written, [("http://www.aiim.org/pdfa/ns/extension/", "pdfaExtension", "ext")]);
|
||||
// Through verify, under 6.6.2.3.3 and not fixable.
|
||||
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#"><pdfaExtension:schemas><rdf:Bag><rdf:li rdf:parseType="Resource">"#,
|
||||
r#"<pdfaSchema:schema>S</pdfaSchema:schema><pdfaSchema:prefix>e</pdfaSchema:prefix></rdf:li></rdf:Bag></pdfaExtension:schemas>"#,
|
||||
r#"</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 issues: Vec<Issue> = verify(&doc, Level::A2b).into_iter().filter(|i| i.clause == "6.6.2.3.3").collect();
|
||||
assert_eq!(issues.len(), 1);
|
||||
assert_eq!(issues[0].message, "The XMP extension schema #1 in metadata stream 3 0 R has no pdfaSchema:namespaceURI entry");
|
||||
assert_eq!(issues[0].object, Some(ObjRef::new(3, 0)));
|
||||
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]
|
||||
fn xmp_extension_findings_say_where_they_are() {
|
||||
// Two schemas lacking the same entries are two findings, each with its schema.
|
||||
let schema = |uri: &str, property: &str| {
|
||||
format!(
|
||||
concat!(
|
||||
r#"<pdfaSchema:schema>S</pdfaSchema:schema><pdfaSchema:namespaceURI>{uri}</pdfaSchema:namespaceURI>"#,
|
||||
r#"<pdfaSchema:prefix>e</pdfaSchema:prefix><pdfaSchema:property><rdf:Seq><rdf:li rdf:parseType="Resource">{property}"#,
|
||||
r#"</rdf:li></rdf:Seq></pdfaSchema:property>"#
|
||||
),
|
||||
uri = uri,
|
||||
property = property
|
||||
)
|
||||
};
|
||||
let named = "<pdfaProperty:name>p</pdfaProperty:name><pdfaProperty:valueType>Text</pdfaProperty:valueType>";
|
||||
let two = format!(r#"{}</rdf:li><rdf:li rdf:parseType="Resource">{}"#, schema("http://a.example/", named), schema("http://b.example/", ""));
|
||||
let found = extension_findings(&two, OWN);
|
||||
let said: Vec<(&str, Option<&str>, &str)> = found.missing.iter().map(|m| (m.schema.as_str(), m.item.as_deref(), m.entry.as_str())).collect();
|
||||
assert_eq!(
|
||||
said,
|
||||
[
|
||||
("http://a.example/", Some("p"), "pdfaProperty:category"),
|
||||
("http://a.example/", Some("p"), "pdfaProperty:description"),
|
||||
("http://b.example/", Some("#1"), "pdfaProperty:name"),
|
||||
("http://b.example/", Some("#1"), "pdfaProperty:valueType"),
|
||||
("http://b.example/", Some("#1"), "pdfaProperty:category"),
|
||||
("http://b.example/", Some("#1"), "pdfaProperty:description"),
|
||||
]
|
||||
);
|
||||
assert!(found.missing.iter().all(|m| m.object == ObjRef::new(3, 0)));
|
||||
// A field is named by its value type and its own name.
|
||||
let field = schema_without("pdfaField:description")
|
||||
.replace(">x</pdfaType:type>", ">Dim</pdfaType:type>")
|
||||
.replace(">x</pdfaField:name>", ">w</pdfaField:name>");
|
||||
let found = extension_findings(&field, OWN);
|
||||
assert_eq!(found.missing.iter().map(|m| m.item.as_deref()).collect::<Vec<_>>(), [Some("Dim/w")]);
|
||||
// A name the packet gives is repeated only so far.
|
||||
let long = format!("http://example.com/{}", "a".repeat(400));
|
||||
let found = extension_findings(&schema(&long, ""), OWN);
|
||||
assert!(found.missing[0].schema.ends_with('…') && found.missing[0].schema.chars().count() == 201, "{}", found.missing[0].schema);
|
||||
// So many findings are enough.
|
||||
let many = vec![schema("http://m.example/", ""); 400].join(r#"</rdf:li><rdf:li rdf:parseType="Resource">"#);
|
||||
assert_eq!(extension_findings(&many, OWN).missing.len(), 1000);
|
||||
|
||||
// Through verify: each wording, with the stream it is in.
|
||||
let verify_packet = |description: &str| -> Vec<String> {
|
||||
let xmp = format!(
|
||||
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="" {}</rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
),
|
||||
description
|
||||
);
|
||||
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], "");
|
||||
verify(&doc, Level::A2b).into_iter().filter(|i| i.clause == "6.6.2.3.3").map(|i| i.message).collect()
|
||||
};
|
||||
let ns = r#"xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#" xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#""#;
|
||||
assert_eq!(
|
||||
verify_packet(&format!(
|
||||
r#"{ns}><pdfaExtension:schemas><rdf:Bag><rdf:li rdf:parseType="Resource">{}</rdf:li></rdf:Bag></pdfaExtension:schemas>"#,
|
||||
schema("http://a.example/", named)
|
||||
)),
|
||||
[
|
||||
"The XMP extension schema http://a.example/ in metadata stream 3 0 R describes p without a pdfaProperty:category entry",
|
||||
"The XMP extension schema http://a.example/ in metadata stream 3 0 R describes p without a pdfaProperty:description entry",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
verify_packet(&format!(
|
||||
r#"{ns}><pdfaExtension:schemas><rdf:Seq><rdf:li rdf:parseType="Resource">{}</rdf:li></rdf:Seq></pdfaExtension:schemas>"#,
|
||||
schema(
|
||||
"http://a.example/",
|
||||
&format!("{named}<pdfaProperty:category>external</pdfaProperty:category><pdfaProperty:description>d</pdfaProperty:description>")
|
||||
)
|
||||
)),
|
||||
["The XMP extension schema container in metadata stream 3 0 R holds rdf:Seq, not one rdf:Bag"]
|
||||
);
|
||||
assert_eq!(
|
||||
verify_packet(&format!(r#"{ns}><pdfaExtension:schemas/>"#)),
|
||||
["The XMP extension schema container in metadata stream 3 0 R holds no rdf:Bag"]
|
||||
);
|
||||
let other = r#"xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" xmlns:s="http://www.aiim.org/pdfa/ns/schema#""#;
|
||||
assert_eq!(
|
||||
verify_packet(&format!(
|
||||
r#"{other}><pdfaExtension:schemas><rdf:Bag><rdf:li rdf:parseType="Resource"><s:schema>S</s:schema><s:namespaceURI>http://a.example/</s:namespaceURI><s:prefix>e</s:prefix></rdf:li></rdf:Bag></pdfaExtension:schemas>"#
|
||||
)),
|
||||
["Metadata stream 3 0 R writes the namespace http://www.aiim.org/pdfa/ns/schema# with the prefix s, not pdfaSchema"]
|
||||
);
|
||||
let default = r#"xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/""#;
|
||||
assert_eq!(
|
||||
verify_packet(&format!(
|
||||
r#"{default}><pdfaExtension:schemas><rdf:Bag><rdf:li rdf:parseType="Resource"><schema xmlns="http://www.aiim.org/pdfa/ns/schema#">S</schema><pdfaSchema:namespaceURI xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#">http://a.example/</pdfaSchema:namespaceURI><pdfaSchema:prefix xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#">e</pdfaSchema:prefix></rdf:li></rdf:Bag></pdfaExtension:schemas>"#
|
||||
)),
|
||||
["Metadata stream 3 0 R writes the namespace http://www.aiim.org/pdfa/ns/schema# without a prefix, not with pdfaSchema"]
|
||||
);
|
||||
}
|
||||
|
||||
/// PDF/A-2 6.6.2.3.1 for the TIFF, EXIF and Additional EXIF schemas: a property is known by its
|
||||
/// namespace URI, whatever prefix the packet binds to it, and is named by the namespace's
|
||||
/// preferred prefix.
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
//! PDF/A-2 6.6.2.3.3, the extension schema container schema: a packet describes each schema it
|
||||
//! extends XMP with in `pdfaExtension:schemas`, a bag of schema descriptions, and each description
|
||||
//! is complete.
|
||||
//!
|
||||
//! The schemas, their entries and their prefixes are those of PDF Association TechNote 0009, "XMP
|
||||
//! Extension Schemas in PDF/A-1" (2008), section 4, for PDF/A-2 6.6.2.3.3:
|
||||
//! - a schema (`pdfaSchema`, 4.3) has `schema`, `namespaceURI` and `prefix`; its `property` and
|
||||
//! `valueType` lists are optional. TechNote 0009, written for PDF/A-1, calls `schema` an
|
||||
//! optional description; PDF/A-2 requires it, as its conformance tests show;
|
||||
//! - a property (`pdfaProperty`, 4.4) has `name`, `valueType`, `category` and `description`;
|
||||
//! - a value type (`pdfaType`, 4.5) has `type`, `namespaceURI`, `prefix`, `description` and
|
||||
//! `field`. TechNote 0009 calls `field` optional; PDF/A-2 requires it, as its conformance tests
|
||||
//! show;
|
||||
//! - a field (`pdfaField`, 4.6) has `name`, `valueType` and `description`;
|
||||
//! - each of the five namespaces is written with its own prefix (`pdfaExtension`, `pdfaSchema`,
|
||||
//! `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 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};
|
||||
|
||||
const RDF: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
|
||||
const EXTENSION: &str = "http://www.aiim.org/pdfa/ns/extension/";
|
||||
const SCHEMA: &str = "http://www.aiim.org/pdfa/ns/schema#";
|
||||
const PROPERTY: &str = "http://www.aiim.org/pdfa/ns/property#";
|
||||
const TYPE: &str = "http://www.aiim.org/pdfa/ns/type#";
|
||||
const FIELD: &str = "http://www.aiim.org/pdfa/ns/field#";
|
||||
|
||||
/// Each container namespace with its required prefix (TechNote 0009, 4.1).
|
||||
const PREFIXES: [(&str, &str); 5] =
|
||||
[(EXTENSION, "pdfaExtension"), (SCHEMA, "pdfaSchema"), (PROPERTY, "pdfaProperty"), (TYPE, "pdfaType"), (FIELD, "pdfaField")];
|
||||
|
||||
/// How many findings a document's packets are read for. A document past this many has plainly
|
||||
/// failed the clause, and listing every one of a hostile packet's descriptions helps nobody.
|
||||
const MAX_FINDINGS: usize = 1000;
|
||||
/// How much of a name the packet gives (a namespace URI, a property) a finding repeats.
|
||||
const MAX_SHOWN_CHARS: usize = 200;
|
||||
|
||||
/// What the extension schema descriptions of a document break of 6.6.2.3.3, in the order the
|
||||
/// metadata streams and their descriptions come.
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct Findings {
|
||||
/// Entries descriptions lack.
|
||||
pub(crate) missing: Vec<Missing>,
|
||||
/// Description namespaces written with another prefix, each once per stream and prefix.
|
||||
pub(crate) prefixes: Vec<Prefix>,
|
||||
/// `pdfaExtension:schemas` containers that aren't one `rdf:Bag` (TechNote 0009, 4.2: "bag
|
||||
/// Schema").
|
||||
pub(crate) not_bag: Vec<NotBag>,
|
||||
}
|
||||
|
||||
/// A description without an entry it needs, or with the entry empty.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Missing {
|
||||
/// The metadata stream.
|
||||
pub(crate) object: ObjRef,
|
||||
/// The schema the description is in: its `pdfaSchema:namespaceURI`, or `#n` for the n-th
|
||||
/// schema of its container when it has none.
|
||||
pub(crate) schema: String,
|
||||
/// The property, value type or field description, when it is one of those and not the schema
|
||||
/// 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 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.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Prefix {
|
||||
pub(crate) object: ObjRef,
|
||||
pub(crate) namespace: &'static str,
|
||||
/// The prefix it needs.
|
||||
pub(crate) own: &'static str,
|
||||
/// The prefix it is written with, empty when it is written as the default namespace.
|
||||
pub(crate) written: String,
|
||||
}
|
||||
|
||||
/// A `pdfaExtension:schemas` container that isn't one `rdf:Bag`.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct NotBag {
|
||||
pub(crate) object: ObjRef,
|
||||
/// The elements it holds instead, by their names as the packet writes them, comma-separated;
|
||||
/// empty when it holds none.
|
||||
pub(crate) found: String,
|
||||
}
|
||||
|
||||
/// Where a description is: its stream, its schema and, below the schema, its own name.
|
||||
struct Place {
|
||||
object: ObjRef,
|
||||
schema: String,
|
||||
item: Option<String>,
|
||||
}
|
||||
|
||||
impl Findings {
|
||||
/// Adds what the packet `doc`, from the metadata stream `object`, breaks.
|
||||
pub(crate) fn add(&mut self, object: ObjRef, doc: &Document<'_>) {
|
||||
let containers = doc.descendants().filter(|n| is(n, EXTENSION, "schemas"));
|
||||
for container in containers {
|
||||
self.prefixes_in(object, doc, &container);
|
||||
let children: Vec<Node<'_, '_>> = container.children().filter(Node::is_element).collect();
|
||||
if !matches!(children.as_slice(), [bag] if is(bag, RDF, "Bag")) && self.room() {
|
||||
let found = children.iter().take(MAX_SHOWN_CHARS).map(|c| written_name(doc, c)).collect::<Vec<_>>().join(", ");
|
||||
self.not_bag.push(NotBag { object, found: shown(&found) });
|
||||
}
|
||||
for (n, schema) in items(&container).iter().enumerate() {
|
||||
let id = value(schema, SCHEMA, "namespaceURI").unwrap_or_else(|| position(n));
|
||||
let at = |item: Option<String>| Place { object, schema: id.clone(), item };
|
||||
self.need(&at(None), schema, SCHEMA, "pdfaSchema", &["schema", "namespaceURI", "prefix"]);
|
||||
let properties = entries(schema, SCHEMA, "property").flat_map(|e| items(&e));
|
||||
for (n, property) in properties.enumerate() {
|
||||
let name = value(&property, PROPERTY, "name").unwrap_or_else(|| position(n));
|
||||
self.need(&at(Some(name)), &property, PROPERTY, "pdfaProperty", &["name", "valueType", "category", "description"]);
|
||||
}
|
||||
let value_types = entries(schema, SCHEMA, "valueType").flat_map(|e| items(&e));
|
||||
for (n, value_type) in value_types.enumerate() {
|
||||
let name = value(&value_type, TYPE, "type").unwrap_or_else(|| position(n));
|
||||
self.need(&at(Some(name.clone())), &value_type, TYPE, "pdfaType", &["type", "namespaceURI", "prefix", "description", "field"]);
|
||||
for (n, field) in entries(&value_type, TYPE, "field").flat_map(|e| items(&e)).enumerate() {
|
||||
let field_name = value(&field, FIELD, "name").unwrap_or_else(|| position(n));
|
||||
self.need(&at(Some(format!("{name}/{field_name}"))), &field, FIELD, "pdfaField", &["name", "valueType", "description"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether another finding may be recorded (see [`MAX_FINDINGS`]).
|
||||
fn room(&self) -> bool {
|
||||
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, 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 {
|
||||
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;
|
||||
let entry = format!("{prefix}:{name}");
|
||||
self.missing.push(Missing { object: *object, schema: schema.clone(), item: item.clone(), entry, empty });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notes container namespaces written in `container` (itself included) with another prefix
|
||||
/// than their own.
|
||||
fn prefixes_in(&mut self, object: ObjRef, doc: &Document<'_>, container: &Node<'_, '_>) {
|
||||
let text = doc.input_text();
|
||||
for n in container.descendants().filter(Node::is_element) {
|
||||
let element = (n.tag_name().namespace(), text.get(n.range()).unwrap_or_default().strip_prefix('<').unwrap_or_default());
|
||||
let attributes = n.attributes().map(|a| (a.namespace(), text.get(a.range_qname()).unwrap_or_default()));
|
||||
for (ns, qname) in std::iter::once(element).chain(attributes) {
|
||||
let Some(&(namespace, own)) = PREFIXES.iter().find(|(uri, _)| Some(*uri) == ns) else { continue };
|
||||
let written = shown(written_prefix(qname));
|
||||
if written != own
|
||||
&& self.room()
|
||||
&& !self.prefixes.iter().any(|p| p.object == object && p.namespace == namespace && p.written == written)
|
||||
{
|
||||
self.prefixes.push(Prefix { object, namespace, own, written });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// At most [`MAX_SHOWN_CHARS`] of `s`, cut at a char boundary, with `…` when cut.
|
||||
fn shown(s: &str) -> String {
|
||||
let mut out: String = s.chars().take(MAX_SHOWN_CHARS).collect();
|
||||
if out.len() < s.len() {
|
||||
out.push('…');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The text of the entry `name` of `ns` in the description `d`, written as an element or an
|
||||
/// attribute, trimmed and [`shown`]; `None` when it is absent or blank.
|
||||
fn value(d: &Node<'_, '_>, ns: &str, name: &str) -> Option<String> {
|
||||
let text = entry(d, ns, name).and_then(|e| e.text()).or_else(|| d.attribute((ns, name)))?.trim();
|
||||
(!text.is_empty()).then(|| shown(text))
|
||||
}
|
||||
|
||||
/// The name of the element `n` as the packet writes it, prefix included.
|
||||
fn written_name<'i>(doc: &Document<'i>, n: &Node<'_, '_>) -> &'i str {
|
||||
qualified_name(doc.input_text().get(n.range()).unwrap_or_default().strip_prefix('<').unwrap_or_default())
|
||||
}
|
||||
|
||||
/// The qualified name the text of a start tag (after its `<`) or of an attribute begins with.
|
||||
fn qualified_name(text: &str) -> &str {
|
||||
text.split(|c: char| c.is_whitespace() || c == '>' || c == '/' || c == '=').next().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The prefix of the qualified name `text` begins with, empty when it has none.
|
||||
fn written_prefix(text: &str) -> &str {
|
||||
qualified_name(text).split_once(':').map_or("", |(p, _)| p)
|
||||
}
|
||||
|
||||
fn is(n: &Node<'_, '_>, ns: &str, name: &str) -> bool {
|
||||
n.is_element() && n.tag_name().namespace() == Some(ns) && n.tag_name().name() == name
|
||||
}
|
||||
|
||||
/// Every element entry `name` of `ns` in the description `d`: a list written twice is read twice.
|
||||
fn entries<'a, 'i>(d: &Node<'a, 'i>, ns: &'a str, name: &'a str) -> impl Iterator<Item = Node<'a, 'i>> {
|
||||
d.children().filter(move |c| is(c, ns, name))
|
||||
}
|
||||
|
||||
/// The element entry `name` of `ns` in the description `d`.
|
||||
fn entry<'a, 'i>(d: &Node<'a, 'i>, ns: &str, name: &str) -> Option<Node<'a, 'i>> {
|
||||
d.children().find(|c| is(c, ns, name))
|
||||
}
|
||||
|
||||
/// The descriptions in the array `e` holds: each item's `rdf:parseType="Resource"` node or
|
||||
/// `rdf:Description`.
|
||||
fn items<'a, 'i>(e: &Node<'a, 'i>) -> Vec<Node<'a, 'i>> {
|
||||
let Some(array) = e.children().find(|c| is(c, RDF, "Bag") || is(c, RDF, "Seq") || is(c, RDF, "Alt")) else { return Vec::new() };
|
||||
array.children().filter(|i| is(i, RDF, "li")).map(|li| li.children().find(|c| is(c, RDF, "Description")).unwrap_or(li)).collect()
|
||||
}
|
||||
@@ -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_mistyped_properties_name_their_type", "crates/preflight/src/tests.rs::xmp_properties_are_known_by_namespace_uri_and_named_as_written", "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/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), 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"]
|
||||
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