extern crate alloc;
use raves_metadata_types::xmp::{XmpElement, XmpPrimitive, XmpValue, parse_types::XmpKind as Kind};
use xmltree::{AttributeName, Element};
use crate::xmp::{
error::XmpError,
value::{XmpElementExt as _, prims::parse_primitive},
};
pub mod error;
mod heuristics;
mod value;
pub mod types {
pub use raves_metadata_types::xmp::{XmpElement, XmpPrimitive, XmpValue, XmpValueStructField};
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)]
pub struct XmpDocument(Vec<XmpElement>);
impl XmpDocument {
pub fn values_ref(&self) -> &[XmpElement] {
&self.0
}
pub fn values_mut(&mut self) -> &mut [XmpElement] {
&mut self.0
}
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub struct Xmp {
document: XmpDocument,
}
impl PartialOrd for Xmp {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.document == other.document {
Some(std::cmp::Ordering::Equal)
} else {
None
}
}
}
impl Xmp {
pub fn new(raw_xml: &str) -> Result<Self, XmpError> {
let element: Element = Element::parse(raw_xml.as_bytes())?;
let document = parse_xmp(&element).map(XmpDocument)?;
Ok(Self { document })
}
pub fn document(&self) -> &XmpDocument {
&self.document
}
#[doc(alias = "union", alias = "concat")]
pub fn combine(mut self, mut other: Self) -> Self {
self.document.0.append(&mut other.document.0);
self
}
}
const RDF_NAMESPACE: &str = r"http://www.w3.org/1999/02/22-rdf-syntax-ns#";
const X_NAMESPACE: &str = r"adobe:ns:meta/";
fn parse_xmp(document: &Element) -> Result<Vec<XmpElement>, XmpError> {
let descriptions = get_rdf_descriptions(document)?;
if descriptions.is_empty() {
log::warn!("No `rdf:Description` elements found in the `rdf:RDF` element.");
return Err(XmpError::NoDescriptionElements);
}
Ok(descriptions
.iter()
.flat_map(|description| {
let desc_attrs = description.attributes.clone();
let parsed_attrs = desc_attrs.iter().flat_map(|(key, val)| {
if let Some(ref attr_namespace) = key.namespace
&& attr_namespace.as_str() == RDF_NAMESPACE
&& key.local_name.as_str() == "about"
{
log::trace!("Skipping `rdf:about` attribute as value on `rdf:Description`...");
return None;
}
log::debug!("Parsing attribute `{key}` with value `{val}`.");
parse_attribute((key.clone(), val.clone()))
});
description
.children
.iter()
.flat_map(|c| c.as_element())
.flat_map(parse_element)
.chain(parsed_attrs)
.collect::<Vec<XmpElement>>()
})
.collect())
}
pub(crate) fn get_rdf_descriptions(document: &Element) -> Result<Vec<&xmltree::Element>, XmpError> {
let parent = document
.get_child("xmpmeta")
.and_then(|c| {
match c.namespace.clone()?.as_str() {
X_NAMESPACE => Some(c),
other => {
log::warn!(
"Found `xmpmeta` element, but with wrong namespace!
- expected: `{X_NAMESPACE}`
- got: `{other}`"
);
None
}
}
})
.inspect(|_| log::debug!("Found an `x:xmpmeta` element."))
.unwrap_or(document);
let rdf = if parent.name == "RDF" {
Some(parent)
} else {
parent.get_child("RDF")
}
.and_then(|rdf| {
match rdf.namespace.clone()?.as_str() {
RDF_NAMESPACE => Some(rdf),
other => {
log::warn!(
"Found `RDF` element, but with wrong namespace!
- expected: `{RDF_NAMESPACE}`
- got: `{other}`"
);
None
}
}
})
.ok_or_else(|| {
log::error!("Couldn't find an `rdf:RDF` element in the document.");
XmpError::NoRdfElement
})?;
Ok(rdf
.children
.iter()
.flat_map(|child| child.as_element())
.filter(|child| {
if child.name != "Description" {
return false;
}
let Some(ref ns) = child.namespace else {
log::error!("Found `Description` element, but doesn't have a namespace!");
return false;
};
if ns != RDF_NAMESPACE {
log::error!(
"Cannot parse `Description` due to incorrect namespace!
- expected: {RDF_NAMESPACE}
- got: {ns}"
);
return false;
}
true
})
.collect())
}
fn parse_attribute((key, value): (AttributeName, String)) -> Option<XmpElement> {
let ns = match key.namespace {
Some(ref ns) => ns.clone(),
None => {
log::warn!(
"Attribute `{}` has no namespace. \
Cannot continue parsing as an element.",
&key.local_name
);
return None;
}
};
let Some(prefix) = key.prefix.clone() else {
log::warn!(
"Attribute has namespace, but no prefix. This is an unexpected \
situation. Please report it! {key:#?}"
);
return None;
};
Some({
let map_pair = (ns.as_str(), key.local_name.as_str());
let value = match raves_metadata_types::xmp::parse_table::XMP_PARSING_MAP.get(&map_pair) {
Some(schema) => {
let prim = match schema {
Kind::Simple(prim) => prim,
other => {
log::error!(
"Attempted to parse attribute, but schema \
requested a non-primitive. got: {other:#?}"
);
return None;
}
};
parse_primitive(value, prim)
.inspect_err(|e| {
log::error!("Failed to parse primitive attribute with schema: {e}")
})
.ok()?
}
None => {
XmpValue::Simple(XmpPrimitive::Text(value))
}
};
XmpElement {
namespace: ns,
prefix,
name: key.local_name,
value,
}
})
}
fn parse_element(element: &Element) -> Option<XmpElement> {
log::trace!("Parsing element `{}`.", element.name);
let Some(ns) = element.namespace.as_ref() else {
log::warn!(
"Element `{name}` has no namespace. Cannot continue parsing as an element.",
name = element.name
);
return None;
};
match raves_metadata_types::xmp::parse_table::XMP_PARSING_MAP
.get(&(ns.as_str(), element.name.as_str()))
{
Some(schema) => element
.value_with_schema(schema)
.inspect_err(|e| {
log::error!(
"Failed to parse element with schema! \n\
- err: \n{e} \n\n\
- schema: {schema:#?}"
)
})
.ok(),
None => element
.value_generic()
.inspect_err(|e| log::error!("Failed to parse element generically! err: {e}"))
.ok(),
}
}
#[cfg(test)]
mod tests {
use raves_metadata_types::xmp::{XmpElement, XmpPrimitive, XmpValue};
use crate::xmp::{Xmp, XmpDocument};
#[test]
fn blank_description_is_ok() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
let xmp = Xmp::new(
r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:ns="ns:myName/" /></rdf:RDF>"#,
)
.expect("should be able to parse blank `rdf:Description`");
assert_eq!(*xmp.document(), XmpDocument(Vec::new()));
}
#[test]
fn respects_rdf_about_attribute() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
let xmp: Xmp = Xmp::new(
r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:ns="ns:myName/">
</rdf:Description>
</rdf:RDF>"#,
)
.expect("should parse XMP correctly");
assert_eq!(*xmp.document(), XmpDocument(Vec::new()));
}
#[test]
fn rdf_about_attribute_isnt_required() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
let xmp: Xmp = Xmp::new(
r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description xmlns:my_ns="https://github.com/onkoe">
<my_ns:MyStruct>
<rdf:Description />
</my_ns:MyStruct>
</rdf:Description>
</rdf:RDF>"#,
)
.expect("shouldn't choke on description with no `rdf:about`");
assert_eq!(
xmp.document().0,
vec![XmpElement {
namespace: "https://github.com/onkoe".into(),
prefix: "my_ns".into(),
name: "MyStruct".into(),
value: XmpValue::Struct(vec![])
}]
);
}
#[test]
fn from_photoshop() {
_ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
const RAW_XML: &str = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c145 79.163499, 2018/08/13-16:40:22">
<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:photoshop="http://ns.adobe.com/photoshop/1.0/" xmlns:tiff="http://ns.adobe.com/tiff/1.0/" xmlns:exif="http://ns.adobe.com/exif/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#" xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#">
<dc:subject><rdf:Bag>
<rdf:li>farts</rdf:li>
<rdf:li>not farts</rdf:li>
<rdf:li>etc.</rdf:li>
</rdf:Bag></dc:subject>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>"#;
let xmp: Xmp =
Xmp::new(RAW_XML).expect("shouldn't choke on description with no `rdf:about`");
assert_eq!(
xmp.document().0,
vec![XmpElement {
namespace: "http://purl.org/dc/elements/1.1/".into(),
prefix: "dc".into(),
name: "subject".into(),
value: XmpValue::UnorderedArray(vec![
XmpElement {
name: "li".into(),
namespace: "http://www.w3.org/1999/02/22-rdf-syntax-ns#".into(),
prefix: "rdf".into(),
value: XmpValue::Simple(XmpPrimitive::Text("farts".into()))
},
XmpElement {
name: "li".into(),
namespace: "http://www.w3.org/1999/02/22-rdf-syntax-ns#".into(),
prefix: "rdf".into(),
value: XmpValue::Simple(XmpPrimitive::Text("not farts".into()))
},
XmpElement {
name: "li".into(),
namespace: "http://www.w3.org/1999/02/22-rdf-syntax-ns#".into(),
prefix: "rdf".into(),
value: XmpValue::Simple(XmpPrimitive::Text("etc.".into()))
},
])
}]
);
}
}