extern crate std;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Clone, Debug)]
pub(crate) struct XmpPacket {
pub xml: String,
pub offset: usize,
}
pub(crate) fn extract_xmp_packets(data: &[u8]) -> Vec<XmpPacket> {
let mut packets = Vec::new();
let begin_marker = b"<?xpacket begin";
let end_marker = b"<?xpacket end";
let mut search_from = 0;
while search_from < data.len() {
let Some(start) = find_bytes(&data[search_from..], begin_marker) else {
break;
};
let abs_start = search_from + start;
let Some(end_marker_pos) = find_bytes(&data[abs_start..], end_marker) else {
break;
};
let abs_end_marker = abs_start + end_marker_pos;
let remaining = &data[abs_end_marker..];
let Some(close_pos) = find_bytes(remaining, b"?>") else {
break;
};
let abs_end = abs_end_marker + close_pos + 2;
if let Ok(xml) = core::str::from_utf8(&data[abs_start..abs_end]) {
packets.push(XmpPacket {
xml: String::from(xml),
offset: abs_start,
});
}
search_from = abs_end;
}
packets
}
pub fn extract_xmp(data: &[u8]) -> Option<String> {
let packets = extract_xmp_packets(data);
if let Some(first) = packets.into_iter().next() {
return Some(first.xml);
}
extract_xmpmeta_block(data)
}
fn extract_xmpmeta_block(data: &[u8]) -> Option<String> {
let open = b"<x:xmpmeta";
let close = b"</x:xmpmeta>";
let start = find_bytes(data, open)?;
let after_open = start;
let end_pos = find_bytes(&data[after_open..], close)?;
let abs_end = after_open + end_pos + close.len();
core::str::from_utf8(&data[start..abs_end])
.ok()
.map(String::from)
}
pub(crate) fn get_xmp_property(xmp_xml: &str, ns_prefix: &str, name: &str) -> Option<String> {
let attr_pattern = format!("{ns_prefix}:{name}=\"");
if let Some(pos) = xmp_xml.find(&attr_pattern) {
let value_start = pos + attr_pattern.len();
if let Some(end) = xmp_xml[value_start..].find('"') {
return Some(String::from(&xmp_xml[value_start..value_start + end]));
}
}
let open_tag = format!("<{ns_prefix}:{name}>");
let close_tag = format!("</{ns_prefix}:{name}>");
if let Some(open_pos) = xmp_xml.find(&open_tag) {
let value_start = open_pos + open_tag.len();
if let Some(close_pos) = xmp_xml[value_start..].find(&close_tag) {
let value = &xmp_xml[value_start..value_start + close_pos];
let trimmed = value.trim();
if !trimmed.contains('<') {
return Some(String::from(trimmed));
}
}
}
None
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct XmpMetadata {
pub raw_xml: Option<String>,
pub creator: Option<String>,
pub description: Option<String>,
pub rights: Option<String>,
pub title: Option<String>,
pub rating: Option<i32>,
pub label: Option<String>,
pub create_date: Option<String>,
pub modify_date: Option<String>,
pub creator_tool: Option<String>,
pub color_mode: Option<String>,
pub white_balance: Option<String>,
pub temperature: Option<i32>,
pub tint: Option<i32>,
pub exposure: Option<String>,
pub contrast: Option<String>,
pub shadows: Option<String>,
pub highlights: Option<String>,
pub saturation: Option<String>,
pub sharpness: Option<String>,
pub exif_image_width: Option<u32>,
pub exif_image_height: Option<u32>,
pub tiff_make: Option<String>,
pub tiff_model: Option<String>,
pub tiff_orientation: Option<u16>,
}
pub fn read_xmp_metadata(data: &[u8]) -> Option<XmpMetadata> {
let xml = extract_xmp(data)?;
let mut m = XmpMetadata {
raw_xml: Some(xml.clone()),
..Default::default()
};
m.creator = get_xmp_property(&xml, "dc", "creator");
m.description = get_xmp_property(&xml, "dc", "description");
m.rights = get_xmp_property(&xml, "dc", "rights");
m.title = get_xmp_property(&xml, "dc", "title");
m.rating = get_xmp_property(&xml, "xmp", "Rating").and_then(|s| s.parse().ok());
m.label = get_xmp_property(&xml, "xmp", "Label");
m.create_date = get_xmp_property(&xml, "xmp", "CreateDate");
m.modify_date = get_xmp_property(&xml, "xmp", "ModifyDate");
m.creator_tool = get_xmp_property(&xml, "xmp", "CreatorTool");
m.color_mode = get_xmp_property(&xml, "photoshop", "ColorMode");
m.white_balance = get_xmp_property(&xml, "crs", "WhiteBalance");
m.temperature = get_xmp_property(&xml, "crs", "Temperature").and_then(|s| s.parse().ok());
m.tint = get_xmp_property(&xml, "crs", "Tint").and_then(|s| s.parse().ok());
m.exposure = get_xmp_property(&xml, "crs", "Exposure2012")
.or_else(|| get_xmp_property(&xml, "crs", "Exposure"));
m.contrast = get_xmp_property(&xml, "crs", "Contrast2012")
.or_else(|| get_xmp_property(&xml, "crs", "Contrast"));
m.shadows = get_xmp_property(&xml, "crs", "Shadows2012")
.or_else(|| get_xmp_property(&xml, "crs", "Shadows"));
m.highlights = get_xmp_property(&xml, "crs", "Highlights2012")
.or_else(|| get_xmp_property(&xml, "crs", "Highlights"));
m.saturation = get_xmp_property(&xml, "crs", "Saturation");
m.sharpness = get_xmp_property(&xml, "crs", "Sharpness");
m.exif_image_width =
get_xmp_property(&xml, "exif", "PixelXDimension").and_then(|s| s.parse().ok());
m.exif_image_height =
get_xmp_property(&xml, "exif", "PixelYDimension").and_then(|s| s.parse().ok());
m.tiff_make = get_xmp_property(&xml, "tiff", "Make");
m.tiff_model = get_xmp_property(&xml, "tiff", "Model");
m.tiff_orientation = get_xmp_property(&xml, "tiff", "Orientation").and_then(|s| s.parse().ok());
Some(m)
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || needle.len() > haystack.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_xmp_from_dng() {
let dirs = ["/mnt/v/input/fivek/dng/"];
for dir in &dirs {
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
for entry in entries.filter_map(|e| e.ok()).take(3) {
let path = entry.path();
if !path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("dng"))
{
continue;
}
let data = std::fs::read(&path).unwrap();
let packets = extract_xmp_packets(&data);
let name = path.file_name().unwrap().to_str().unwrap();
eprintln!("{name}: found {} XMP packet(s)", packets.len());
assert!(!packets.is_empty(), "DNG should contain XMP: {name}");
for (i, pkt) in packets.iter().enumerate() {
eprintln!(" Packet {i}: offset={}, len={}", pkt.offset, pkt.xml.len());
assert!(pkt.xml.contains("<?xpacket begin"));
assert!(pkt.xml.contains("<?xpacket end"));
}
let meta = read_xmp_metadata(&data);
assert!(meta.is_some(), "should parse XMP metadata from {name}");
let meta = meta.unwrap();
eprintln!(" tiff:Make = {:?}", meta.tiff_make);
eprintln!(" tiff:Model = {:?}", meta.tiff_model);
eprintln!(" xmp:CreatorTool = {:?}", meta.creator_tool);
eprintln!(" crs:WhiteBalance = {:?}", meta.white_balance);
eprintln!(" xmp:Rating = {:?}", meta.rating);
return; }
}
eprintln!("Skipping: no DNG files found for XMP test");
}
#[test]
fn extract_xmp_property_attribute_form() {
let xmp = r#"<?xpacket begin="..." ?><x:xmpmeta><rdf:RDF>
<rdf:Description xmp:Rating="5" xmp:Label="Red" tiff:Make="Nikon" />
</rdf:RDF></x:xmpmeta><?xpacket end="w"?>"#;
assert_eq!(get_xmp_property(xmp, "xmp", "Rating"), Some("5".into()));
assert_eq!(get_xmp_property(xmp, "xmp", "Label"), Some("Red".into()));
assert_eq!(get_xmp_property(xmp, "tiff", "Make"), Some("Nikon".into()));
assert_eq!(get_xmp_property(xmp, "xmp", "Missing"), None);
}
#[test]
fn extract_xmp_property_element_form() {
let xmp = r#"<?xpacket begin="..." ?>
<x:xmpmeta>
<rdf:RDF>
<rdf:Description>
<xmp:CreatorTool>Adobe Camera Raw 9.0</xmp:CreatorTool>
<dc:description>Test photo</dc:description>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>"#;
assert_eq!(
get_xmp_property(xmp, "xmp", "CreatorTool"),
Some("Adobe Camera Raw 9.0".into())
);
assert_eq!(
get_xmp_property(xmp, "dc", "description"),
Some("Test photo".into())
);
}
#[test]
fn no_xmp_returns_none() {
let data = b"This is not a RAW file";
assert!(extract_xmp(data).is_none());
assert!(extract_xmp_packets(data).is_empty());
}
#[test]
fn xmp_from_raw_samples() {
let dir = "/mnt/v/input/raw-samples/";
let Ok(entries) = std::fs::read_dir(dir) else {
eprintln!("Skipping: raw-samples not found");
return;
};
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let name = path.file_name().unwrap().to_str().unwrap().to_string();
let Ok(data) = std::fs::read(&path) else {
continue;
};
let packets = extract_xmp_packets(&data);
if packets.is_empty() {
eprintln!("{name}: no XMP found");
} else {
eprintln!("{name}: {} XMP packet(s)", packets.len());
if let Some(meta) = read_xmp_metadata(&data) {
eprintln!(" Make={:?} Model={:?}", meta.tiff_make, meta.tiff_model);
}
}
}
}
}