use crate::error::{Error, Result};
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
use xml::reader::{EventReader, XmlEvent};
pub struct XmpReader;
pub(crate) fn namespace_prefix(uri: &str) -> &str {
match uri {
"http://purl.org/dc/elements/1.1/" => "dc",
"http://ns.adobe.com/xap/1.0/" => "xmp",
"http://ns.adobe.com/xap/1.0/mm/" => "xmpMM",
"http://ns.adobe.com/xap/1.0/rights/" => "xmpRights",
"http://ns.adobe.com/tiff/1.0/" => "tiff",
"http://ns.adobe.com/exif/1.0/" => "exif",
"http://ns.adobe.com/exif/1.0/aux/" => "aux",
"http://ns.adobe.com/photoshop/1.0/" => "photoshop",
"http://ns.adobe.com/camera-raw-settings/1.0/" => "crs",
"http://ns.adobe.com/lightroom/1.0/" => "lr",
"http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/" => "Iptc4xmpCore",
"http://iptc.org/std/Iptc4xmpExt/2008-02-29/" => "Iptc4xmpExt",
"http://ns.google.com/photos/1.0/camera/" => "GCamera",
"http://ns.google.com/photos/1.0/image/" => "GImage",
"http://ns.google.com/photos/1.0/container/" => "GContainer",
"http://ns.google.com/photos/1.0/container/item/" => "GContainerItem",
"http://ns.google.com/photos/dd/1.0/device/" => "GDevice",
"http://ns.adobe.com/xmp/note/" => "xmpNote",
"adobe:ns:meta/" => "x",
"http://ns.adobe.com/pdf/1.3/" => "pdf",
"http://ns.adobe.com/xap/1.0/bj/" => "xmpBJ",
"http://ns.adobe.com/xap/1.0/sType/Job#" => "stJob",
"http://ns.adobe.com/xap/1.0/t/pg/" => "xmpTPg",
"http://ns.adobe.com/xap/1.0/g/" => "xmpG",
"http://ns.adobe.com/xap/1.0/g/img/" => "xmpGImg",
"http://ns.adobe.com/xap/1.0/sType/Dimensions#" => "stDim",
"http://ns.adobe.com/xap/1.0/sType/ResourceRef#" => "stRef",
"http://ns.adobe.com/xap/1.0/sType/Font#" => "stFnt",
"http://ns.adobe.com/xap/1.0/sType/ManifestItem#" => "stMfs",
"http://www.w3.org/2000/01/rdf-schema#" => "rdfs",
"http://ns.microsoft.com/photo/1.0/" => "MicrosoftPhoto",
"http://ns.useplus.org/ldf/xmp/1.0/" => "plus",
"http://ns.adobe.com/xap/1.0/sType/Area#" => "stArea",
"http://www.metadataworkinggroup.com/schemas/regions/" => "mwg-rs",
"http://www.metadataworkinggroup.com/schemas/keywords/" => "mwg-kw",
_ => "",
}
}
fn register_declared_prefix(
cur_ns: &mut std::collections::HashMap<String, String>,
resolved_uri: &mut std::collections::HashMap<String, String>,
uri: &str,
prefix: &str,
) {
if uri.is_empty() || prefix.is_empty() || prefix == "xml" || prefix == "xmlns" {
return;
}
if !namespace_prefix(uri).is_empty() {
return;
}
if cur_ns.contains_key(uri) {
return;
}
let used = if resolved_uri.contains_key(prefix) || is_known_xmp_prefix(prefix) {
let mut i = 0;
while resolved_uri.contains_key(&format!("tmp{i}")) {
i += 1;
}
format!("tmp{i}")
} else {
prefix.to_string()
};
resolved_uri.insert(used.clone(), uri.to_string());
cur_ns.insert(uri.to_string(), used);
}
fn xmp_group_prefix(uri: &str, cur_ns: &std::collections::HashMap<String, String>) -> String {
let std_prefix = namespace_prefix(uri);
if !std_prefix.is_empty() {
return std_prefix.to_string();
}
match cur_ns.get(uri) {
Some(declared) => declared.clone(),
None => "XMP".to_string(),
}
}
fn namespace_category(prefix: &str) -> &str {
match prefix {
"dc" => "Author",
"xmp" | "xmpMM" | "xmpRights" => "Other",
"tiff" => "Image",
"exif" | "aux" => "Camera",
"photoshop" => "Image",
"Iptc4xmpCore" | "Iptc4xmpExt" => "Other",
_ => "Other",
}
}
fn xmp_toplevel_family2<'a>(own_prefix: &str, raw_local: &str, fallback: &'a str) -> &'a str {
let canonical = exiftool_namespace_alias(own_prefix).unwrap_or(own_prefix);
let family1 = format!("XMP-{canonical}");
if crate::tags::group2::xmp_toplevel_is_invented(&family1, raw_local) {
crate::tags::group2::XMP_INVENTED_SENTINEL
} else {
fallback
}
}
fn is_hdrp_makernote_attr(local_name: &str) -> bool {
local_name == "HdrPlusMakernote" || local_name == "hdrp_makernote"
}
fn emit_hdrp_makernote(b64_value: &str, tags: &mut Vec<Tag>) {
use crate::metadata::google_hdrp::decode_hdrp_makernote;
let raw_bytes = b64_value.trim().len();
let print = format!(
"(Binary data {} bytes, use -b option to extract)",
raw_bytes
);
tags.push(Tag {
id: TagId::Text("GCamera:HdrPlusMakernote".into()),
name: "HDRPlusMakerNote".into(),
description: "HDRPlusMakerNote".into(),
group: TagGroup {
family0: "XMP".into(),
family1: "XMP-GCamera".into(),
family2: "Other".into(),
family3: "Main".into(),
},
raw_value: Value::String(b64_value.to_string()),
print_value: print,
priority: 0,
});
let hdrp_tags = decode_hdrp_makernote(b64_value);
tags.extend(hdrp_tags);
}
impl XmpReader {
pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
let mut tags = Vec::new();
let converted: Option<String> = if data.starts_with(&[0xFE, 0xFF]) {
let units: Vec<u16> = data[2..]
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&units))
} else if data.starts_with(&[0xFF, 0xFE]) && !data.starts_with(&[0xFF, 0xFE, 0x00, 0x00]) {
let units: Vec<u16> = data[2..]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&units))
} else if data.len() > 4 && data[0] == 0 && data[1] != 0 {
let units: Vec<u16> = data
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
Some(String::from_utf16_lossy(&units))
} else {
None
};
let xml_data: &str = if let Some(ref s) = converted {
s.as_str()
} else if data.starts_with(&[0xEF, 0xBB, 0xBF]) {
std::str::from_utf8(&data[3..])
.map_err(|e| Error::InvalidXmp(format!("invalid UTF-8: {}", e)))?
} else {
std::str::from_utf8(data)
.or_else(|_| {
let trimmed = &data[..data.iter().rposition(|&b| b == b'>').unwrap_or(0) + 1];
std::str::from_utf8(trimmed)
})
.map_err(|e| Error::InvalidXmp(format!("invalid UTF-8: {}", e)))?
};
let xml_sanitized: String = sanitize_xmp_xml(xml_data);
let xml_clean: String = fix_malformed_xml(&xml_sanitized);
let xml_escaped: String = escape_attribute_whitespace(&xml_clean);
let xml_for_parse: &str = &xml_escaped;
let is_inx = {
let trimmed = xml_for_parse.trim_start();
trimmed.starts_with("<?xml") && {
trimmed
.lines()
.take(5)
.any(|l| l.trim_start().starts_with("<?aid "))
}
};
if is_inx {
if let Some(cdata_start) = xml_for_parse.find("<![CDATA[<?xpacket begin") {
let xmp_start = cdata_start + 9; if let Some(end_marker) = xml_for_parse[xmp_start..].find("<?xpacket end=") {
let after_end = xmp_start + end_marker;
if let Some(close) = xml_for_parse[after_end..].find("?>") {
let xmp_end = after_end + close + 2; let xmp_data = &xml_for_parse[xmp_start..xmp_end];
let xmp_bytes = xmp_data.as_bytes().to_vec();
return XmpReader::read(&xmp_bytes);
}
}
}
return Ok(tags);
}
let is_rdf = xml_for_parse.contains("rdf:RDF") || xml_for_parse.contains("rdf:Description");
if !is_rdf {
return read_generic_xml(xml_for_parse);
}
let node_bags: std::collections::HashMap<String, Vec<String>> =
collect_node_bag_values(xml_for_parse);
let blank_node_props: std::collections::HashMap<String, Vec<(String, String, String)>> =
collect_blank_node_properties(xml_for_parse);
let inline_referenced_node_ids: std::collections::HashSet<String> =
collect_inline_referenced_node_ids(xml_for_parse);
let parser = EventReader::from_str(xml_for_parse);
let mut path: Vec<(String, String)> = Vec::new(); let mut current_text = String::new();
let mut in_rdf_li = false;
let mut list_values: Vec<String> = Vec::new();
let mut emit_empty_depths: std::collections::HashSet<usize> =
std::collections::HashSet::new();
let mut et_id_at_depth: std::collections::HashMap<usize, String> =
std::collections::HashMap::new();
let mut parse_resource_depths: Vec<usize> = Vec::new();
let mut inline_blank_node_stack: Vec<(String, String)> = Vec::new();
let mut suppress_direct_emit_depth: Option<usize> = None;
let mut gcontainer_fields: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut in_gcontainer_seq = false;
let mut in_gcontainer_li = false;
let mut current_li_lang: Option<String> = None;
let mut in_lang_alt = false;
let mut lang_alt_in_bag = false;
let mut bag_lang_values: std::collections::HashMap<String, Vec<Option<String>>> =
std::collections::HashMap::new();
let mut bag_item_count: usize = 0;
let mut cur_uri: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut cur_ns: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut resolved_uri: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for event in parser {
match event {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
let ns_uri = name.namespace.as_deref().unwrap_or("");
if let Some(pfx) = name.prefix.as_deref() {
cur_uri.insert(pfx.to_string(), ns_uri.to_string());
register_declared_prefix(&mut cur_ns, &mut resolved_uri, ns_uri, pfx);
}
for a in &attributes {
if let (Some(pfx), Some(uri)) =
(a.name.prefix.as_deref(), a.name.namespace.as_deref())
{
register_declared_prefix(&mut cur_ns, &mut resolved_uri, uri, pfx);
}
}
path.push((ns_uri.to_string(), name.local_name.clone()));
current_text.clear();
let et_id = attributes.iter().find(|a| {
a.name.local_name == "id"
&& (a.name.prefix.as_deref() == Some("et")
|| a.name.namespace.as_deref()
== Some("http://ns.exiftool.org/1.0/"))
});
if let Some(a) = et_id {
emit_empty_depths.insert(path.len());
et_id_at_depth.insert(path.len(), a.value.clone());
}
let has_parse_resource = attributes.iter().any(|a| {
a.name.local_name == "parseType"
&& (a.name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
|| a.name.prefix.as_deref() == Some("rdf"))
&& a.value == "Resource"
});
if has_parse_resource {
parse_resource_depths.push(path.len());
}
if name.local_name == "xmpmeta" || name.local_name == "xapmeta" {
for attr in &attributes {
if attr.name.local_name == "xmptk" || attr.name.local_name == "xaptk" {
tags.push(Tag {
id: TagId::Text("x:xmptk".into()),
name: "XMPToolkit".into(),
description: "XMP Toolkit".into(),
group: TagGroup {
family0: "XMP".into(),
family1: "XMP-x".into(),
family2: "Other".into(),
family3: "Main".into(),
},
raw_value: Value::String(attr.value.clone()),
print_value: attr.value.clone(),
priority: 0,
});
}
}
}
if name.local_name != "Description"
&& name.local_name != "RDF"
&& name.namespace.as_deref()
!= Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
if let Some(node_ref) = attributes.iter().find(|a| {
a.name.local_name == "nodeID"
&& (a.name.prefix.as_deref() == Some("rdf")
|| a.name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#"))
}) {
let node_id = &node_ref.value;
if let Some(bag_values) = node_bags.get(node_id) {
let ns_uri = name.namespace.as_deref().unwrap_or("");
let prefix = namespace_prefix(ns_uri);
let group_prefix = if prefix.is_empty() {
name.prefix.as_deref().unwrap_or("XMP")
} else {
prefix
};
let category = namespace_category(group_prefix);
let category =
xmp_toplevel_family2(group_prefix, &name.local_name, category);
let full_name = ucfirst(&name.local_name);
let value = if bag_values.len() == 1 {
Value::String(bag_values[0].clone())
} else {
Value::List(
bag_values
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
)
};
let pv = value.to_display_string();
tags.push(Tag {
id: TagId::Text(format!(
"{}:{}",
group_prefix, name.local_name
)),
name: full_name.clone(),
description: full_name,
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path,
&name.local_name,
group_prefix,
&cur_ns,
),
family2: category.into(),
family3: "Main".into(),
},
raw_value: value,
print_value: pv,
priority: 0,
});
}
if let Some(bn_props) = blank_node_props.get(node_id) {
let elem_ns = name.namespace.as_deref().unwrap_or("");
let elem_prefix_ns = namespace_prefix(elem_ns);
let elem_group = if elem_prefix_ns.is_empty() {
name.prefix.as_deref().unwrap_or("XMP")
} else {
elem_prefix_ns
};
let parent_uc = ucfirst(&strip_non_ascii(&name.local_name));
let anc_prefix =
build_struct_tag_prefix_without_last(&path, &name.local_name);
let elem_flat = if anc_prefix.is_empty() {
parent_uc.clone()
} else {
let stripped = strip_struct_prefix(&anc_prefix, &parent_uc);
format!("{}{}", anc_prefix, stripped)
};
for (prop_ns, prop_local, prop_val) in bn_props {
let prop_prefix = namespace_prefix(prop_ns);
let prop_group = if prop_prefix.is_empty() {
elem_group
} else {
prop_prefix
};
let prop_cat = namespace_category(prop_group);
let prop_uc = ucfirst(&strip_non_ascii(prop_local));
let stripped = strip_struct_prefix(&elem_flat, &prop_uc);
let flat_raw = format!("{}{}", elem_flat, stripped);
let flat = apply_flat_name_remap(&flat_raw).to_string();
tags.push(Tag {
id: TagId::Text(format!("{}:{}", prop_group, flat)),
name: flat.clone(),
description: flat,
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path, prop_local, prop_group, &cur_ns,
),
family2: prop_cat.into(),
family3: "Main".into(),
},
raw_value: Value::String(prop_val.clone()),
print_value: prop_val.clone(),
priority: 0,
});
}
}
}
}
let in_suppressed_bn = suppress_direct_emit_depth
.map(|d| path.len() > d)
.unwrap_or(false);
if name.local_name != "Description"
&& name.local_name != "RDF"
&& !in_suppressed_bn
&& name.namespace.as_deref()
!= Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
if let Some(res_attr) = attributes.iter().find(|a| {
a.name.local_name == "resource"
&& (a.name.prefix.as_deref() == Some("rdf")
|| a.name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#"))
}) {
let resource_val = res_attr.value.clone();
let ns_uri = name.namespace.as_deref().unwrap_or("");
let prefix = namespace_prefix(ns_uri);
let group_prefix = if prefix.is_empty() {
name.prefix.as_deref().unwrap_or("XMP")
} else {
prefix
};
let category = namespace_category(group_prefix);
let remapped = remap_xmp_tag_name(group_prefix, &name.local_name);
let full_name = if !parse_resource_depths.is_empty() {
let ancestor_prefix =
build_struct_tag_prefix_without_last(&path, &name.local_name);
if !ancestor_prefix.is_empty() {
let field_stripped =
strip_struct_prefix(&ancestor_prefix, &remapped);
let candidate =
format!("{}{}", ancestor_prefix, field_stripped);
apply_flat_name_remap(&candidate).to_string()
} else {
apply_flat_name_remap(&remapped).to_string()
}
} else {
apply_flat_name_remap(&remapped).to_string()
};
tags.push(Tag {
id: TagId::Text(format!("{}:{}", group_prefix, name.local_name)),
name: full_name.clone(),
description: full_name,
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path,
&name.local_name,
group_prefix,
&cur_ns,
),
family2: category.into(),
family3: "Main".into(),
},
raw_value: Value::String(resource_val.clone()),
print_value: resource_val,
priority: 0,
});
}
}
let rdf_ns_check = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
let desc_node_id = attributes
.iter()
.find(|a| {
a.name.local_name == "nodeID"
&& (a.name.prefix.as_deref() == Some("rdf")
|| a.name.namespace.as_deref() == Some(rdf_ns_check))
})
.map(|a| a.value.clone());
let is_top_level_blank_node_desc = name.local_name == "Description"
&& name.namespace.as_deref() == Some(rdf_ns_check)
&& desc_node_id
.as_ref()
.map(|nid| inline_referenced_node_ids.contains(nid.as_str()))
.unwrap_or(false)
&& path
.iter()
.rev()
.nth(1)
.map(|(ns, ln)| {
ns == rdf_ns_check
|| ln == "RDF"
|| ln == "xmpmeta"
|| ln == "xapmeta"
})
.unwrap_or(true);
if name.local_name == "Description" && !is_top_level_blank_node_desc {
for attr in &attributes {
if attr.name.local_name == "about" {
if !attr.value.is_empty() {
tags.push(Tag {
id: TagId::Text("rdf:about".into()),
name: "About".into(),
description: "About".into(),
group: TagGroup {
family0: "XMP".into(),
family1: "XMP-rdf".into(),
family2: "Other".into(),
family3: "Main".into(),
},
raw_value: Value::String(attr.value.clone()),
print_value: attr.value.clone(),
priority: 0,
});
}
continue;
}
if attr.name.local_name == "nodeID"
&& (attr.name.prefix.as_deref() == Some("rdf")
|| attr.name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#"))
{
continue;
}
if attr.name.prefix.as_deref() == Some("xmlns") {
continue;
}
if attr.name.local_name.starts_with("xmlns") {
continue;
}
if attr.name.prefix.as_deref() == Some("et")
|| attr.name.namespace.as_deref()
== Some("http://ns.exiftool.org/1.0/")
|| attr.name.namespace.as_deref()
== Some("http://ns.exiftool.ca/1.0/")
{
continue;
}
let attr_ns = attr.name.namespace.as_deref().unwrap_or("");
let attr_prefix = namespace_prefix(attr_ns);
let group_prefix = if attr_prefix.is_empty() {
attr.name.prefix.as_deref().unwrap_or("XMP")
} else {
attr_prefix
};
{
if !attr.value.is_empty()
&& group_prefix == "GCamera"
&& is_hdrp_makernote_attr(&attr.name.local_name)
{
emit_hdrp_makernote(&attr.value, &mut tags);
continue;
}
let category = namespace_category(group_prefix);
let remapped =
remap_xmp_tag_name(group_prefix, &attr.name.local_name);
let rdf_ns2 = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
let is_top_level = match path.iter().rev().nth(1) {
Some(p) => {
p.0 == rdf_ns2
|| p.1 == "RDF"
|| p.1 == "xmpmeta"
|| p.1 == "xapmeta"
}
None => true,
};
let full_name = if is_top_level {
remapped
} else {
let parent_elem = path.iter().rev().nth(1).unwrap();
let parent_uc = ucfirst(&strip_non_ascii(&parent_elem.1));
let field_uc = ucfirst(&strip_non_ascii(&attr.name.local_name));
let stripped = strip_struct_prefix(&parent_uc, &field_uc);
apply_flat_name_remap(&format!("{}{}", parent_uc, stripped))
.to_string()
};
let category = if is_top_level {
xmp_toplevel_family2(
group_prefix,
&attr.name.local_name,
category,
)
} else {
category
};
tags.push(Tag {
id: TagId::Text(format!(
"{}:{}",
group_prefix, attr.name.local_name
)),
name: full_name.clone(),
description: full_name,
group: TagGroup {
family0: "XMP".to_string(),
family1: xmp_family1(
&path,
&attr.name.local_name,
group_prefix,
&cur_ns,
),
family2: category.to_string(),
family3: "Main".into(),
},
raw_value: parse_xmp_value(&attr.value),
print_value: attr.value.clone(),
priority: 0,
});
}
}
}
let rdf_ns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
if name.local_name == "Description" && name.namespace.as_deref() == Some(rdf_ns)
{
if let Some(nid_attr) = attributes.iter().find(|a| {
a.name.local_name == "nodeID"
&& (a.name.prefix.as_deref() == Some("rdf")
|| a.name.namespace.as_deref() == Some(rdf_ns))
}) {
let nid = nid_attr.value.clone();
let parent_is_property = path
.iter()
.rev()
.nth(1)
.map(|(pns, pln)| {
pns != rdf_ns
&& pln != "RDF"
&& pln != "xmpmeta"
&& pln != "xapmeta"
})
.unwrap_or(false);
if parent_is_property {
if let Some(parent) = path.iter().rev().nth(1) {
inline_blank_node_stack.push((nid, parent.1.clone()));
}
} else if inline_referenced_node_ids.contains(nid.as_str()) {
suppress_direct_emit_depth = Some(path.len());
} else {
if let Some(bn_props) = blank_node_props.get(nid.as_str()) {
for (prop_ns, prop_local, prop_val) in bn_props.clone() {
let prop_prefix = xmp_group_prefix(&prop_ns, &cur_ns);
let prop_prefix = prop_prefix.as_str();
let category = namespace_category(prop_prefix);
let remapped = remap_xmp_tag_name(prop_prefix, &prop_local);
tags.push(Tag {
id: TagId::Text(format!(
"{}:{}",
prop_prefix, prop_local
)),
name: remapped.clone(),
description: remapped,
group: TagGroup {
family0: "XMP".to_string(),
family1: format!("XMP-{}", prop_prefix),
family2: category.to_string(),
family3: "Main".into(),
},
raw_value: parse_xmp_value(&prop_val),
print_value: prop_val.clone(),
priority: 0,
});
}
}
suppress_direct_emit_depth = Some(path.len());
}
}
}
let is_rdf_structural = name.namespace.as_deref() == Some(rdf_ns)
|| name.local_name == "Description"
|| name.local_name == "RDF"
|| name.local_name == "li"
|| name.local_name == "Seq"
|| name.local_name == "Bag"
|| name.local_name == "Alt"
|| name.local_name == "xmpmeta"
|| name.local_name == "xapmeta"
|| name.namespace.as_deref() == Some("adobe:ns:meta/");
if !is_rdf_structural && !attributes.is_empty() {
let elem_ns = name.namespace.as_deref().unwrap_or("");
let elem_prefix = namespace_prefix(elem_ns);
let elem_group = if elem_prefix.is_empty() {
name.prefix.as_deref().unwrap_or("XMP")
} else {
elem_prefix
};
let ancestors_prefix =
build_struct_tag_prefix_without_last(&path, &name.local_name);
let elem_uc = ucfirst(&strip_non_ascii(&name.local_name));
let elem_flat = if ancestors_prefix.is_empty() {
elem_uc.clone()
} else {
let stripped = strip_struct_prefix(&ancestors_prefix, &elem_uc);
format!("{}{}", ancestors_prefix, stripped)
};
for attr in &attributes {
if attr.name.prefix.as_deref() == Some("xmlns") {
continue;
}
if attr.name.local_name.starts_with("xmlns") {
continue;
}
if attr.name.prefix.as_deref() == Some("rdf")
|| attr.name.namespace.as_deref() == Some(rdf_ns)
{
continue;
}
if attr.name.prefix.as_deref() == Some("et")
|| attr.name.namespace.as_deref()
== Some("http://ns.exiftool.org/1.0/")
|| attr.name.namespace.as_deref()
== Some("http://ns.exiftool.ca/1.0/")
{
continue;
}
if attr.name.prefix.as_deref() == Some("xml")
|| attr.name.namespace.as_deref()
== Some("http://www.w3.org/XML/1998/namespace")
{
continue;
}
if attr.name.namespace.as_deref()
== Some("http://ns.google.com/photos/1.0/container/item/")
{
continue;
}
let attr_ns = attr.name.namespace.as_deref().unwrap_or("");
let attr_prefix_resolved = namespace_prefix(attr_ns);
let attr_group = if attr_prefix_resolved.is_empty() {
attr.name.prefix.as_deref().unwrap_or(elem_group)
} else {
attr_prefix_resolved
};
let field_uc = ucfirst(&strip_non_ascii(&attr.name.local_name));
let field_stripped = strip_struct_prefix(&elem_flat, &field_uc);
let flat_name_raw = format!("{}{}", elem_flat, field_stripped);
let flat_name = apply_flat_name_remap(&flat_name_raw).to_string();
let category = namespace_category(attr_group);
let pv = attr.value.clone();
tags.push(Tag {
id: TagId::Text(format!("{}:{}", attr_group, flat_name)),
name: flat_name.clone(),
description: flat_name,
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path,
&attr.name.local_name,
elem_group,
&cur_ns,
),
family2: category.into(),
family3: "Main".into(),
},
raw_value: parse_xmp_value(&attr.value),
print_value: pv,
priority: 0,
});
}
}
if name.local_name == "Seq"
&& name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
if let Some(parent) = path.iter().rev().nth(1) {
if parent.1 == "Directory"
&& parent.0 == "http://ns.google.com/photos/1.0/container/"
{
in_gcontainer_seq = true;
}
}
}
if in_gcontainer_seq
&& name.local_name == "li"
&& name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
in_gcontainer_li = true;
in_rdf_li = true;
} else if name.local_name == "li"
&& name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
if in_lang_alt {
current_li_lang = attributes
.iter()
.find(|a| {
a.name.local_name == "lang"
&& (a.name.prefix.as_deref() == Some("xml")
|| a.name.namespace.as_deref()
== Some("http://www.w3.org/XML/1998/namespace"))
})
.map(|a| a.value.clone());
}
in_rdf_li = true;
}
if name.local_name == "Alt"
&& name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
in_lang_alt = true;
let depth = path.len();
if depth >= 3 {
let li_elem = &path[depth - 2]; let bag_elem = &path[depth - 3]; if li_elem.1 == "li"
&& li_elem.0 == "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
&& bag_elem.1 == "Bag"
&& bag_elem.0 == "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
{
lang_alt_in_bag = true;
}
}
}
if in_gcontainer_li
&& name.local_name == "Item"
&& name.namespace.as_deref()
== Some("http://ns.google.com/photos/1.0/container/")
{
let mut found: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
for attr in &attributes {
if attr.name.namespace.as_deref()
== Some("http://ns.google.com/photos/1.0/container/item/")
{
let field = ucfirst(&attr.name.local_name);
found.insert(field, attr.value.clone());
}
}
let known = ["Mime", "Semantic", "Length"];
for k in &known {
if let Some(v) = found.get(*k) {
gcontainer_fields
.entry(k.to_string())
.or_default()
.push(v.clone());
}
}
}
}
Ok(XmlEvent::Characters(text)) | Ok(XmlEvent::CData(text)) => {
current_text.push_str(&text);
}
Ok(XmlEvent::EndElement { name }) => {
let ns_uri = name.namespace.as_deref().unwrap_or("");
if name.local_name == "li"
&& name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
if in_lang_alt {
let lang = current_li_lang
.take()
.unwrap_or_else(|| "x-default".to_string());
let text = normalize_xml_text(¤t_text);
let opt_text: Option<String> = Some(text.clone());
if lang_alt_in_bag {
let entry = bag_lang_values.entry(lang.clone()).or_default();
while entry.len() < bag_item_count {
entry.push(None);
}
entry.push(opt_text);
} else {
if lang == "x-default" {
list_values.push(text);
} else {
bag_lang_values.entry(lang).or_default().push(opt_text);
}
}
} else if lang_alt_in_bag && !in_lang_alt {
bag_item_count += 1;
} else if in_gcontainer_li {
in_gcontainer_li = false;
} else if !normalize_xml_text(¤t_text).is_empty() {
list_values.push(normalize_xml_text(¤t_text));
}
in_rdf_li = false;
path.pop();
current_text.clear();
continue;
}
if (name.local_name == "Seq"
|| name.local_name == "Bag"
|| name.local_name == "Alt")
&& name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
if name.local_name == "Alt" {
in_lang_alt = false;
if lang_alt_in_bag {
path.pop();
current_text.clear();
continue;
}
if let Some(parent) = path.iter().rev().nth(1) {
let tag_name = parent.1.clone();
let group_prefix = xmp_group_prefix(&parent.0, &cur_ns);
let group_prefix = group_prefix.as_str();
let category = namespace_category(group_prefix);
let rdf_ns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
let in_struct_li_alt = !parse_resource_depths.is_empty()
|| path
.iter()
.rev()
.nth(2)
.map(|(ns, ln)| ln == "li" && ns == rdf_ns)
.unwrap_or(false);
let (full_tag_name, emit_group_prefix, emit_category) =
if in_struct_li_alt {
let ancestor_prefix =
build_struct_tag_prefix_without_last(&path, &tag_name);
let field_uc = ucfirst(&strip_non_ascii(&tag_name));
if !ancestor_prefix.is_empty() {
let stripped =
strip_struct_prefix(&ancestor_prefix, &field_uc);
let flat_raw =
format!("{}{}", ancestor_prefix, stripped);
let flat = apply_flat_name_remap(&flat_raw).to_string();
let sp_gp = path
.iter()
.rev()
.skip(2)
.skip_while(|(ns, ln)| {
ln == "li"
|| ln == "Bag"
|| ln == "Seq"
|| ln == "Alt"
|| ns == rdf_ns
})
.find(|(ns, ln)| {
ln != "Description" && ns != rdf_ns
})
.map(|(sp_ns, _)| {
let p = namespace_prefix(sp_ns);
if p.is_empty() {
group_prefix
} else {
p
}
})
.unwrap_or(group_prefix);
let cat = namespace_category(sp_gp);
(flat, sp_gp.to_string(), cat.to_string())
} else {
(
ucfirst(&strip_non_ascii(&tag_name)),
group_prefix.to_string(),
category.to_string(),
)
}
} else {
let tn = apply_flat_name_remap(&ucfirst(&strip_non_ascii(
&tag_name,
)))
.to_string();
let cat =
xmp_toplevel_family2(group_prefix, &tag_name, category);
(tn, group_prefix.to_string(), cat.to_string())
};
let has_nonempty = list_values.iter().any(|s| !s.is_empty());
if !list_values.is_empty() && has_nonempty {
let main_val = if list_values.len() == 1 {
Value::String(list_values[0].clone())
} else {
Value::List(
list_values
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
)
};
let pv = main_val.to_display_string();
tags.push(Tag {
id: TagId::Text(format!(
"{}:{}",
emit_group_prefix, tag_name
)),
name: full_tag_name.clone(),
description: full_tag_name.clone(),
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path,
&tag_name,
&emit_group_prefix,
&cur_ns,
),
family2: emit_category.clone(),
family3: "Main".into(),
},
raw_value: main_val,
print_value: pv,
priority: 0,
});
}
list_values.clear();
let mut lang_keys: Vec<String> =
bag_lang_values.keys().cloned().collect();
lang_keys.sort();
for lang in &lang_keys {
let vals = &bag_lang_values[lang];
let non_none: Vec<String> =
vals.iter().filter_map(|v| v.clone()).collect();
if !non_none.is_empty() {
let lang_tag = format!("{}-{}", full_tag_name, lang);
let val = if non_none.len() == 1 {
Value::String(non_none[0].clone())
} else {
Value::List(
non_none
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
)
};
let pv = val.to_display_string();
tags.push(Tag {
id: TagId::Text(format!(
"{}-{}:{}",
emit_group_prefix, lang, tag_name
)),
name: lang_tag.clone(),
description: lang_tag.clone(),
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path,
&tag_name,
&emit_group_prefix,
&cur_ns,
),
family2: emit_category.clone(),
family3: "Main".into(),
},
raw_value: val,
print_value: pv,
priority: 0,
});
}
}
bag_lang_values.clear();
}
path.pop();
current_text.clear();
continue;
}
if name.local_name == "Bag" && lang_alt_in_bag {
lang_alt_in_bag = false;
bag_item_count = 0;
if let Some(parent) = path.iter().rev().nth(1) {
let tag_name = parent.1.clone();
let group_prefix = xmp_group_prefix(&parent.0, &cur_ns);
let group_prefix = group_prefix.as_str();
let category = namespace_category(group_prefix);
let ancestor_prefix =
build_struct_tag_prefix_without_last(&path, &tag_name);
let field_uc = ucfirst(&strip_non_ascii(&tag_name));
let full_flat_base = if !ancestor_prefix.is_empty() {
let stripped = strip_struct_prefix(&ancestor_prefix, &field_uc);
let raw = format!("{}{}", ancestor_prefix, stripped);
apply_flat_name_remap(&raw).to_string()
} else {
apply_flat_name_remap(&field_uc).to_string()
};
let category = if ancestor_prefix.is_empty() {
xmp_toplevel_family2(group_prefix, &tag_name, category)
} else {
category
};
let mut lang_keys: Vec<String> =
bag_lang_values.keys().cloned().collect();
lang_keys.sort_by(|a, b| {
if a == "x-default" {
std::cmp::Ordering::Less
} else if b == "x-default" {
std::cmp::Ordering::Greater
} else {
a.cmp(b)
}
});
for lang in &lang_keys {
let vals = &bag_lang_values[lang];
let is_default = lang == "x-default";
let present: Vec<&str> =
vals.iter().filter_map(|v| v.as_deref()).collect();
if present.is_empty() {
continue;
}
let (tag_key, tag_display) = if is_default {
(full_flat_base.clone(), full_flat_base.clone())
} else {
let lt = format!("{}-{}", full_flat_base, lang);
(lt.clone(), lt)
};
for value in present {
tags.push(Tag {
id: TagId::Text(format!(
"{}:{}",
group_prefix, tag_key
)),
name: tag_key.clone(),
description: tag_display.clone(),
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path,
&tag_key,
group_prefix,
&cur_ns,
),
family2: category.into(),
family3: "Main".into(),
},
raw_value: Value::String(value.to_string()),
print_value: value.to_string(),
priority: 0,
});
}
}
bag_lang_values.clear();
}
path.pop();
current_text.clear();
continue;
}
if in_gcontainer_seq && name.local_name == "Seq" {
in_gcontainer_seq = false;
for (field, values) in &gcontainer_fields {
let tag_name = format!("DirectoryItem{}", field);
let value = if values.len() == 1 {
Value::String(values[0].clone())
} else {
Value::List(
values.iter().map(|s| Value::String(s.clone())).collect(),
)
};
let print_value = value.to_display_string();
tags.push(Tag {
id: TagId::Text(format!("GContainer:{}", tag_name)),
name: tag_name.clone(),
description: tag_name.clone(),
group: TagGroup {
family0: "XMP".into(),
family1: "XMP-GContainer".into(),
family2: "Image".into(),
family3: "Main".into(),
},
raw_value: value,
print_value,
priority: 0,
});
}
gcontainer_fields.clear();
} else if !list_values.is_empty() {
if let Some(parent) = path.iter().rev().nth(1) {
let tag_name = &parent.1;
if tag_name == "RDF"
|| tag_name == "xmpmeta"
|| tag_name == "xapmeta"
|| parent.0 == "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|| parent.0 == "adobe:ns:meta/"
{
list_values.clear();
path.pop();
current_text.clear();
continue;
}
let group_prefix = xmp_group_prefix(&parent.0, &cur_ns);
let group_prefix = group_prefix.as_str();
let _category = namespace_category(group_prefix);
let value = if list_values.len() == 1 {
Value::String(list_values[0].clone())
} else {
Value::List(
list_values
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
)
};
let ancestor_prefix =
build_struct_tag_prefix_without_last(&path, tag_name);
let field_uc = remap_xmp_tag_name(group_prefix, tag_name);
let (full_name, emit_group_prefix) = if !ancestor_prefix.is_empty()
{
let field_stripped =
strip_struct_prefix(&ancestor_prefix, &field_uc);
let raw = format!("{}{}", ancestor_prefix, field_stripped);
let flat = apply_flat_name_remap(&raw).to_string();
let sp_gp = path
.iter()
.rev()
.skip(1) .skip(1) .skip_while(|(ns, ln)| {
ln == "li"
|| ln == "Bag"
|| ln == "Seq"
|| ln == "Alt"
|| ns
== "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
})
.find(|(ns, ln)| {
ln != "Description"
&& ns
!= "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
})
.map(|(sp_ns, _)| {
let p = namespace_prefix(sp_ns);
if p.is_empty() {
group_prefix
} else {
p
}
})
.unwrap_or(group_prefix);
(flat, sp_gp.to_string())
} else {
let flat = apply_flat_name_remap(&field_uc).to_string();
(flat, group_prefix.to_string())
};
let emit_cat = namespace_category(&emit_group_prefix);
let emit_cat = if ancestor_prefix.is_empty() {
xmp_toplevel_family2(&emit_group_prefix, tag_name, emit_cat)
} else {
emit_cat
};
let print_value = value.to_display_string();
tags.push(Tag {
id: TagId::Text(format!("{}:{}", emit_group_prefix, tag_name)),
name: full_name.clone(),
description: full_name,
group: TagGroup {
family0: "XMP".to_string(),
family1: xmp_family1(
&path,
tag_name,
&emit_group_prefix,
&cur_ns,
),
family2: emit_cat.to_string(),
family3: "Main".into(),
},
raw_value: value,
print_value,
priority: 0,
});
}
list_values.clear();
}
path.pop();
current_text.clear();
continue;
}
if !normalize_xml_text(¤t_text).is_empty()
&& in_rdf_li
&& name.namespace.as_deref()
!= Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
&& name.local_name != "Description"
{
let ancestor_prefix =
build_struct_tag_prefix_without_last(&path, &name.local_name);
if !ancestor_prefix.is_empty() {
let field_local = ucfirst(&strip_non_ascii(&name.local_name));
let field_stripped =
strip_struct_prefix(&ancestor_prefix, &field_local);
let flat_name_raw = format!("{}{}", ancestor_prefix, field_stripped);
let flat_name = apply_flat_name_remap(&flat_name_raw).to_string();
let prefix = namespace_prefix(ns_uri);
let group_prefix = if prefix.is_empty() {
name.prefix.as_deref().unwrap_or("XMP")
} else {
prefix
};
let category = namespace_category(group_prefix);
tags.push(Tag {
id: TagId::Text(format!("{}:{}", group_prefix, flat_name)),
name: flat_name.clone(),
description: flat_name,
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path,
&name.local_name,
group_prefix,
&cur_ns,
),
family2: category.into(),
family3: "Main".into(),
},
raw_value: parse_xmp_value(&normalize_xml_text(¤t_text)),
print_value: normalize_xml_text(¤t_text),
priority: 0,
});
}
path.pop();
current_text.clear();
continue;
}
let has_et_depth = emit_empty_depths.contains(&path.len());
let in_suppressed_blank_node = suppress_direct_emit_depth
.map(|d| path.len() > d)
.unwrap_or(false);
if (!normalize_xml_text(¤t_text).is_empty() || has_et_depth)
&& !in_rdf_li
&& !in_suppressed_blank_node
{
let prefix = namespace_prefix(ns_uri);
let tag_name = &name.local_name;
if tag_name != "Description"
&& name.namespace.as_deref()
!= Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
let _ = prefix;
let group_prefix_owned = xmp_group_prefix(ns_uri, &cur_ns);
let group_prefix = group_prefix_owned.as_str();
let category = namespace_category(group_prefix);
let text_val = normalize_xml_text(¤t_text);
let named_et_id = et_id_at_depth
.get(&path.len())
.map(|id| id.parse::<i64>().is_err())
.unwrap_or(false);
let (value, mut print_value) = if named_et_id {
(Value::String(text_val.clone()), text_val.clone())
} else {
let v = parse_xmp_value(&text_val);
let pv = xmp_rational_decimal(&v)
.unwrap_or_else(|| v.to_display_string());
(v, pv)
};
let remapped = remap_xmp_tag_name(group_prefix, tag_name);
let ancestor_prefix =
build_struct_tag_prefix_without_last(&path, tag_name);
let full_name = if !ancestor_prefix.is_empty() {
let field_stripped =
strip_struct_prefix(&ancestor_prefix, &remapped);
let candidate = format!("{}{}", ancestor_prefix, field_stripped);
apply_flat_name_remap(&candidate).to_string()
} else {
apply_flat_name_remap(&remapped).to_string()
};
if let Some(pc) =
xmp_exif_print_override(group_prefix, &full_name, &value)
{
print_value = pc;
}
let category = if ancestor_prefix.is_empty() {
xmp_toplevel_family2(group_prefix, tag_name, category)
} else {
category
};
tags.push(Tag {
id: TagId::Text(format!("{}:{}", group_prefix, tag_name)),
name: full_name.clone(),
description: full_name,
group: TagGroup {
family0: "XMP".to_string(),
family1: xmp_family1(&path, tag_name, group_prefix, &cur_ns),
family2: category.to_string(),
family3: "Main".into(),
},
raw_value: value,
print_value,
priority: 0,
});
}
}
if parse_resource_depths.last() == Some(&path.len()) {
parse_resource_depths.pop();
}
emit_empty_depths.remove(&path.len());
et_id_at_depth.remove(&path.len());
if name.local_name == "Description"
&& name.namespace.as_deref()
== Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
{
if suppress_direct_emit_depth == Some(path.len()) {
suppress_direct_emit_depth = None;
}
if let Some((node_id, parent_local)) =
inline_blank_node_stack.last().cloned()
{
if path
.last()
.map(|(_, ln)| ln == "Description")
.unwrap_or(false)
{
inline_blank_node_stack.pop();
if let Some(bn_props) = blank_node_props.get(&node_id) {
let parent_uc = ucfirst(&strip_non_ascii(&parent_local));
let parent_ns = path
.iter()
.rev()
.nth(1)
.map(|(ns, _)| ns.as_str())
.unwrap_or("");
let parent_group = xmp_group_prefix(parent_ns, &cur_ns);
for (prop_ns, prop_local, prop_val) in bn_props {
let own = xmp_group_prefix(prop_ns, &cur_ns);
let prop_group: &str = if own == "XMP" {
parent_group.as_str()
} else {
own.as_str()
};
let prop_cat = namespace_category(prop_group);
let prop_uc = ucfirst(&strip_non_ascii(prop_local));
let stripped = strip_struct_prefix(&parent_uc, &prop_uc);
let flat_raw = format!("{}{}", parent_uc, stripped);
let flat = apply_flat_name_remap(&flat_raw).to_string();
{
tags.push(Tag {
id: TagId::Text(format!("{}:{}", prop_group, flat)),
name: flat.clone(),
description: flat,
group: TagGroup {
family0: "XMP".into(),
family1: xmp_family1(
&path, prop_local, prop_group, &cur_ns,
),
family2: prop_cat.into(),
family3: "Main".into(),
},
raw_value: Value::String(prop_val.clone()),
print_value: prop_val.clone(),
priority: 0,
});
}
}
}
}
}
}
path.pop();
current_text.clear();
}
Err(_) => continue,
_ => {}
}
}
let has_gainmap = tags
.iter()
.any(|t| t.name == "DirectoryItemSemantic" && t.print_value.contains("GainMap"));
if has_gainmap {
let gainmap_mime = tags
.iter()
.find(|t| t.name == "DirectoryItemSemantic")
.and_then(|t| {
if let Value::List(ref items) = t.raw_value {
items
.iter()
.enumerate()
.find(|(_, v)| v.to_display_string() == "GainMap")
.map(|(i, _)| i)
} else {
None
}
})
.and_then(|idx| {
tags.iter()
.find(|t| t.name == "DirectoryItemMime")
.and_then(|t| match &t.raw_value {
Value::List(items) => items.get(idx).map(|v| v.to_display_string()),
Value::String(s) => {
if idx == 0 {
Some(s.clone())
} else {
None
}
}
_ => None,
})
})
.unwrap_or_else(|| "image/jpeg".to_string());
let warning_msg = format!(
"[minor] Error reading GainMap {} from trailer",
gainmap_mime
);
tags.push(crate::tag::warning_tag(warning_msg));
}
for tag in tags.iter_mut() {
let fam1 = tag.group.family1.as_str();
if fam1 != "XMP-exif" && fam1 != "XMP-tiff" {
continue;
}
if tag.name == "Flash" {
continue;
}
let numv = match &tag.raw_value {
Value::String(s) => match s.parse::<i64>() {
Ok(i) if i >= 0 => Value::U32(i as u32),
Ok(i) => Value::I32(i as i32),
Err(_) => tag.raw_value.clone(),
},
other => other.clone(),
};
if let Some(pv) = crate::tags::exif::print_conv_by_tag_name(&tag.name, &numv) {
tag.print_value = pv;
} else if let Some(f) = xmp_rational_decimal(&numv) {
tag.print_value = f;
}
}
for tag in tags.iter_mut() {
if tag.group.family1 == "XMP-pdf" && tag.name == "Trapped" {
if let Some(rest) = tag.print_value.strip_prefix('/') {
tag.print_value = rest.to_string();
}
if let Value::String(s) = &tag.raw_value {
if let Some(rest) = s.strip_prefix('/') {
tag.raw_value = Value::String(rest.to_string());
}
}
}
}
for tag in tags.iter_mut() {
if tag.group.family1 == "XMP-photomechanic" && tag.name == "TimeCreated" {
let converted = exif_time(&tag.print_value);
tag.print_value = converted.clone();
tag.raw_value = Value::String(converted);
}
}
for tag in tags.iter_mut() {
if let Some(quotient) = convert_xmp_rational(&tag.print_value) {
if xmp_property_is_unknown_aliased(&tag.group.family1, &tag.name) {
tag.print_value = quotient.clone();
tag.raw_value = Value::String(quotient);
continue;
}
}
if let Some(reformatted) = convert_xmp_date(&tag.print_value) {
tag.print_value = reformatted;
if xmp_property_is_unknown_aliased(&tag.group.family1, &tag.name) {
tag.group.family2 = "Time".into();
}
}
if tag.print_value.contains("ns.useplus.org/ldf/vocab/") {
let mapped: Vec<String> = tag
.print_value
.split(", ")
.map(|item| {
let code = item.trim().rsplit('/').next().unwrap_or("").trim();
plus_vocab(code)
.map(|s| s.to_string())
.unwrap_or_else(|| item.to_string())
})
.collect();
tag.print_value = mapped.join(", ");
if let Value::List(_) = tag.raw_value {
tag.raw_value = Value::List(mapped.into_iter().map(Value::String).collect());
}
}
if tag.name == "ComponentsConfiguration" {
let labels: Vec<String> = tag
.print_value
.split(", ")
.map(|c| {
match c.trim() {
"0" => "-",
"1" => "Y",
"2" => "Cb",
"3" => "Cr",
"4" => "R",
"5" => "G",
"6" => "B",
other => other,
}
.to_string()
})
.collect();
tag.print_value = labels.join(", ");
if let Value::List(_) = tag.raw_value {
tag.raw_value = Value::List(labels.into_iter().map(Value::String).collect());
}
}
if tag.name == "LensInfo" {
let converted: String = tag
.print_value
.split_whitespace()
.map(|tok| match tok.split_once('/') {
Some((n, d)) => {
let nn: f64 = n.parse().unwrap_or(0.0);
let dd: f64 = d.parse().unwrap_or(0.0);
if dd == 0.0 {
if nn == 0.0 {
"undef".to_string()
} else {
"inf".to_string()
}
} else {
crate::value::format_g15(nn / dd)
}
}
None => tok.to_string(),
})
.collect::<Vec<_>>()
.join(" ");
if let Some(formatted) = crate::tags::exif::print_lens_info(&converted) {
tag.print_value = formatted;
}
}
if tag.name == "Prefs" {
let p: Vec<&str> = tag.print_value.split(':').collect();
if p.len() == 4 {
tag.print_value = format!(
"Tagged:{}, ColorClass:{}, Rating:{}, FrameNum:{}",
p[0].trim(),
p[1].trim(),
p[2].trim(),
p[3].trim()
);
}
}
if tag.name.ends_with("FlashReturn") {
tag.print_value = match tag.print_value.trim() {
"0" => "No return detection",
"2" => "Return not detected",
"3" => "Return detected",
other => other,
}
.to_string();
}
if tag.name == "FlashMode" || tag.name.ends_with("FlashMode") {
tag.print_value = match tag.print_value.trim() {
"0" => "Unknown",
"1" => "On",
"2" => "Off",
"3" => "Auto",
other => other,
}
.to_string();
}
if tag.name == "ColorMode" {
tag.print_value = match tag.print_value.trim() {
"0" => "Bitmap",
"1" => "Grayscale",
"2" => "Indexed",
"3" => "RGB",
"4" => "CMYK",
"7" => "Multichannel",
"8" => "Duotone",
"9" => "Lab",
other => other,
}
.to_string();
}
if tag.name == "Urgency" {
tag.print_value = match tag.print_value.trim() {
"0" => "0 (reserved)".to_string(),
"1" => "1 (most urgent)".to_string(),
"5" => "5 (normal urgency)".to_string(),
"8" => "8 (least urgent)".to_string(),
other => other.to_string(),
};
}
if tag.name == "MediaSummaryCode" {
if let Some(expanded) = expand_media_summary_code(&tag.print_value) {
tag.print_value = expanded;
}
}
}
let mut tags = aggregate_duplicate_xmp_tags(tags);
for tag in &mut tags {
if let Some(prefix) = tag.group.family1.strip_prefix("XMP-") {
if let Some(short) = exiftool_namespace_alias(prefix) {
tag.group.family1 = format!("XMP-{short}");
}
}
}
for tag in &mut tags {
let restored = tag
.group
.family1
.strip_prefix("XMP-")
.and_then(|prefix| cur_uri.get(prefix))
.and_then(|uri| exiftool_rdf_groups(uri));
if let Some((group0, group1)) = restored {
tag.group.family0 = group0;
tag.group.family1 = group1;
tag.group.family2 = "Unknown".into();
}
}
Ok(tags)
}
}
fn exiftool_rdf_groups(uri: &str) -> Option<(String, String)> {
let rest = uri
.strip_prefix("http://ns.exiftool.org/")
.or_else(|| uri.strip_prefix("http://ns.exiftool.ca/"))?;
let mut segments = rest.splitn(3, '/');
let group0 = segments.next()?;
let group1 = segments.next()?;
segments.next()?;
if group1 == "System" {
return Some(("XML".to_string(), "XML-System".to_string()));
}
if group1.starts_with(|c: char| c.is_ascii_digit()) {
return Some(("XML".to_string(), format!("XML-{group0}")));
}
Some((group0.to_string(), group1.to_string()))
}
fn xmp_property_is_unknown_aliased(family1: &str, name: &str) -> bool {
let resolved = match family1
.strip_prefix("XMP-")
.and_then(exiftool_namespace_alias)
{
Some(short) => format!("XMP-{short}"),
None => family1.to_string(),
};
crate::tags::group2::xmp_property_is_unknown(&resolved, name)
}
fn exiftool_namespace_alias(prefix: &str) -> Option<&'static str> {
Some(match prefix {
"Iptc4xmpCore" => "iptcCore",
"Iptc4xmpExt" => "iptcExt",
"photomechanic" => "photomech",
"MicrosoftPhoto" => "microsoft",
"prismusagerights" => "pur",
"GettyImagesGIFT" => "getty",
"hdr_metadata" => "hdr",
_ => return None,
})
}
fn aggregate_duplicate_xmp_tags(tags: Vec<Tag>) -> Vec<Tag> {
let mut result: Vec<Tag> = Vec::with_capacity(tags.len());
let mut name_to_idx: std::collections::HashMap<(String, String), usize> =
std::collections::HashMap::new();
for tag in tags {
if !tag.group.family1.starts_with("XMP") {
result.push(tag);
continue;
}
let key = (tag.group.family1.clone(), tag.name.clone());
if let Some(&idx) = name_to_idx.get(&key) {
let prefix = tag.group.family1.strip_prefix("XMP-").unwrap_or("");
if is_known_xmp_prefix(prefix) {
let existing = &mut result[idx];
if existing.print_value != tag.print_value {
match &mut existing.raw_value {
Value::List(items) => items.push(Value::String(tag.print_value.clone())),
_ => {
existing.raw_value = Value::List(vec![
Value::String(existing.print_value.clone()),
Value::String(tag.print_value.clone()),
]);
}
}
existing.print_value = format!("{}, {}", existing.print_value, tag.print_value);
}
} else if crate::metadata::exif::keep_duplicates()
&& !result.iter().any(|t| {
t.name == tag.name
&& t.group.family1 == tag.group.family1
&& t.print_value == tag.print_value
})
{
result.push(tag);
}
} else {
let idx = result.len();
name_to_idx.insert(key, idx);
result.push(tag);
}
}
result
}
fn is_known_xmp_prefix(prefix: &str) -> bool {
matches!(
prefix,
"dc" | "xmp"
| "xmpMM"
| "xmpRights"
| "tiff"
| "exif"
| "aux"
| "photoshop"
| "crs"
| "lr"
| "Iptc4xmpCore"
| "Iptc4xmpExt"
| "GCamera"
| "GImage"
| "GContainer"
| "GContainerItem"
| "GDevice"
| "xmpNote"
| "x"
| "pdf"
| "xmpBJ"
| "stJob"
| "xmpTPg"
| "xmpG"
| "xmpGImg"
| "stDim"
| "stRef"
| "stFnt"
| "stMfs"
| "rdfs"
| "MicrosoftPhoto"
| "plus"
| "stArea"
| "mwg-rs"
| "mwg-kw"
)
}
pub fn flash_numeric_to_string(val: u32) -> String {
match val {
0x00 => "No Flash".into(),
0x01 => "Fired".into(),
0x05 => "Fired, Return not detected".into(),
0x07 => "Fired, Return detected".into(),
0x08 => "On, Did not fire".into(),
0x09 => "On, Fired".into(),
0x0d => "On, Return not detected".into(),
0x0f => "On, Return detected".into(),
0x10 => "Off, Did not fire".into(),
0x14 => "Off, Did not fire, Return not detected".into(),
0x18 => "Auto, Did not fire".into(),
0x19 => "Auto, Fired".into(),
0x1d => "Auto, Fired, Return not detected".into(),
0x1f => "Auto, Fired, Return detected".into(),
0x20 => "No flash function".into(),
0x30 => "Off, No flash function".into(),
0x41 => "Fired, Red-eye reduction".into(),
0x45 => "Fired, Red-eye reduction, Return not detected".into(),
0x47 => "Fired, Red-eye reduction, Return detected".into(),
0x49 => "On, Red-eye reduction".into(),
0x4d => "On, Red-eye reduction, Return not detected".into(),
0x4f => "On, Red-eye reduction, Return detected".into(),
0x50 => "Off, Red-eye reduction".into(),
0x58 => "Auto, Did not fire, Red-eye reduction".into(),
0x59 => "Auto, Fired, Red-eye reduction".into(),
0x5d => "Auto, Fired, Red-eye reduction, Return not detected".into(),
0x5f => "Auto, Fired, Red-eye reduction, Return detected".into(),
_ => format!("Unknown (0x{:02x})", val),
}
}
fn xmp_family1(
path: &[(String, String)],
field_ln: &str,
own_prefix: &str,
cur_ns: &std::collections::HashMap<String, String>,
) -> String {
match struct_root_group_prefix(path, field_ln, cur_ns) {
Some(root) => format!("XMP-{root}"),
None => format!("XMP-{own_prefix}"),
}
}
fn struct_root_group_prefix(
path: &[(String, String)],
exclude_ln: &str,
cur_ns: &std::collections::HashMap<String, String>,
) -> Option<String> {
let rdf_ns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
let end = path
.iter()
.rposition(|(_, ln)| ln == exclude_ln)
.unwrap_or(path.len());
let (ns, _) = path[..end].iter().find(|(ns, ln)| {
ns != rdf_ns
&& ns != "adobe:ns:meta/"
&& !matches!(
ln.as_str(),
"Description" | "RDF" | "xmpmeta" | "xapmeta" | "Seq" | "Bag" | "Alt" | "li"
)
})?;
let group = xmp_group_prefix(ns, cur_ns);
if group == "XMP" {
None
} else {
Some(group)
}
}
fn build_struct_tag_prefix_without_last(path: &[(String, String)], exclude_ln: &str) -> String {
build_struct_tag_prefix_excluding(path, Some(exclude_ln))
}
fn build_struct_tag_prefix_excluding(
path: &[(String, String)],
exclude_last: Option<&str>,
) -> String {
let rdf_ns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
let mut result = String::new();
let effective_path: &[(String, String)] = if let Some(excl) = exclude_last {
let mut end = path.len();
for i in (0..path.len()).rev() {
if path[i].1 == excl {
end = i;
break;
}
}
&path[..end]
} else {
path
};
for (ns, ln) in effective_path {
if ns == rdf_ns
|| ln == "Description"
|| ln == "RDF"
|| ln == "xmpmeta"
|| ln == "xapmeta"
|| ns == "adobe:ns:meta/"
{
continue;
}
if ln == "Seq" || ln == "Bag" || ln == "Alt" || ln == "li" {
continue;
}
let part = ucfirst(&strip_non_ascii(ln));
if result.is_empty() {
result = part;
} else {
let stripped = strip_struct_prefix(&result, &part);
result = format!("{}{}", result, stripped);
}
}
result
}
fn strip_struct_prefix(parent: &str, field: &str) -> String {
if field.starts_with(parent) && field.len() > parent.len() {
let stripped = &field[parent.len()..];
if !stripped.is_empty() {
return stripped.to_string();
}
}
let parent_chars: Vec<char> = parent.chars().collect();
for start in 1..parent_chars.len().saturating_sub(1) {
if !parent_chars[start].is_uppercase() {
continue;
}
let suffix: String = parent_chars[start..].iter().collect();
if field.starts_with(suffix.as_str()) && suffix.len() > 1 {
let stripped = &field[suffix.len()..];
if !stripped.is_empty() {
return stripped.to_string();
}
}
}
field.to_string()
}
fn remap_xmp_tag_name(group_prefix: &str, local_name: &str) -> String {
let clean_name = strip_non_ascii(local_name);
let local_name = clean_name.as_str();
match (group_prefix, local_name) {
("tiff", "ImageLength") => "ImageHeight".into(),
("tiff", "BitsPerSample") => "BitsPerSample".into(),
("exif", "PixelXDimension") => "ExifImageWidth".into(),
("exif", "PixelYDimension") => "ExifImageHeight".into(),
("exif", "ExposureBiasValue") => "ExposureCompensation".into(),
("exif", "ISOSpeedRatings") => "ISO".into(),
("photoshop", "ICCProfile") => "ICCProfileName".into(),
("photoshop", "ColorMode") => "ColorMode".into(),
("plus", "Version") => "PLUSVersion".into(),
_ => {
let has_lowercase = local_name.chars().any(|c| c.is_lowercase());
let has_uppercase = local_name.chars().any(|c| c.is_uppercase());
if has_uppercase && !has_lowercase && local_name.len() > 1 {
ucfirst(&local_name.to_lowercase())
} else {
ucfirst(local_name)
}
}
}
}
fn apply_flat_name_remap(name: &str) -> String {
if let Some(rest) = name.strip_prefix("RegionsRegionListExtensions") {
let remapped_rest = apply_flat_name_remap(rest);
return format!("RegionExtensions{}", remapped_rest);
}
let mapped = match name {
"ArtworkOrObjectAOCopyrightNotice" => "ArtworkCopyrightNotice",
"ArtworkOrObjectAOCreator" => "ArtworkCreator",
"ArtworkOrObjectAODateCreated" => "ArtworkDateCreated",
"ArtworkOrObjectAOSource" => "ArtworkSource",
"ArtworkOrObjectAOSourceInvNo" => "ArtworkSourceInventoryNo",
"ArtworkOrObjectAOTitle" => "ArtworkTitle",
"ArtworkOrObjectAOCurrentCopyrightOwnerName" => "ArtworkCopyrightOwnerName",
"ArtworkOrObjectAOCurrentCopyrightOwnerId" => "ArtworkCopyrightOwnerID",
"ArtworkOrObjectAOCurrentLicensorName" => "ArtworkLicensorName",
"ArtworkOrObjectAOCurrentLicensorId" => "ArtworkLicensorID",
"ArtworkOrObjectAOCreatorId" => "ArtworkCreatorID",
"ArtworkOrObjectAOCircaDateCreated" => "ArtworkCircaDateCreated",
"ArtworkOrObjectAOStylePeriod" => "ArtworkStylePeriod",
"ArtworkOrObjectAOSourceInvURL" => "ArtworkSourceInvURL",
"ArtworkOrObjectAOContentDescription" => "ArtworkContentDescription",
"ArtworkOrObjectAOContributionDescription" => "ArtworkContributionDescription",
"ArtworkOrObjectAOPhysicalDescription" => "ArtworkPhysicalDescription",
"RegionsRegionListName" => "RegionName",
"RegionsRegionListType" => "RegionType",
"RegionsRegionListDescription" => "RegionDescription",
"RegionsRegionListFocusUsage" => "RegionFocusUsage",
"RegionsRegionListBarCodeValue" => "RegionBarCodeValue",
"RegionsRegionListSeeAlso" => "RegionSeeAlso",
"RegionsRegionListRotation" => "RegionRotation",
"RegionsRegionListAreaH" => "RegionAreaH",
"RegionsRegionListAreaW" => "RegionAreaW",
"RegionsRegionListAreaX" => "RegionAreaX",
"RegionsRegionListAreaY" => "RegionAreaY",
"RegionsRegionListAreaD" => "RegionAreaD",
"RegionsRegionListAreaUnit" => "RegionAreaUnit",
"KeywordsHierarchyKeyword" => "HierarchicalKeywords1",
"KeywordsHierarchyChildrenKeyword" => "HierarchicalKeywords2",
"KeywordsHierarchyChildrenChildrenKeyword" => "HierarchicalKeywords3",
"KeywordsHierarchyChildrenChildrenChildrenKeyword" => "HierarchicalKeywords4",
"KeywordsHierarchyChildrenChildrenChildrenChildrenKeyword" => "HierarchicalKeywords5",
"KeywordsHierarchyChildrenChildrenChildrenChildrenChildrenKeyword" => {
"HierarchicalKeywords6"
}
"ColorantsSwatchName" => "ColorantSwatchName",
"ColorantsMode" => "ColorantMode",
"ColorantsType" => "ColorantType",
"ColorantsCyan" => "ColorantCyan",
"ColorantsMagenta" => "ColorantMagenta",
"ColorantsYellow" => "ColorantYellow",
"ColorantsBlack" => "ColorantBlack",
"ColorantsGray" => "ColorantGray",
"ColorantsRed" => "ColorantRed",
"ColorantsGreen" => "ColorantGreen",
"ColorantsBlue" => "ColorantBlue",
"ColorantsL" => "ColorantL",
"ColorantsA" => "ColorantA",
"ColorantsB" => "ColorantB",
"FontsFontName" => "FontName",
"FontsFontFamily" => "FontFamily",
"FontsFontFace" => "FontFace",
"FontsFontType" => "FontType",
"FontsVersionString" => "FontVersion",
"FontsComposite" => "FontComposite",
"FontsFontFileName" => "FontFileName",
"ThumbnailsFormat" => "ThumbnailFormat",
"ThumbnailsWidth" => "ThumbnailWidth",
"ThumbnailsHeight" => "ThumbnailHeight",
"ThumbnailsImage" => "ThumbnailImage",
_ => name,
};
mapped.to_string()
}
fn xmp_rational_decimal(v: &Value) -> Option<String> {
match v {
Value::URational(n, d) if *d != 0 => Some(if *n % *d == 0 {
(*n / *d).to_string()
} else {
crate::value::format_g15(*n as f64 / *d as f64)
}),
Value::IRational(n, d) if *d != 0 => Some(if *n % *d == 0 {
(*n / *d).to_string()
} else {
crate::value::format_g15(*n as f64 / *d as f64)
}),
_ => None,
}
}
fn exif_time(val: &str) -> String {
let mut s: String = val.replace(' ', ":");
while s.ends_with('\0') {
s.pop();
}
let b = s.as_bytes();
let dig = |i: usize| b.get(i).is_some_and(u8::is_ascii_digit);
if b.len() >= 6 && (0..6).all(dig) {
s = format!("{}:{}:{}{}", &s[0..2], &s[2..4], &s[4..6], &s[6..]);
}
let t = s.trim_end();
let b = t.as_bytes();
if b.len() >= 5 {
let i = b.len() - 5;
if (b[i] == b'+' || b[i] == b'-') && b[i + 1..].iter().all(u8::is_ascii_digit) {
s = format!("{}:{}", &t[..i + 3], &t[i + 3..]);
}
}
s
}
fn convert_xmp_rational(val: &str) -> Option<String> {
let (num, den) = val.split_once('/')?;
let parse = |s: &str| -> Option<i64> {
let digits = s.strip_prefix('-').unwrap_or(s);
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
};
let (num, den) = (parse(num)?, parse(den)?);
Some(if den != 0 {
crate::value::format_g15(num as f64 / den as f64)
} else if num != 0 {
"inf".to_string()
} else {
"undef".to_string()
})
}
pub(crate) fn convert_xmp_date(val: &str) -> Option<String> {
let b = val.as_bytes();
let dig = |s: &[u8]| !s.is_empty() && s.iter().all(|c| c.is_ascii_digit());
if b.len() >= 16
&& b[4] == b'-'
&& b[7] == b'-'
&& (b[10] == b'T' || b[10] == b' ')
&& dig(&b[0..4])
&& dig(&b[5..7])
&& dig(&b[8..10])
&& dig(&b[11..13])
&& b[13] == b':'
&& dig(&b[14..16])
{
let date = format!("{}:{}:{}", &val[0..4], &val[5..7], &val[8..10]);
let hhmm = &val[11..16];
let mut idx = 16;
let mut secs = "";
if b.len() >= 19 && b[16] == b':' && dig(&b[17..19]) {
secs = &val[16..19];
idx = 19;
}
let tail = val[idx..].trim_start();
if tail.contains(char::is_whitespace) {
return None;
}
return Some(format!("{} {}{}{}", date, hhmm, secs, tail));
}
let date_only = (val.len() == 7 && b[4] == b'-' && dig(&b[0..4]) && dig(&b[5..7]))
|| (val.len() == 10
&& b[4] == b'-'
&& b[7] == b'-'
&& dig(&b[0..4])
&& dig(&b[5..7])
&& dig(&b[8..10]));
if date_only {
return Some(val.replace('-', ":"));
}
None
}
fn expand_media_summary_code(val: &str) -> Option<String> {
use crate::tags::plus_media_matrix::media_matrix;
let val = val.to_uppercase();
let rest = val.strip_prefix("|PLUS|")?;
let mut parts = rest.splitn(3, '|');
let ver = parts.next()?;
let num = parts.next()?;
let code: String = parts
.next()
.unwrap_or("")
.chars()
.filter(|c| c.is_ascii_digit() || c.is_ascii_uppercase() || *c == '|')
.collect();
let mut ver_s = ver.to_string();
if let Some(caps) = ver
.strip_prefix('V')
.filter(|s| s.chars().all(|c| c.is_ascii_digit()) && s.len() >= 3)
{
let digits = caps.trim_start_matches('0');
if caps.len() >= 2 {
let (maj, min) = caps.split_at(caps.len() - 2);
let maj = maj.trim_start_matches('0');
let maj = if maj.is_empty() { "0" } else { maj };
let _ = digits;
ver_s = format!("{} (LDF Version {}.{})", ver, maj, min);
}
}
let mut num_s = num.to_string();
if let Some(d) = num.strip_prefix('U') {
if d.chars().all(|c| c.is_ascii_digit()) && !d.is_empty() {
let n = d.trim_start_matches('0');
let n = if n.is_empty() { "0" } else { n };
num_s = format!("{} ({} Media Usages:)", num, n);
}
}
let mut out = format!("PLUS {} {}", ver_s, num_s);
let bytes: Vec<char> = code.chars().collect();
let mut i = 0;
while i + 4 <= bytes.len() {
if bytes[i].is_ascii_digit()
&& bytes[i + 1].is_ascii_uppercase()
&& bytes[i + 2].is_ascii_uppercase()
&& bytes[i + 3].is_ascii_uppercase()
{
let mmid: String = bytes[i..i + 4].iter().collect();
if let Some(desc) = media_matrix(&mmid) {
out.push_str(&format!(" {} ({})", mmid, desc));
} else if &mmid[..2] == "1I" {
let c1 = bytes[i + 2] as u32;
let c2 = bytes[i + 3] as u32;
let n = (c1 - 65) * 26 + (c2 - 65) + 1;
out.push_str(&format!("; {} ({} Usage Items:)", mmid, n));
} else if &mmid[..3] == "1UN" {
out.push_str(&format!(" (Usage Number {})", bytes[i + 3]));
} else {
out.push_str(&format!(" {}", mmid));
}
i += 4;
} else {
i += 1;
}
}
Some(out)
}
fn plus_vocab(code: &str) -> Option<&'static str> {
Some(match code {
"AG-A15" => "Age 15",
"AG-A16" => "Age 16",
"AG-A17" => "Age 17",
"AG-A18" => "Age 18",
"AG-A19" => "Age 19",
"AG-A20" => "Age 20",
"AG-A21" => "Age 21",
"AG-A22" => "Age 22",
"AG-A23" => "Age 23",
"AG-A24" => "Age 24",
"AG-A25" => "Age 25 or Over",
"AG-U14" => "Age 14 or Under",
"AG-UNK" => "Age Unknown",
"AL-CLR" => "No Colorization",
"AL-CRP" => "No Cropping",
"AL-DCL" => "No De-Colorization",
"AL-FLP" => "No Flipping",
"AL-MRG" => "No Merging",
"AL-RET" => "No Retouching",
"CR-CAI" => "Credit Adjacent To Image",
"CR-CCA" => "Credit in Credits Area",
"CR-COI" => "Credit on Image",
"CR-NRQ" => "Not Required",
"CS-PRO" => "Protected",
"CS-PUB" => "Public Domain",
"CS-UNK" => "Unknown",
"CW-AWR" => "Adult Content Warning Required",
"CW-NRQ" => "Not Required",
"CW-UNK" => "Unknown",
"DP-LIC" => "Duplication Only as Necessary Under License",
"DP-NDC" => "No Duplication Constraints",
"DP-NOD" => "No Duplication",
"FF-BMP" => "Windows Bitmap (BMP)",
"FF-DNG" => "Digital Negative (DNG)",
"FF-EPS" => "Encapsulated PostScript (EPS)",
"FF-GIF" => "Graphics Interchange Format (GIF)",
"FF-JPG" => "JPEG Interchange Formats (JPG, JIF, JFIF)",
"FF-OTR" => "Other",
"FF-PIC" => "Macintosh Picture (PICT)",
"FF-PNG" => "Portable Network Graphics (PNG)",
"FF-PSD" => "Photoshop Document (PSD)",
"FF-RAW" => "Proprietary RAW Image Format",
"FF-TIF" => "Tagged Image File Format (TIFF)",
"FF-WMP" => "Windows Media Photo (HD Photo)",
"IF-MFN" => "Maintain File Name",
"IF-MFT" => "Maintain File Type",
"IF-MID" => "Maintain ID in File Name",
"IF-MMD" => "Maintain Metadata",
"MR-LMR" => "Limited or Incomplete Model Releases",
"MR-NAP" => "Not Applicable",
"MR-NON" => "None",
"MR-UMR" => "Unlimited Model Releases",
"PR-LPR" => "Limited or Incomplete Property Releases",
"PR-NAP" => "Not Applicable",
"PR-NON" => "None",
"PR-UPR" => "Unlimited Property Releases",
"RE-NAP" => "Not Applicable",
"RE-REU" => "Repeat Use",
"SZ-G50" => "Greater than 50 MB",
"SZ-U01" => "Up to 1 MB",
"SZ-U10" => "Up to 10 MB",
"SZ-U30" => "Up to 30 MB",
"SZ-U50" => "Up to 50 MB",
"TY-ILL" => "Illustrated Image",
"TY-MCI" => "Multimedia or Composited Image",
"TY-OTR" => "Other",
"TY-PHO" => "Photographic Image",
"TY-VID" => "Video",
_ => return None,
})
}
fn xmp_exif_print_override(prefix: &str, name: &str, value: &Value) -> Option<String> {
if prefix != "exif" {
return None;
}
let v = value.as_f64()?;
match name {
"ExposureTime" => Some(crate::tags::canon_sub::print_exposure_time(v)),
"ShutterSpeedValue" => Some(crate::tags::canon_sub::print_exposure_time(2f64.powf(-v))),
_ => None,
}
}
fn parse_xmp_value(text: &str) -> Value {
if let Some(slash) = text.find('/') {
let num_str = &text[..slash];
let den_str = &text[slash + 1..];
if !num_str.is_empty()
&& !den_str.is_empty()
&& !num_str.contains(' ')
&& !den_str.contains(' ')
{
if let (Ok(n), Ok(d)) = (num_str.parse::<i64>(), den_str.parse::<u64>()) {
if d > 0 {
if n >= 0 {
return Value::URational(n as u32, d as u32);
} else {
return Value::IRational(n as i32, d as i32);
}
}
}
}
}
Value::String(text.to_string())
}
fn ucfirst(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
fn strip_non_ascii(s: &str) -> String {
s.chars().filter(|c| c.is_ascii()).collect()
}
fn xml_elem_to_camel(s: &str) -> String {
if s.contains('_') || s.chars().all(|c| c.is_uppercase() || !c.is_alphabetic()) {
let lower = s.to_lowercase();
let mut result = String::with_capacity(lower.len());
let mut capitalize_next = true;
for ch in lower.chars() {
if ch == '_' {
capitalize_next = true;
} else if capitalize_next {
for c in ch.to_uppercase() {
result.push(c);
}
capitalize_next = false;
} else {
result.push(ch);
}
}
result
} else {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
}
fn normalize_xml_text(s: &str) -> String {
let trimmed = s.trim();
if !trimmed.contains('\n') && !trimmed.contains('\r') {
return trimmed.to_string();
}
let mut result = String::with_capacity(trimmed.len());
let mut last_was_space = false;
for c in trimmed.chars() {
if c.is_whitespace() {
if !last_was_space {
result.push(' ');
last_was_space = true;
}
} else {
result.push(c);
last_was_space = false;
}
}
result
}
fn generic_xml_family1(prefixes: &[&str]) -> String {
match prefixes.iter().find(|p| !p.is_empty()) {
Some(p) => format!("XMP-{p}"),
None => "XMP".to_string(),
}
}
fn xml_prefix_of(uri: Option<&str>, literal: Option<&str>) -> String {
let by_uri = namespace_prefix(uri.unwrap_or(""));
if !by_uri.is_empty() {
by_uri.to_string()
} else {
literal.unwrap_or("").to_string()
}
}
fn read_generic_xml(xml: &str) -> Result<Vec<Tag>> {
use xml::reader::{EventReader, XmlEvent};
let mut tags = Vec::new();
let parser = EventReader::from_str(xml);
let mut path: Vec<String> = Vec::new(); let mut prefixes: Vec<String> = Vec::new();
let mut current_text = String::new();
let mut has_children: Vec<bool> = Vec::new();
for event in parser {
match event {
Ok(XmlEvent::StartElement {
name,
attributes,
namespace,
..
}) => {
let local = xml_elem_to_camel(&name.local_name);
let path_str = format!("{}{}", path.join(""), local);
if let Some(last) = has_children.last_mut() {
*last = true;
}
path.push(local.clone());
prefixes.push(xml_prefix_of(
name.namespace.as_deref(),
name.prefix.as_deref(),
));
has_children.push(false);
current_text.clear();
if path.len() == 1 {
if let Some(default_ns) = namespace.get("") {
let tag_name = format!("{}Xmlns", local);
let val = Value::String(default_ns.to_string());
let pv = val.to_display_string();
tags.push(Tag {
id: TagId::Text(format!("XMP:{}", tag_name)),
name: tag_name.clone(),
description: tag_name,
group: TagGroup {
family0: "XMP".into(),
family1: "XMP".into(),
family2: "Other".into(),
family3: "Main".into(),
},
raw_value: val,
print_value: pv,
priority: 0,
});
}
}
for attr in &attributes {
let aname = &attr.name;
if aname.prefix.as_deref() == Some("xmlns")
|| aname.local_name == "xmlns"
|| aname.local_name.starts_with("xmlns:")
{
continue;
}
let attr_local = xml_elem_to_camel(&aname.local_name);
let tag_name = format!("{}{}", path_str, attr_local);
let attr_pfx =
xml_prefix_of(aname.namespace.as_deref(), aname.prefix.as_deref());
let mut chain: Vec<&str> = prefixes.iter().map(String::as_str).collect();
chain.push(attr_pfx.as_str());
let family1 = generic_xml_family1(&chain);
let attr_val: String = attr.value.trim().to_string();
let val = Value::String(attr_val.clone());
let pv = val.to_display_string();
tags.push(Tag {
id: TagId::Text(format!("XMP:{}", tag_name)),
name: tag_name.clone(),
description: tag_name,
group: TagGroup {
family0: "XMP".into(),
family1: family1.clone(),
family2: "Other".into(),
family3: "Main".into(),
},
raw_value: val,
print_value: pv,
priority: 0,
});
}
}
Ok(XmlEvent::Characters(text)) | Ok(XmlEvent::CData(text)) => {
current_text.push_str(&text);
}
Ok(XmlEvent::EndElement { .. }) => {
let text = normalize_xml_text(¤t_text);
let is_leaf = !has_children.last().copied().unwrap_or(false);
if (is_leaf || !text.is_empty()) && !path.is_empty() {
let tag_name = path.join("");
let val = Value::String(text.clone());
let pv = val.to_display_string();
let chain: Vec<&str> = prefixes.iter().map(String::as_str).collect();
let family1 = generic_xml_family1(&chain);
tags.push(Tag {
id: TagId::Text(format!("XMP:{}", tag_name)),
name: tag_name.clone(),
description: tag_name,
group: TagGroup {
family0: "XMP".into(),
family1,
family2: "Other".into(),
family3: "Main".into(),
},
raw_value: val,
print_value: pv,
priority: 0,
});
}
current_text.clear();
has_children.pop();
path.pop();
prefixes.pop();
}
Err(_) => continue,
_ => {}
}
}
for tag in tags.iter_mut() {
if let Some(reformatted) = convert_xmp_date(&tag.print_value) {
tag.print_value = reformatted;
tag.group.family2 = "Time".into();
}
}
Ok(tags)
}
fn fix_malformed_xml(xml: &str) -> String {
let mut stack: Vec<String> = Vec::new();
let mut result = String::with_capacity(xml.len());
let mut pos = 0;
while pos < xml.len() {
if let Some(rel) = xml[pos..].find('<') {
let lt = pos + rel;
result.push_str(&xml[pos..lt]);
pos = lt;
let rest = &xml[pos..];
if rest.starts_with("</") {
if let Some(gt_rel) = rest.find('>') {
let tag_name = rest[2..gt_rel].trim().to_string();
let gt = pos + gt_rel;
if stack.last().map(|s| s == &tag_name).unwrap_or(false) {
stack.pop();
result.push_str(&xml[pos..=gt]);
} else if stack.contains(&tag_name) {
while stack.last().map(|s| s != &tag_name).unwrap_or(false) {
stack.pop();
}
stack.pop();
result.push_str(&xml[pos..=gt]);
}
pos = gt + 1;
} else {
result.push('<');
pos += 1;
}
} else if rest.starts_with("<!") || rest.starts_with("<?") {
let end = if rest.starts_with("<!--") {
rest.find("-->").map(|e| pos + e + 3)
} else if rest.starts_with("<![CDATA[") {
rest.find("]]>").map(|e| pos + e + 3)
} else {
rest.find("?>").map(|e| pos + e + 2)
};
if let Some(end_pos) = end {
result.push_str(&xml[pos..end_pos]);
pos = end_pos;
} else {
result.push('<');
pos += 1;
}
} else {
if let Some(gt_rel) = rest.find('>') {
let gt = pos + gt_rel;
let inner = rest[1..gt_rel].trim();
let is_self_closing = inner.ends_with('/');
if !is_self_closing {
let tag_name = inner
.split(|c: char| c.is_whitespace() || c == '/')
.next()
.unwrap_or("")
.to_string();
if !tag_name.is_empty() {
stack.push(tag_name);
}
}
result.push_str(&xml[pos..=gt]);
pos = gt + 1;
} else {
result.push('<');
pos += 1;
}
}
} else {
result.push_str(&xml[pos..]);
break;
}
}
result
}
fn sanitize_xmp_xml(xml: &str) -> String {
let mut result = String::with_capacity(xml.len());
let mut in_pi = false; let chars: Vec<char> = xml.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if !in_pi && i + 1 < chars.len() && c == '<' && chars[i + 1] == '?' {
in_pi = true;
result.push(c);
} else if in_pi && c == '?' && i + 1 < chars.len() && chars[i + 1] == '>' {
in_pi = false;
result.push(c);
result.push(chars[i + 1]);
i += 2;
continue;
} else if in_pi {
if c == '\t'
|| c == '\n'
|| c == '\r'
|| (c as u32 >= 0x20 && c as u32 <= 0xD7FF)
|| (c as u32 >= 0xE000 && c as u32 <= 0xFFFD)
|| (c as u32 >= 0x10000 && c as u32 <= 0x10FFFF)
{
result.push(c);
} else {
result.push(' ');
}
} else {
result.push(c);
}
i += 1;
}
result
}
fn chars_start_with(chars: &[char], pat: &str) -> bool {
let p: Vec<char> = pat.chars().collect();
chars.len() >= p.len() && chars[..p.len()] == p[..]
}
fn copy_through(chars: &[char], i: usize, end: &str, out: &mut String) -> usize {
let e: Vec<char> = end.chars().collect();
let mut j = i;
let stop = loop {
if j + e.len() > chars.len() {
break chars.len();
}
if chars[j..j + e.len()] == e[..] {
break j + e.len();
}
j += 1;
};
out.extend(&chars[i..stop]);
stop - i
}
fn escape_attribute_whitespace(xml: &str) -> String {
let mut out = String::with_capacity(xml.len());
let chars: Vec<char> = xml.chars().collect();
let mut i = 0;
let mut quote: Option<char> = None; let mut in_tag = false;
while i < chars.len() {
let c = chars[i];
if let Some(q) = quote {
if c == q {
quote = None;
out.push(c);
} else {
match c {
'\r' => out.push_str(" "),
'\n' => out.push_str(" "),
'\t' => out.push_str("	"),
_ => out.push(c),
}
}
i += 1;
continue;
}
if in_tag {
match c {
'"' | '\'' => quote = Some(c),
'>' => in_tag = false,
_ => {}
}
out.push(c);
i += 1;
continue;
}
if c == '<' {
let verbatim = [("<!--", "-->"), ("<![CDATA[", "]]>"), ("<?", "?>")]
.into_iter()
.find(|(open, _)| chars_start_with(&chars[i..], open));
if let Some((_, end)) = verbatim {
i += copy_through(&chars, i, end, &mut out);
continue;
}
in_tag = true;
}
out.push(c);
i += 1;
}
out
}
fn collect_node_bag_values(xml: &str) -> std::collections::HashMap<String, Vec<String>> {
use xml::reader::{EventReader, XmlEvent};
let mut map: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
let parser = EventReader::from_str(xml);
let mut current_node_id: Option<String> = None;
let mut current_items: Vec<String> = Vec::new();
let mut in_li = false;
let mut current_text = String::new();
let rdf_ns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
for event in parser {
match event {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
current_text.clear();
let local = &name.local_name;
let ns = name.namespace.as_deref().unwrap_or("");
if (local == "Bag" || local == "Seq" || local == "Alt") && ns == rdf_ns {
if let Some(nid) = attributes.iter().find(|a| {
a.name.local_name == "nodeID"
&& (a.name.prefix.as_deref() == Some("rdf") || ns == rdf_ns)
}) {
current_node_id = Some(nid.value.clone());
current_items.clear();
}
} else if local == "li" && ns == rdf_ns && current_node_id.is_some() {
in_li = true;
}
}
Ok(XmlEvent::Characters(text)) | Ok(XmlEvent::CData(text)) => {
if in_li {
current_text.push_str(&text);
}
}
Ok(XmlEvent::EndElement { name }) => {
let local = &name.local_name;
let ns = name.namespace.as_deref().unwrap_or("");
if local == "li" && ns == rdf_ns && in_li {
let val = normalize_xml_text(¤t_text);
if !val.is_empty() {
current_items.push(val);
}
in_li = false;
current_text.clear();
} else if (local == "Bag" || local == "Seq" || local == "Alt") && ns == rdf_ns {
if let Some(nid) = current_node_id.take() {
map.insert(nid, std::mem::take(&mut current_items));
}
}
}
Err(_) => continue,
_ => {}
}
}
map
}
fn collect_inline_referenced_node_ids(xml: &str) -> std::collections::HashSet<String> {
use xml::reader::{EventReader, XmlEvent};
let mut set: std::collections::HashSet<String> = std::collections::HashSet::new();
let parser = EventReader::from_str(xml);
let rdf_ns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
let mut path: Vec<(String, String)> = Vec::new();
for event in parser {
match event {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
let local = name.local_name.clone();
let ns = name.namespace.as_deref().unwrap_or("").to_string();
if local == "Description" && ns == rdf_ns {
if let Some(nid_attr) = attributes.iter().find(|a| {
a.name.local_name == "nodeID"
&& (a.name.prefix.as_deref() == Some("rdf")
|| a.name.namespace.as_deref() == Some(rdf_ns))
}) {
let parent_is_top_level = path
.last()
.map(|(pns, pln)| {
(pln == "RDF" && pns == rdf_ns)
|| pln == "xmpmeta"
|| pln == "xapmeta"
})
.unwrap_or(true);
if !parent_is_top_level {
set.insert(nid_attr.value.clone());
}
}
}
path.push((ns, local));
}
Ok(XmlEvent::EndElement { .. }) => {
path.pop();
}
Err(_) => continue,
_ => {}
}
}
set
}
fn collect_blank_node_properties(
xml: &str,
) -> std::collections::HashMap<String, Vec<(String, String, String)>> {
use xml::reader::{EventReader, XmlEvent};
let mut map: std::collections::HashMap<String, Vec<(String, String, String)>> =
std::collections::HashMap::new();
let parser = EventReader::from_str(xml);
let mut current_node_id: Option<String> = None;
let mut current_text = String::new();
let mut in_property = false;
let mut current_prop_ns = String::new();
let mut current_prop_local = String::new();
let rdf_ns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
for event in parser {
match event {
Ok(XmlEvent::StartElement {
name, attributes, ..
}) => {
current_text.clear();
let local = &name.local_name;
let ns = name.namespace.as_deref().unwrap_or("");
if local == "Description" && ns == rdf_ns {
if let Some(nid_attr) = attributes.iter().find(|a| {
a.name.local_name == "nodeID"
&& (a.name.prefix.as_deref() == Some("rdf")
|| a.name.namespace.as_deref() == Some(rdf_ns))
}) {
let nid = nid_attr.value.clone();
current_node_id = Some(nid.clone());
let entry = map.entry(nid).or_default();
for attr in &attributes {
if attr.name.local_name == "nodeID" || attr.name.local_name == "about" {
continue;
}
if attr.name.prefix.as_deref() == Some("xmlns") {
continue;
}
if attr.name.local_name.starts_with("xmlns") {
continue;
}
if attr.name.prefix.as_deref() == Some("rdf")
|| attr.name.namespace.as_deref() == Some(rdf_ns)
{
continue;
}
let attr_ns = attr.name.namespace.as_deref().unwrap_or("").to_string();
entry.push((attr_ns, attr.name.local_name.clone(), attr.value.clone()));
}
}
} else if current_node_id.is_some() && ns != rdf_ns && local != "RDF" {
in_property = true;
current_prop_ns = ns.to_string();
current_prop_local = local.clone();
if let Some(res_attr) = attributes.iter().find(|a| {
a.name.local_name == "resource"
&& a.name.namespace.as_deref() == Some(rdf_ns)
}) {
let nid = current_node_id.as_ref().unwrap().clone();
let entry = map.entry(nid).or_default();
entry.push((ns.to_string(), local.clone(), res_attr.value.clone()));
in_property = false; }
}
}
Ok(XmlEvent::Characters(text)) | Ok(XmlEvent::CData(text)) => {
if in_property {
current_text.push_str(&text);
}
}
Ok(XmlEvent::EndElement { name }) => {
let local = &name.local_name;
let ns = name.namespace.as_deref().unwrap_or("");
if local == "Description" && ns == rdf_ns {
current_node_id = None;
in_property = false;
} else if in_property {
let text = normalize_xml_text(¤t_text);
if let Some(nid) = ¤t_node_id {
let entry = map.entry(nid.clone()).or_default();
entry.push((current_prop_ns.clone(), current_prop_local.clone(), text));
}
in_property = false;
current_text.clear();
}
}
Err(_) => continue,
_ => {}
}
}
map
}
#[cfg(test)]
mod attribute_whitespace_tests {
use super::escape_attribute_whitespace as esc;
#[test]
fn literal_whitespace_in_an_attribute_becomes_a_character_reference() {
assert_eq!(esc("<a v=\"x\ry\"/>"), "<a v=\"x y\"/>");
assert_eq!(esc("<a v=\"x\n y\"/>"), "<a v=\"x y\"/>");
assert_eq!(esc("<a v=\"x\ty\"/>"), "<a v=\"x	y\"/>");
assert_eq!(esc("<a v='x\ry'/>"), "<a v='x y'/>");
}
#[test]
fn markup_and_text_are_left_alone() {
assert_eq!(
esc("<a\n v=\"1\">\ntext\n</a>"),
"<a\n v=\"1\">\ntext\n</a>"
);
}
#[test]
fn cdata_comments_and_pis_are_copied_through() {
for doc in [
"<a><![CDATA[x\ry]]></a>",
"<a><!-- x\ry --></a>",
"<?xpacket begin=\"\" id=\"x\ry\"?><a/>",
] {
assert_eq!(esc(doc), doc, "rewrote {doc:?}");
}
}
#[test]
fn unterminated_markup_does_not_lose_input() {
assert_eq!(esc("<a><!-- never closed"), "<a><!-- never closed");
assert_eq!(esc("<a v=\"unclosed\r"), "<a v=\"unclosed ");
}
#[test]
fn the_value_survives_a_round_trip_through_the_parser() {
use xml::reader::{EventReader, XmlEvent};
let doc = esc("<a v=\"http://x/1 http://y/2\n http://z/3\"/>");
let value = EventReader::from_str(&doc)
.into_iter()
.find_map(|e| match e {
Ok(XmlEvent::StartElement { attributes, .. }) => Some(attributes[0].value.clone()),
_ => None,
})
.expect("start element");
assert_eq!(value, "http://x/1 http://y/2\n http://z/3");
}
}