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:
mtlcl410 committed 2026-11-07 08:33:30 +08:00
commit 19f4e1ef21
4 files changed
+218 -73

No files matched your search

+26 -10
View File
@@ -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) {
+89 -5
View File
@@ -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]
+102 -57
View File
@@ -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()),
+1 -1
View File
@@ -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]]