mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
Merge branch 'fix/pdfa-xmp-value-types' into fix/pdfa-xmp-exif-types
# Conflicts: # crates/preflight/src/lib.rs # crates/preflight/src/tests.rs # crates/preflight/src/xmp_types.rs # parity/acrobat-features.toml
This commit is contained in:
commit
19f4e1ef21
4 files changed
+218
-73
No files matched your search
+26
-10
@@ -151,7 +151,8 @@ pub const WORDINGS: &[&str] = &[
|
||||
"The document has no XMP metadata",
|
||||
"The metadata doesn't identify the file as {}",
|
||||
"The title in the metadata and the document information differ",
|
||||
"The XMP property {property} in metadata stream {object} doesn't have the value type the XMP Specification gives it",
|
||||
"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 document as a whole.
|
||||
"The document is encrypted",
|
||||
"Device colour is used, but there is no PDF/A output intent",
|
||||
@@ -509,15 +510,30 @@ 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.
|
||||
for (object, name) in xmp_types::mistyped_in(&objs) {
|
||||
let values = vec![("{property}", name), ("{object}", object.to_string())];
|
||||
located_issue(
|
||||
"6.6.2.3.1",
|
||||
"The XMP property {property} in metadata stream {object} doesn't have the value type the XMP Specification gives it",
|
||||
values,
|
||||
object,
|
||||
false,
|
||||
);
|
||||
let mut mistyped = Vec::new();
|
||||
xmp_types::for_each_packet(&objs, |object, packet| {
|
||||
mistyped.extend(xmp_types::mistyped(packet).into_iter().map(|((name, written), ty)| (object, name, written, ty)));
|
||||
});
|
||||
for (object, name, written, ty) in mistyped {
|
||||
let mut values = vec![("{property}", name.clone()), ("{object}", object.to_string()), ("{type}", ty)];
|
||||
if written == name {
|
||||
located_issue(
|
||||
"6.6.2.3.1",
|
||||
"The XMP property {property} in metadata stream {object} should have the value type {type}",
|
||||
values,
|
||||
object,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
values.push(("{written}", written));
|
||||
located_issue(
|
||||
"6.6.2.3.1",
|
||||
"The XMP property {property} in metadata stream {object}, written {written}, should have the value type {type}",
|
||||
values,
|
||||
object,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
let used = uses_device_colour(doc, &objs);
|
||||
match pdfa_intent_profile(doc) {
|
||||
|
||||
@@ -507,7 +507,7 @@ fn mistyped(body: &str) -> Vec<String> {
|
||||
r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF {XMP_NS}><rdf:Description rdf:about="" {body}</rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
);
|
||||
let parsed = xmp::parse(&xmp).expect("parses");
|
||||
xmp_types::mistyped(&parsed).into_iter().collect()
|
||||
xmp_types::mistyped(&parsed).into_keys().map(|(name, _)| name).collect()
|
||||
}
|
||||
|
||||
/// [`mistyped`] for the property `p` written as an element holding `v`.
|
||||
@@ -680,9 +680,9 @@ fn xmp_value_types_are_reported_by_verify() {
|
||||
assert_eq!(
|
||||
messages,
|
||||
[
|
||||
"The XMP property crs:Contrast in metadata stream 3 0 R doesn't have the value type the XMP Specification gives it",
|
||||
"The XMP property crs:Exposure in metadata stream 3 0 R doesn't have the value type the XMP Specification gives it",
|
||||
"The XMP property crs:Contrast in metadata stream 5 0 R doesn't have the value type the XMP Specification gives it"
|
||||
"The XMP property crs:Contrast in metadata stream 3 0 R should have the value type Integer",
|
||||
"The XMP property crs:Exposure in metadata stream 3 0 R should have the value type Real",
|
||||
"The XMP property crs:Contrast in metadata stream 5 0 R should have the value type Integer"
|
||||
]
|
||||
);
|
||||
let objects: Vec<Option<ObjRef>> = found.iter().map(|i| i.object).collect();
|
||||
@@ -692,7 +692,7 @@ fn xmp_value_types_are_reported_by_verify() {
|
||||
// what the document wrote goes in as it stands.
|
||||
let reordered = |w: &str| w.replace("The XMP property {property} in metadata stream {object}", "In {object}, {property}");
|
||||
assert_eq!(found[0].said(&in_english), found[0].message);
|
||||
assert!(found[0].said(&reordered).starts_with("In 3 0 R, crs:Contrast doesn't"), "{}", found[0].said(&reordered));
|
||||
assert_eq!(found[0].said(&reordered), "In 3 0 R, crs:Contrast should have the value type Integer");
|
||||
// A UTF-16 packet, either byte order, is read.
|
||||
for big in [true, false] {
|
||||
let xmp = format!(
|
||||
@@ -808,6 +808,90 @@ fn wordings_fill_their_holes_once() {
|
||||
assert_eq!(fill("é{}ü", Some("v"), &[]), "évü");
|
||||
}
|
||||
|
||||
/// PDF/A-2 6.6.2.3.1: a mistyped property is reported with the type the XMP Specification gives it,
|
||||
/// as the specification writes it.
|
||||
#[test]
|
||||
fn xmp_mistyped_properties_name_their_type() {
|
||||
let types = |body: &str| -> Vec<(String, String)> {
|
||||
let xmp = format!(
|
||||
r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF {XMP_NS}><rdf:Description rdf:about="" {body}</rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
);
|
||||
xmp_types::mistyped(&xmp::parse(&xmp).expect("parses")).into_iter().map(|((name, _), ty)| (name, ty)).collect()
|
||||
};
|
||||
let t = |name: &str, ty: &str| (name.to_owned(), ty.to_owned());
|
||||
assert_eq!(
|
||||
types(r#"crs:Contrast="high" crs:Exposure="bright" xmp:CreateDate="never">"#),
|
||||
[t("crs:Contrast", "Integer"), t("crs:Exposure", "Real"), t("xmp:CreateDate", "Date")]
|
||||
);
|
||||
assert_eq!(
|
||||
types("><xmpTPg:PlateNames>Cyan</xmpTPg:PlateNames><dc:title>T</dc:title>"),
|
||||
[t("dc:title", "Lang Alt"), t("xmpTPg:PlateNames", "seq Text")]
|
||||
);
|
||||
assert_eq!(
|
||||
types("><xmpTPg:MaxPageSize>A4</xmpTPg:MaxPageSize><xmpMM:History>h</xmpMM:History>"),
|
||||
[t("xmpMM:History", "seq ResourceEvent"), t("xmpTPg:MaxPageSize", "Dimensions")]
|
||||
);
|
||||
assert_eq!(types("><xmpMM:Pantry>p</xmpMM:Pantry>"), [t("xmpMM:Pantry", "bag struct")]);
|
||||
// The TIFF and EXIF types, as the 2005 edition writes them.
|
||||
assert_eq!(types(r#"tiff:XResolution="0.5" exif:GPSLatitude="N">"#), [t("exif:GPSLatitude", "GPSCoordinate"), t("tiff:XResolution", "Rational")]);
|
||||
assert_eq!(
|
||||
types("><tiff:BitsPerSample>8</tiff:BitsPerSample><exif:Flash>fired</exif:Flash><exif:OECF>o</exif:OECF>"),
|
||||
[t("exif:Flash", "Flash"), t("exif:OECF", "OECF/SFR"), t("tiff:BitsPerSample", "seq Integer")]
|
||||
);
|
||||
}
|
||||
|
||||
/// PDF/A-2 6.6.2.3.1: a property is known by its namespace URI, whatever prefix the packet binds
|
||||
/// to it, and is named by the namespace's preferred prefix and as the packet writes it. A property
|
||||
/// a predefined namespace doesn't define, and a namespace that isn't predefined, are left to the
|
||||
/// clauses about them.
|
||||
#[test]
|
||||
fn xmp_properties_are_known_by_namespace_uri_and_named_as_written() {
|
||||
let crs = "http://ns.adobe.com/camera-raw-settings/1.0/";
|
||||
let found = |body: &str| -> Vec<(String, String)> {
|
||||
let xmp = format!(
|
||||
r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF {XMP_NS}><rdf:Description rdf:about="" {body}</rdf:Description></rdf:RDF></x:xmpmeta>"#
|
||||
);
|
||||
xmp_types::mistyped(&xmp::parse(&xmp).expect("parses")).into_keys().collect()
|
||||
};
|
||||
let n = |name: &str, written: &str| (name.to_owned(), written.to_owned());
|
||||
// Another prefix for the namespace, as an attribute and as an element, and no prefix at all.
|
||||
assert_eq!(found(&format!(r#"xmlns:c="{crs}" c:Contrast="high">"#)), [n("crs:Contrast", "c:Contrast")]);
|
||||
assert_eq!(found(&format!(r#"><c:Exposure xmlns:c="{crs}">bright</c:Exposure>"#)), [n("crs:Exposure", "c:Exposure")]);
|
||||
assert_eq!(found(&format!(r#"><Exposure xmlns="{crs}">bright</Exposure>"#)), [n("crs:Exposure", "Exposure")]);
|
||||
assert!(found(&format!(r#"xmlns:c="{crs}" c:Contrast="5">"#)).is_empty());
|
||||
// The preferred prefix is written as it is.
|
||||
assert_eq!(found(r#"crs:Contrast="high">"#), [n("crs:Contrast", "crs:Contrast")]);
|
||||
// A long prefix is repeated only so far.
|
||||
let long = "p".repeat(100);
|
||||
let said = found(&format!(r#"xmlns:{long}="{crs}" {long}:Contrast="high">"#));
|
||||
assert_eq!(said.len(), 1);
|
||||
assert_eq!(said[0].1, format!("{}…:Contrast", "p".repeat(64)));
|
||||
// The preferred prefix bound to another namespace is not that namespace.
|
||||
assert!(found(r#"><crs:Contrast xmlns:crs="http://example.com/not-crs/">high</crs:Contrast>"#).is_empty());
|
||||
assert!(found(r#"xmlns:dc="http://example.com/not-dc/" dc:title="T">"#).is_empty());
|
||||
// A property a predefined namespace doesn't define, with any value.
|
||||
assert!(found(r#"crs:NotAProperty="high" dc:notAProperty="T">"#).is_empty());
|
||||
assert!(found("><xmp:NotAProperty><rdf:Seq><rdf:li>x</rdf:li></rdf:Seq></xmp:NotAProperty>").is_empty());
|
||||
// A namespace that isn't predefined, with a predefined property's local name.
|
||||
assert!(found(r#"xmlns:ex="http://example.com/ns/" ex:Contrast="high">"#).is_empty());
|
||||
assert!(found(r#"><ex:title xmlns:ex="http://example.com/ns/">T</ex:title>"#).is_empty());
|
||||
|
||||
// Through verify, the written name is said when it differs.
|
||||
let xmp = format!(
|
||||
r#"<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF {XMP_NS}><rdf:Description rdf:about="" xmlns:c="{crs}" c:Contrast="high" crs:Exposure="bright"/></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.1").map(|i| i.message).collect();
|
||||
assert_eq!(
|
||||
messages,
|
||||
[
|
||||
"The XMP property crs:Contrast in metadata stream 3 0 R, written c:Contrast, should have the value type Integer",
|
||||
"The XMP property crs:Exposure in metadata stream 3 0 R should have the value type Real",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! The other simple types are text. A field a structure doesn't define, and a property a
|
||||
//! namespace doesn't define, are left to the clauses about them.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use pdfcraft_cos::{DecodeBudget, ObjRef, Object};
|
||||
use roxmltree::Node;
|
||||
@@ -54,7 +54,8 @@ enum Ty {
|
||||
Seq(&'static Ty),
|
||||
Bag(&'static Ty),
|
||||
Alt(&'static Ty),
|
||||
Struct(&'static [Field]),
|
||||
/// A structure: the name the specification gives its type, and its fields.
|
||||
Struct(&'static str, &'static [Field]),
|
||||
/// A structure whose fields aren't given (`xmpMM:Pantry`).
|
||||
AnyStruct,
|
||||
/// Any of these: where editions of the specification differ.
|
||||
@@ -148,7 +149,7 @@ const RESOURCE_REF: &[Field] = &[
|
||||
/// Part 2 Table 10.
|
||||
const VERSION: &[Field] = &[
|
||||
(ST_VER, "comments", Text),
|
||||
(ST_VER, "event", Ty::Struct(RESOURCE_EVENT)),
|
||||
(ST_VER, "event", Ty::Struct("ResourceEvent", RESOURCE_EVENT)),
|
||||
(ST_VER, "modifier", Text),
|
||||
(ST_VER, "modifyDate", Date),
|
||||
(ST_VER, "version", Text),
|
||||
@@ -157,16 +158,16 @@ const VERSION: &[Field] = &[
|
||||
const JOB: &[Field] = &[(ST_JOB, "id", Text), (ST_JOB, "name", Text), (ST_JOB, "url", Text)];
|
||||
/// Part 2 Table 12.
|
||||
const BEAT_SPLICE_STRETCH: &[Field] =
|
||||
&[(XMP_DM, "riseInDecibel", Real), (XMP_DM, "riseInTimeDuration", Ty::Struct(TIME)), (XMP_DM, "useFileBeatsMarker", Boolean)];
|
||||
&[(XMP_DM, "riseInDecibel", Real), (XMP_DM, "riseInTimeDuration", Ty::Struct("Time", TIME)), (XMP_DM, "useFileBeatsMarker", Boolean)];
|
||||
/// Part 2 Table 13.
|
||||
const CUE_POINT_PARAM: &[Field] = &[(XMP_DM, "key", Text), (XMP_DM, "value", Text)];
|
||||
/// A marker's start and duration: FrameCount (text) in Part 2 Table 16, a Time structure in the
|
||||
/// September 2005 edition that PDF/A-2 cites. Either is taken.
|
||||
const FRAME_COUNT_OR_TIME: Ty = Ty::OneOf(&[Text, Ty::Struct(TIME)]);
|
||||
const FRAME_COUNT_OR_TIME: Ty = Ty::OneOf(&[Text, Ty::Struct("Time", TIME)]);
|
||||
/// Part 2 Table 16.
|
||||
const MARKER: &[Field] = &[
|
||||
(XMP_DM, "comment", Text),
|
||||
(XMP_DM, "cuePointParams", Ty::Seq(&Ty::Struct(CUE_POINT_PARAM))),
|
||||
(XMP_DM, "cuePointParams", Ty::Seq(&Ty::Struct("CuePointParam", CUE_POINT_PARAM))),
|
||||
(XMP_DM, "cuePointType", Text),
|
||||
(XMP_DM, "duration", FRAME_COUNT_OR_TIME),
|
||||
(XMP_DM, "location", Text),
|
||||
@@ -179,10 +180,10 @@ const MARKER: &[Field] = &[
|
||||
];
|
||||
/// Part 2 Table 17.
|
||||
const MEDIA: &[Field] = &[
|
||||
(XMP_DM, "duration", Ty::Struct(TIME)),
|
||||
(XMP_DM, "duration", Ty::Struct("Time", TIME)),
|
||||
(XMP_DM, "managed", Boolean),
|
||||
(XMP_DM, "path", Text),
|
||||
(XMP_DM, "startTime", Ty::Struct(TIME)),
|
||||
(XMP_DM, "startTime", Ty::Struct("Time", TIME)),
|
||||
(XMP_DM, "track", Text),
|
||||
(XMP_DM, "webStatement", Text),
|
||||
];
|
||||
@@ -197,8 +198,12 @@ const TIMECODE: &[Field] = &[(XMP_DM, "timeFormat", Text), (XMP_DM, "timeValue",
|
||||
/// Part 2 Table 22.
|
||||
const TIME_SCALE_STRETCH: &[Field] = &[(XMP_DM, "frameOverlappingPercentage", Real), (XMP_DM, "frameSize", Real), (XMP_DM, "quality", Text)];
|
||||
/// Part 2 Table 23.
|
||||
const TRACK: &[Field] =
|
||||
&[(XMP_DM, "frameRate", Text), (XMP_DM, "markers", Ty::Seq(&Ty::Struct(MARKER))), (XMP_DM, "trackName", Text), (XMP_DM, "trackType", Text)];
|
||||
const TRACK: &[Field] = &[
|
||||
(XMP_DM, "frameRate", Text),
|
||||
(XMP_DM, "markers", Ty::Seq(&Ty::Struct("Marker", MARKER))),
|
||||
(XMP_DM, "trackName", Text),
|
||||
(XMP_DM, "trackType", Text),
|
||||
];
|
||||
/// Part 2 Table 31.
|
||||
const ANCESTOR: &[Field] = &[(PHOTOSHOP, "AncestorID", Text)];
|
||||
/// Part 2 Table 32.
|
||||
@@ -255,7 +260,7 @@ const SCHEMAS: &[Schema] = &[
|
||||
("ModifyDate", Date),
|
||||
("Nickname", Text),
|
||||
("Rating", Real),
|
||||
("Thumbnails", Ty::Alt(&Ty::Struct(THUMBNAIL))),
|
||||
("Thumbnails", Ty::Alt(&Ty::Struct("Thumbnail", THUMBNAIL))),
|
||||
],
|
||||
},
|
||||
// ISO 16684-1 Table 6.
|
||||
@@ -269,12 +274,12 @@ const SCHEMAS: &[Schema] = &[
|
||||
uri: "http://ns.adobe.com/xap/1.0/mm/",
|
||||
prefix: "xmpMM",
|
||||
properties: &[
|
||||
("DerivedFrom", Ty::Struct(RESOURCE_REF)),
|
||||
("DerivedFrom", Ty::Struct("ResourceRef", RESOURCE_REF)),
|
||||
("DocumentID", Ty::Guid),
|
||||
("History", Ty::Seq(&Ty::Struct(RESOURCE_EVENT))),
|
||||
("Ingredients", Ty::Bag(&Ty::Struct(RESOURCE_REF))),
|
||||
("History", Ty::Seq(&Ty::Struct("ResourceEvent", RESOURCE_EVENT))),
|
||||
("Ingredients", Ty::Bag(&Ty::Struct("ResourceRef", RESOURCE_REF))),
|
||||
("InstanceID", Ty::Guid),
|
||||
("ManagedFrom", Ty::Struct(RESOURCE_REF)),
|
||||
("ManagedFrom", Ty::Struct("ResourceRef", RESOURCE_REF)),
|
||||
("Manager", Text),
|
||||
("ManageTo", Text),
|
||||
("ManageUI", Text),
|
||||
@@ -284,22 +289,22 @@ const SCHEMAS: &[Schema] = &[
|
||||
("RenditionClass", Text),
|
||||
("RenditionParams", Text),
|
||||
("VersionID", Text),
|
||||
("Versions", Ty::Seq(&Ty::Struct(VERSION))),
|
||||
("Versions", Ty::Seq(&Ty::Struct("Version", VERSION))),
|
||||
("LastURL", Text),
|
||||
("RenditionOf", Ty::Struct(RESOURCE_REF)),
|
||||
("RenditionOf", Ty::Struct("ResourceRef", RESOURCE_REF)),
|
||||
("SaveID", Integer),
|
||||
],
|
||||
},
|
||||
// Part 2 Table 27.
|
||||
Schema { uri: "http://ns.adobe.com/xap/1.0/bj/", prefix: "xmpBJ", properties: &[("JobRef", Ty::Bag(&Ty::Struct(JOB)))] },
|
||||
Schema { uri: "http://ns.adobe.com/xap/1.0/bj/", prefix: "xmpBJ", properties: &[("JobRef", Ty::Bag(&Ty::Struct("Job", JOB)))] },
|
||||
// Part 2 Table 28.
|
||||
Schema {
|
||||
uri: "http://ns.adobe.com/xap/1.0/t/pg/",
|
||||
prefix: "xmpTPg",
|
||||
properties: &[
|
||||
("Colorants", Ty::Seq(&Ty::Struct(COLORANT))),
|
||||
("Fonts", Ty::Bag(&Ty::Struct(FONT))),
|
||||
("MaxPageSize", Ty::Struct(DIMENSIONS)),
|
||||
("Colorants", Ty::Seq(&Ty::Struct("Colorant", COLORANT))),
|
||||
("Fonts", Ty::Bag(&Ty::Struct("Font", FONT))),
|
||||
("MaxPageSize", Ty::Struct("Dimensions", DIMENSIONS)),
|
||||
("NPages", Integer),
|
||||
("PlateNames", Ty::Seq(&Text)),
|
||||
],
|
||||
@@ -312,14 +317,14 @@ const SCHEMAS: &[Schema] = &[
|
||||
("absPeakAudioFilePath", Text),
|
||||
("album", Text),
|
||||
("altTapeName", Text),
|
||||
("altTimecode", Ty::Struct(TIMECODE)),
|
||||
("altTimecode", Ty::Struct("Timecode", TIMECODE)),
|
||||
("artist", Text),
|
||||
("audioModDate", Date),
|
||||
("audioChannelType", Text),
|
||||
("audioCompressor", Text),
|
||||
("audioSampleRate", Integer),
|
||||
("audioSampleType", Text),
|
||||
("beatSpliceParams", Ty::Struct(BEAT_SPLICE_STRETCH)),
|
||||
("beatSpliceParams", Ty::Struct("BeatSpliceStretch", BEAT_SPLICE_STRETCH)),
|
||||
("cameraAngle", Text),
|
||||
("cameraLabel", Text),
|
||||
("cameraModel", Text),
|
||||
@@ -327,31 +332,31 @@ const SCHEMAS: &[Schema] = &[
|
||||
("client", Text),
|
||||
("comment", Text),
|
||||
("composer", Text),
|
||||
("contributedMedia", Ty::Bag(&Ty::Struct(MEDIA))),
|
||||
("contributedMedia", Ty::Bag(&Ty::Struct("Media", MEDIA))),
|
||||
("copyright", Text),
|
||||
("director", Text),
|
||||
("directorPhotography", Text),
|
||||
("duration", Ty::Struct(TIME)),
|
||||
("duration", Ty::Struct("Time", TIME)),
|
||||
("engineer", Text),
|
||||
("fileDataRate", Rational),
|
||||
("genre", Text),
|
||||
("good", Boolean),
|
||||
("instrument", Text),
|
||||
("introTime", Ty::Struct(TIME)),
|
||||
("introTime", Ty::Struct("Time", TIME)),
|
||||
("key", Text),
|
||||
("logComment", Text),
|
||||
("loop", Boolean),
|
||||
("numberOfBeats", Real),
|
||||
("markers", Ty::Seq(&Ty::Struct(MARKER))),
|
||||
("markers", Ty::Seq(&Ty::Struct("Marker", MARKER))),
|
||||
("metadataModDate", Date),
|
||||
("outCue", Ty::Struct(TIME)),
|
||||
("outCue", Ty::Struct("Time", TIME)),
|
||||
("projectName", Text),
|
||||
("projectRef", Ty::Struct(PROJECT_LINK)),
|
||||
("projectRef", Ty::Struct("ProjectLink", PROJECT_LINK)),
|
||||
("pullDown", Text),
|
||||
("relativePeakAudioFilePath", Text),
|
||||
("relativeTimestamp", Ty::Struct(TIME)),
|
||||
("relativeTimestamp", Ty::Struct("Time", TIME)),
|
||||
("releaseDate", Date),
|
||||
("resampleParams", Ty::Struct(RESAMPLE_STRETCH)),
|
||||
("resampleParams", Ty::Struct("ResampleStretch", RESAMPLE_STRETCH)),
|
||||
("scaleType", Text),
|
||||
("scene", Text),
|
||||
("shotDate", Date),
|
||||
@@ -361,23 +366,23 @@ const SCHEMAS: &[Schema] = &[
|
||||
("shotNumber", Text),
|
||||
("shotSize", Text),
|
||||
("speakerPlacement", Text),
|
||||
("startTimecode", Ty::Struct(TIMECODE)),
|
||||
("startTimecode", Ty::Struct("Timecode", TIMECODE)),
|
||||
("stretchMode", Text),
|
||||
("takeNumber", Integer),
|
||||
("tapeName", Text),
|
||||
("tempo", Real),
|
||||
("timeScaleParams", Ty::Struct(TIME_SCALE_STRETCH)),
|
||||
("timeScaleParams", Ty::Struct("TimeScaleStretch", TIME_SCALE_STRETCH)),
|
||||
("timeSignature", Text),
|
||||
("trackNumber", Integer),
|
||||
("Tracks", Ty::Bag(&Ty::Struct(TRACK))),
|
||||
("Tracks", Ty::Bag(&Ty::Struct("Track", TRACK))),
|
||||
("videoAlphaMode", Text),
|
||||
("videoAlphaPremultipleColor", Ty::Struct(COLORANT)),
|
||||
("videoAlphaPremultipleColor", Ty::Struct("Colorant", COLORANT)),
|
||||
("videoAlphaUnityIsTransparent", Boolean),
|
||||
("videoColorSpace", Text),
|
||||
("videoCompressor", Text),
|
||||
("videoFieldOrder", Text),
|
||||
("videoFrameRate", Text),
|
||||
("videoFrameSize", Ty::Struct(DIMENSIONS)),
|
||||
("videoFrameSize", Ty::Struct("Dimensions", DIMENSIONS)),
|
||||
("videoModDate", Date),
|
||||
("videoPixelDepth", Text),
|
||||
("videoPixelAspectRatio", Rational),
|
||||
@@ -405,7 +410,7 @@ const SCHEMAS: &[Schema] = &[
|
||||
("Country", Text),
|
||||
("Credit", Text),
|
||||
("DateCreated", Date),
|
||||
("DocumentAncestors", Ty::Bag(&Ty::Struct(ANCESTOR))),
|
||||
("DocumentAncestors", Ty::Bag(&Ty::Struct("Ancestor", ANCESTOR))),
|
||||
("Headline", Text),
|
||||
("History", Text),
|
||||
("ICCProfile", Text),
|
||||
@@ -413,7 +418,7 @@ const SCHEMAS: &[Schema] = &[
|
||||
("Source", Text),
|
||||
("State", Text),
|
||||
("SupplementalCategories", Ty::Bag(&Text)),
|
||||
("TextLayers", Ty::Seq(&Ty::Struct(LAYER))),
|
||||
("TextLayers", Ty::Seq(&Ty::Struct("Layer", LAYER))),
|
||||
("TransmissionReference", Text),
|
||||
("Urgency", Integer),
|
||||
],
|
||||
@@ -520,7 +525,7 @@ const SCHEMAS: &[Schema] = &[
|
||||
("ExposureProgram", Integer),
|
||||
("SpectralSensitivity", Text),
|
||||
("ISOSpeedRatings", Ty::Seq(&Integer)),
|
||||
("OECF", Ty::Struct(OECF_SFR)),
|
||||
("OECF", Ty::Struct("OECF/SFR", OECF_SFR)),
|
||||
("ShutterSpeedValue", Rational),
|
||||
("ApertureValue", Rational),
|
||||
("BrightnessValue", Rational),
|
||||
@@ -529,11 +534,11 @@ const SCHEMAS: &[Schema] = &[
|
||||
("SubjectDistance", Rational),
|
||||
("MeteringMode", Integer),
|
||||
("LightSource", Integer),
|
||||
("Flash", Ty::Struct(FLASH)),
|
||||
("Flash", Ty::Struct("Flash", FLASH)),
|
||||
("FocalLength", Rational),
|
||||
("SubjectArea", Ty::Seq(&Integer)),
|
||||
("FlashEnergy", Rational),
|
||||
("SpatialFrequencyResponse", Ty::Struct(OECF_SFR)),
|
||||
("SpatialFrequencyResponse", Ty::Struct("OECF/SFR", OECF_SFR)),
|
||||
("FocalPlaneXResolution", Rational),
|
||||
("FocalPlaneYResolution", Rational),
|
||||
("FocalPlaneResolutionUnit", Integer),
|
||||
@@ -542,7 +547,7 @@ const SCHEMAS: &[Schema] = &[
|
||||
("SensingMethod", Integer),
|
||||
("FileSource", Integer),
|
||||
("SceneType", Integer),
|
||||
("CFAPattern", Ty::Struct(CFA_PATTERN)),
|
||||
("CFAPattern", Ty::Struct("CFAPattern", CFA_PATTERN)),
|
||||
("CustomRendered", Integer),
|
||||
("ExposureMode", Integer),
|
||||
("WhiteBalance", Integer),
|
||||
@@ -553,7 +558,7 @@ const SCHEMAS: &[Schema] = &[
|
||||
("Contrast", Integer),
|
||||
("Saturation", Integer),
|
||||
("Sharpness", Integer),
|
||||
("DeviceSettingDescription", Ty::Struct(DEVICE_SETTINGS)),
|
||||
("DeviceSettingDescription", Ty::Struct("DeviceSettings", DEVICE_SETTINGS)),
|
||||
("SubjectDistanceRange", Integer),
|
||||
("ImageUniqueID", Text),
|
||||
("GPSVersionID", Text),
|
||||
@@ -588,11 +593,9 @@ const SCHEMAS: &[Schema] = &[
|
||||
Schema { uri: "http://ns.adobe.com/exif/1.0/aux/", prefix: "aux", properties: &[("Lens", Text), ("SerialNumber", Text)] },
|
||||
];
|
||||
|
||||
/// The properties of predefined schemas whose values don't have their type, in every metadata
|
||||
/// stream among the indirect objects `objs` (ISO 32000-1 14.3.2): the stream and the property as
|
||||
/// `prefix:name`, by stream in the order of `objs` and by name within one.
|
||||
pub(crate) fn mistyped_in(objs: &[(ObjRef, std::sync::Arc<Object>)]) -> Vec<(ObjRef, String)> {
|
||||
let mut out = Vec::new();
|
||||
/// Calls `f` with every metadata stream among the indirect objects `objs` (ISO 32000-1 14.3.2) that
|
||||
/// parses as XML, and the stream's object, each decoded once within one allowance.
|
||||
pub(crate) fn for_each_packet(objs: &[(ObjRef, std::sync::Arc<Object>)], mut f: impl FnMut(ObjRef, &roxmltree::Document<'_>)) {
|
||||
let mut budget = DecodeBudget::new(MAX_METADATA_BYTES, 0);
|
||||
for (r, o) in objs {
|
||||
let Object::Stream(s) = &**o else { continue };
|
||||
@@ -605,10 +608,9 @@ pub(crate) fn mistyped_in(objs: &[(ObjRef, std::sync::Arc<Object>)]) -> Vec<(Obj
|
||||
let Ok(bytes) = budget.decode_within(s, crate::xmp::MAX_XMP_BYTES) else { continue };
|
||||
let text = unicode(&bytes);
|
||||
if let Some(parsed) = crate::xmp::parse(&text) {
|
||||
out.extend(mistyped(&parsed).into_iter().map(|name| (*r, name)));
|
||||
f(*r, &parsed);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A packet's text in the Unicode encoding its byte order mark gives: UTF-16 big- or
|
||||
@@ -627,17 +629,20 @@ fn unicode(bytes: &[u8]) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The properties of predefined schemas in the packet `doc` whose values don't have their type,
|
||||
/// as `prefix:name` in name order.
|
||||
pub(crate) fn mistyped(doc: &roxmltree::Document<'_>) -> BTreeSet<String> {
|
||||
let mut out = BTreeSet::new();
|
||||
/// The properties of predefined schemas in the packet `doc` whose values don't have their type, in
|
||||
/// name order: each by its name with the namespace's preferred prefix and by its name as the packet
|
||||
/// writes it (see [`written`]), with the type it should have as [`named`] gives it.
|
||||
pub(crate) fn mistyped(doc: &roxmltree::Document<'_>) -> BTreeMap<(String, String), String> {
|
||||
let mut out = BTreeMap::new();
|
||||
let text = doc.input_text();
|
||||
let descriptions = doc.descendants().filter(|n| is(n, RDF, "Description") && n.parent_element().is_some_and(|p| is(&p, RDF, "RDF")));
|
||||
for d in descriptions {
|
||||
for a in d.attributes() {
|
||||
if let Some((schema, ty)) = a.namespace().and_then(|ns| property(ns, a.name()))
|
||||
&& !simple(ty, a.value())
|
||||
{
|
||||
out.insert(format!("{}:{}", schema.prefix, a.name()));
|
||||
let qname = text.get(a.range_qname()).unwrap_or_default();
|
||||
out.insert((format!("{}:{}", schema.prefix, a.name()), written(qname, a.name())), named(ty));
|
||||
}
|
||||
}
|
||||
for p in d.children().filter(Node::is_element) {
|
||||
@@ -645,13 +650,53 @@ pub(crate) fn mistyped(doc: &roxmltree::Document<'_>) -> BTreeSet<String> {
|
||||
if let Some((schema, ty)) = name.namespace().and_then(|ns| property(ns, name.name()))
|
||||
&& !fits(ty, &p, 0)
|
||||
{
|
||||
out.insert(format!("{}:{}", schema.prefix, name.name()));
|
||||
let qname = text.get(p.range()).unwrap_or_default().strip_prefix('<').unwrap_or_default();
|
||||
out.insert((format!("{}:{}", schema.prefix, name.name()), written(qname, name.name())), named(ty));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// How long a prefix the packet writes is repeated.
|
||||
const MAX_PREFIX_CHARS: usize = 64;
|
||||
|
||||
/// The property `local` as the packet writes it, from the text its qualified name begins
|
||||
/// (`qname`): `prefix:local`, or `local` alone in the default namespace. A prefix past
|
||||
/// [`MAX_PREFIX_CHARS`] is cut, with `…`.
|
||||
fn written(qname: &str, local: &str) -> String {
|
||||
let qname = qname.split(|c: char| c.is_whitespace() || matches!(c, '>' | '/' | '=')).next().unwrap_or_default();
|
||||
let Some((prefix, _)) = qname.split_once(':') else { return local.to_owned() };
|
||||
let mut out: String = prefix.chars().take(MAX_PREFIX_CHARS).collect();
|
||||
if out.len() < prefix.len() {
|
||||
out.push('…');
|
||||
}
|
||||
format!("{out}:{local}")
|
||||
}
|
||||
|
||||
/// `ty` as the XMP Specification writes value types: `Integer`, `GPSCoordinate`, `Lang Alt`,
|
||||
/// `seq Rational`, a structure by its type's name, such as `Flash`.
|
||||
fn named(ty: Ty) -> String {
|
||||
let array = |kind: &str, item: &Ty| format!("{kind} {}", named(*item));
|
||||
match ty {
|
||||
Text => "Text".into(),
|
||||
Ty::Guid => "GUID".into(),
|
||||
Integer => "Integer".into(),
|
||||
Real => "Real".into(),
|
||||
Boolean => "Boolean".into(),
|
||||
Date => "Date".into(),
|
||||
Rational => "Rational".into(),
|
||||
Ty::GpsCoordinate => "GPSCoordinate".into(),
|
||||
LangAlt => "Lang Alt".into(),
|
||||
Ty::Seq(item) => array("seq", item),
|
||||
Ty::Bag(item) => array("bag", item),
|
||||
Ty::Alt(item) => array("alt", item),
|
||||
Ty::Struct(name, _) => name.into(),
|
||||
Ty::AnyStruct => "struct".into(),
|
||||
Ty::OneOf(types) => types.iter().map(|t| named(*t)).collect::<Vec<_>>().join(" or "),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(ns: &str, name: &str) -> Option<(&'static Schema, Ty)> {
|
||||
let schema = SCHEMAS.iter().find(|s| s.uri == ns)?;
|
||||
schema.properties.iter().find(|(n, _)| *n == name).map(|(_, t)| (schema, *t))
|
||||
@@ -723,7 +768,7 @@ fn fits(ty: Ty, e: &Node<'_, '_>, depth: usize) -> bool {
|
||||
};
|
||||
array(e, &children, kind).is_some_and(|items| items.iter().all(|i| fits(*item, i, deeper)))
|
||||
}
|
||||
Ty::Struct(fields) => {
|
||||
Ty::Struct(_, fields) => {
|
||||
let Some(holder) = structure(e, &children, resource, description) else { return false };
|
||||
let attrs_ok = field_attributes(&holder).all(|a| match a.namespace().and_then(|ns| field(fields, ns, a.name())) {
|
||||
Some(t) => simple(t, a.value()),
|
||||
|
||||
@@ -8003,7 +8003,7 @@ 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/automation/tests/it/automation.rs::pdfa_through_tools", "crates/ui-egui/tests/it/compare.rs::pdfa_dialog_verifies_and_converts"]
|
||||
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."
|
||||
|
||||
[[feature]]
|
||||
|
||||
Reference in new issue
Block a user