Skip to content

Commit

Permalink
(MINOR) XmpMeta can now optionally format itself using Rust-style for…
Browse files Browse the repository at this point in the history
…matting (#193)
  • Loading branch information
scouten committed Jul 7, 2024
1 parent 74576a2 commit d949041
Show file tree
Hide file tree
Showing 3 changed files with 352 additions and 7 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ fs_extra = "1.3"

[dev-dependencies]
anyhow = "1.0.67"
pretty_assertions = "1.4.0"
tempfile = "3.2"
140 changes: 140 additions & 0 deletions src/tests/xmp_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3401,6 +3401,146 @@ mod impl_display {
}
}

mod impl_display_alternate {
use std::str::FromStr;

use pretty_assertions::assert_eq;

use crate::{tests::fixtures::*, XmpMeta};

#[test]
fn simple_case() {
let m = XmpMeta::from_str(STRUCT_EXAMPLE).unwrap();

assert_eq!(
r###"XmpMeta {
@name: "",
xmpRights: schema {
@ns: "http://ns.adobe.com/xap/1.0/rights/",
Marked: "True",
},
Iptc4xmpCore: schema {
@ns: "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/",
CreatorContactInfo: struct {
CiAdrPcode: "98110",
CiAdrCtry: "US",
},
},
}"###,
format!("{m:#}"),
);
}

#[test]
fn purple_square() {
let m = XmpMeta::from_str(PURPLE_SQUARE_XMP).unwrap();

dbg!(&m);

assert_eq!(
r###"XmpMeta {
@name: "",
dc: schema {
@ns: "http://purl.org/dc/elements/1.1/",
format: "application/vnd.adobe.photoshop",
description: array ordered alt_text {
@items: [
qualified {
@value: "a test file (öäüßÖÄÜ€中文)",
?xml:lang: qualifier {
@value: "x-default",
},
},
],
},
title: array ordered alt_text {
@items: [
qualified {
@value: "Purple Square",
?xml:lang: qualifier {
@value: "x-default",
},
},
],
},
creator: array ordered {
@items: [
"Llywelyn",
],
},
subject: array {
@items: [
"purple",
"square",
"Stefan",
"XMP",
"XMPFiles",
"test",
],
},
},
xmp: schema {
@ns: "http://ns.adobe.com/xap/1.0/",
CreatorTool: "Adobe Photoshop CS2 Windows",
CreateDate: "2006-04-25T15:32:01+02:00",
ModifyDate: "2006-04-27T15:38:36.655+02:00",
MetadataDate: "2006-04-26T16:47:10+02:00",
},
xmpMM: schema {
@ns: "http://ns.adobe.com/xap/1.0/mm/",
DocumentID: "uuid:FE607D9B5FD4DA118B7787757E22306B",
InstanceID: "uuid:BF664E7B33D5DA119129F691B53239AD",
},
tiff: schema {
@ns: "http://ns.adobe.com/tiff/1.0/",
Orientation: "1",
XResolution: "720000/10000",
YResolution: "720000/10000",
ResolutionUnit: "2",
NativeDigest: "256,257,258,259,262,274,277,284,530,531,282,283,296,301,318,319,529,532,306,270,271,272,305,315,33432;6F0EC2A1D6ADFA4DF4BB00D7C83AFAC0",
},
exif: schema {
@ns: "http://ns.adobe.com/exif/1.0/",
PixelXDimension: "200",
PixelYDimension: "200",
ColorSpace: "-1",
NativeDigest: "36864,40960,40961,37121,37122,40962,40963,37510,40964,36867,36868,33434,33437,34850,34852,34855,34856,37377,37378,37379,37380,37381,37382,37383,37384,37385,37386,37396,41483,41484,41486,41487,41488,41492,41493,41495,41728,41729,41730,41985,41986,41987,41988,41989,41990,41991,41992,41993,41994,41995,41996,42016,0,2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,20,22,23,24,25,26,27,28,30;D891A8B493E755131A3267739F6277DB",
},
photoshop: schema {
@ns: "http://ns.adobe.com/photoshop/1.0/",
ColorMode: "3",
ICCProfile: "Dell 1905FP Color Profile",
CaptionWriter: "Stefan",
History: "",
},
pdf: schema {
@ns: "http://ns.adobe.com/pdf/1.3/",
Keywords: "\"XMP metadata schema XML RDF\"",
Copyright: "2005 Adobe Systems Inc.",
},
pdfx: schema {
@ns: "http://ns.adobe.com/pdfx/1.3/",
Copyright: "2005 Adobe Systems Inc.",
},
xmpRights: schema {
@ns: "http://ns.adobe.com/xap/1.0/rights/",
Marked: "False",
},
}"###,
format!("{m:#}")
);
}

#[test]
fn init_fail() {
let m = XmpMeta::new_fail();
assert_eq!(
format!("{m:#}"),
"ERROR (NoCppToolkit): C++ XMP Toolkit not available"
);
}
}

mod impl_send {
use std::str::FromStr;

Expand Down
218 changes: 211 additions & 7 deletions src/xmp_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use std::{
use crate::{
ffi::{self, CXmpString},
IterOptions, OpenFileOptions, XmpDateTime, XmpError, XmpErrorType, XmpFile, XmpIterator,
XmpResult, XmpValue,
XmpProperty, XmpResult, XmpValue,
};

/// Represents the data model of an XMP packet.
Expand Down Expand Up @@ -52,6 +52,83 @@ use crate::{
/// expression. Must not be an empty string. The first component of a path
/// expression can be a namespace prefix; if so, the prefix must have been
/// registered via [`XmpMeta::register_namespace`].
///
/// ### Debug formatting using C++ XMP Toolkit
///
/// Using traditional `.to_string()` formatting yields
/// debug output from C++ XMP Toolkit:
///
/// ```
/// # use xmp_toolkit::XmpMeta;
/// # use std::str::FromStr;
/// const STRUCT_EXAMPLE: &str = r#"
/// <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.0-c000 1.000000, 0000/00/00-00:00:00">
/// <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
/// <rdf:Description rdf:about=""
/// xmlns:xmp="http://ns.adobe.com/xap/1.0/"
/// xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/"
/// xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
/// xmpRights:Marked="True">
/// <Iptc4xmpCore:CreatorContactInfo
/// Iptc4xmpCore:CiAdrPcode="98110"
/// Iptc4xmpCore:CiAdrCtry="US"/>
/// </rdf:Description>
/// </rdf:RDF>
/// </x:xmpmeta>
/// "#;
///
/// let m = XmpMeta::from_str(STRUCT_EXAMPLE).unwrap();
///
/// assert_eq!(
/// r###"<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/" xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"> <xmpRights:Marked>True</xmpRights:Marked> <Iptc4xmpCore:CreatorContactInfo rdf:parseType="Resource"> <Iptc4xmpCore:CiAdrPcode>98110</Iptc4xmpCore:CiAdrPcode> <Iptc4xmpCore:CiAdrCtry>US</Iptc4xmpCore:CiAdrCtry> </Iptc4xmpCore:CreatorContactInfo> </rdf:Description> </rdf:RDF> </x:xmpmeta>"###,
/// m.to_string(),
/// );
/// ```
///
/// ### Debug formatting using Rust-style formatting
///
/// Using alternate formatting `fmt!("{xmp:#}")"` yields `to_string()`
/// debug output with more Rust-like formatting:
///
/// ```
/// # use xmp_toolkit::XmpMeta;
/// # use std::str::FromStr;
/// const STRUCT_EXAMPLE: &str = r#"
/// <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.0-c000 1.000000, 0000/00/00-00:00:00">
/// <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
/// <rdf:Description rdf:about=""
/// xmlns:xmp="http://ns.adobe.com/xap/1.0/"
/// xmlns:xmpRights="http://ns.adobe.com/xap/1.0/rights/"
/// xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
/// xmpRights:Marked="True">
/// <Iptc4xmpCore:CreatorContactInfo
/// Iptc4xmpCore:CiAdrPcode="98110"
/// Iptc4xmpCore:CiAdrCtry="US"/>
/// </rdf:Description>
/// </rdf:RDF>
/// </x:xmpmeta>
/// "#;
///
/// let m = XmpMeta::from_str(STRUCT_EXAMPLE).unwrap();
///
/// assert_eq!(
/// r###"XmpMeta {
/// @name: "",
/// xmpRights: schema {
/// @ns: "http://ns.adobe.com/xap/1.0/rights/",
/// Marked: "True",
/// },
/// Iptc4xmpCore: schema {
/// @ns: "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/",
/// CreatorContactInfo: struct {
/// CiAdrPcode: "98110",
/// CiAdrCtry: "US",
/// },
/// },
/// }"###,
/// format!("{m:#}"),
/// );
/// ```
pub struct XmpMeta {
pub(crate) m: Option<*mut ffi::CXmpMeta>,
}
Expand Down Expand Up @@ -2017,15 +2094,142 @@ impl fmt::Debug for XmpMeta {

impl fmt::Display for XmpMeta {
/// Convert the XMP data model to RDF using a compact formatting.
///
/// If the `:#` flag is used (alternate formatting), use
/// Rust-style struct formatting.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
if f.alternate() {
if self.m.is_none() {
return write!(f, "ERROR (NoCppToolkit): C++ XMP Toolkit not available");
}

let mut ds = f.debug_struct("XmpMeta");
ds.field("@name", &self.name());

for schema in self.iter(IterOptions::default().immediate_children_only()) {
let prefix = XmpMeta::namespace_prefix(&schema.schema_ns)
.unwrap_or("-no prefix-".to_owned());
ds.field(
prefix.trim_end_matches(':'),
&PropertyDisplayHelper(self, &schema),
);
}

ds.finish()
} else {
match self.to_string_with_options(
ToStringOptions::default()
.omit_packet_wrapper()
.omit_all_formatting(),
) {
Ok(s) => write!(f, "{}", s.trim_end()),
Err(err) => write!(f, "ERROR ({:#?}): {}", err.error_type, err.debug_message),
}
}
}
}

struct PropertyDisplayHelper<'a>(pub &'a XmpMeta, pub &'a XmpProperty);

impl<'a> fmt::Debug for PropertyDisplayHelper<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self.to_string_with_options(
ToStringOptions::default()
.omit_packet_wrapper()
.omit_all_formatting(),
let mut flags: Vec<&'static str> = vec![];
let value = &self.1.value;

if value.is_schema_node() {
flags.push("schema");
}
if value.is_uri() {
flags.push("uri");
}
if value.is_struct() {
flags.push("struct");
}
if value.is_array() {
flags.push("array");
}
if value.is_ordered() {
flags.push("ordered");
}
if value.is_alt_text() {
flags.push("alt_text");
} else if value.is_alternate() {
flags.push("alternate");
}
if value.has_qualifiers() {
flags.push("qualified");
}
if value.is_qualifier() {
flags.push("qualifier");
}

let node_type = flags.join(" ");

let mut ds = f.debug_struct(&node_type);
if value.is_schema_node() {
ds.field("@ns", &self.1.schema_ns);
}

if !value.value.is_empty() {
ds.field("@value", &value.value);
}

let ns_prefix = XmpMeta::namespace_prefix(&self.1.schema_ns)
.unwrap_or_else(|| "-no-prefix-".to_owned());

let path_prefix = if !value.is_schema_node() {
Some(format!("{name}/", name = self.1.name))
} else {
None
};

if value.is_array() {
ds.field("@items", &PropertyListHelper(self.0, self.1));
} else {
for prop in self.0.iter(
IterOptions::default()
.property(&self.1.schema_ns, &self.1.name)
.immediate_children_only(),
) {
let name = if let Some(ref path_prefix) = path_prefix {
prop.name.trim_start_matches(path_prefix).to_owned()
} else {
prop.name.to_owned()
};

let name = name.trim_start_matches(&ns_prefix).to_owned();

if prop.value.has_no_flags() && prop.schema_ns == self.1.schema_ns {
ds.field(&name, &prop.value.value);
} else {
ds.field(&name, &PropertyDisplayHelper(self.0, &prop));
}
}
}

ds.finish()
}
}

struct PropertyListHelper<'a>(pub &'a XmpMeta, pub &'a XmpProperty);

impl<'a> fmt::Debug for PropertyListHelper<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let mut dl = f.debug_list();

for prop in self.0.iter(
IterOptions::default()
.property(&self.1.schema_ns, &self.1.name)
.immediate_children_only(),
) {
Ok(s) => write!(f, "{}", s.trim_end()),
Err(err) => write!(f, "ERROR ({:#?}): {}", err.error_type, err.debug_message),
if prop.value.has_no_flags() && prop.schema_ns == self.1.schema_ns {
dl.entry(&prop.value.value);
} else {
dl.entry(&PropertyDisplayHelper(self.0, &prop));
}
}

dl.finish()
}
}

Expand Down

0 comments on commit d949041

Please sign in to comment.