xmp_inmemory_utils.rs (10318B)
1 // Copyright 2022 Adobe. All rights reserved. 2 // This file is licensed to you under the Apache License, 3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) 4 // or the MIT license (http://opensource.org/licenses/MIT), 5 // at your option. 6 7 // Unless required by applicable law or agreed to in writing, 8 // this software is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or 10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the 11 // specific language governing permissions and limitations under 12 // each license. 13 14 use std::io::Cursor; 15 16 use fast_xml::{ 17 events::{BytesStart, Event}, 18 Reader, Writer, 19 }; 20 use tracing::error; 21 22 use crate::{ 23 asset_io::CAIRead, jumbf_io::get_cailoader_handler, utils::hash_utils::vec_compare, Error, 24 Result, 25 }; 26 27 const RDF_DESCRIPTION: &[u8] = b"rdf:Description"; 28 29 pub const MIN_XMP: &str = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 6.0.0"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" > </rdf:Description></rdf:RDF> </x:xmpmeta> "#; 30 31 #[derive(Default)] 32 pub struct XmpInfo { 33 pub document_id: Option<String>, 34 pub instance_id: Option<String>, 35 pub provenance: Option<String>, 36 } 37 38 impl XmpInfo { 39 /// search xmp data for provenance, documentID and instanceID 40 pub fn from_source(source: &mut dyn CAIRead, format: &str) -> Self { 41 let xmp = get_cailoader_handler(format).and_then(|cai_loader| { 42 // read xmp if available 43 cai_loader.read_xmp(source) 44 }); 45 46 // todo: do this in one pass through XMP 47 let provenance = xmp.as_deref().and_then(extract_provenance); 48 let document_id = xmp.as_deref().and_then(extract_document_id); 49 let instance_id = xmp.as_deref().and_then(extract_instance_id); 50 Self { 51 document_id, 52 instance_id, 53 provenance, 54 } 55 } 56 } 57 58 /// Extract an a value from XMP using a key 59 fn extract_xmp_key(xmp: &str, key: &str) -> Option<String> { 60 let mut reader = Reader::from_str(xmp); 61 reader.trim_text(true); 62 let mut buf = Vec::new(); 63 64 loop { 65 match reader.read_event(&mut buf) { 66 Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { 67 if e.name() == RDF_DESCRIPTION { 68 // attribute case 69 let value = e.attributes().find(|a| { 70 if let Ok(attribute) = a { 71 vec_compare(attribute.key, key.as_bytes()) 72 } else { 73 false 74 } 75 }); 76 if let Some(Ok(attribute)) = value { 77 if let Ok(s) = String::from_utf8(attribute.value.to_vec()) { 78 return Some(s); 79 } 80 } 81 } else if e.name() == key.as_bytes() { 82 // tag case 83 let mut buf: Vec<u8> = Vec::new(); 84 if let Ok(s) = reader.read_text(e.name(), &mut buf) { 85 return Some(s); 86 } 87 } 88 } 89 Ok(Event::Eof) => break, 90 _ => {} 91 } 92 buf.clear(); 93 } 94 None 95 } 96 97 // writes the event to the writer) 98 /// Add a value to XMP using a key, replaces the value if the key exists 99 fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result<String> { 100 let mut reader = Reader::from_str(xmp); 101 reader.trim_text(true); 102 let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2); 103 let mut buf = Vec::new(); 104 let mut added = false; 105 loop { 106 let event = reader 107 .read_event(&mut buf) 108 .map_err(|e| Error::XmpReadError(e.to_string()))?; 109 // println!("{:?}", event); 110 match event { 111 Event::Start(ref e) if e.name() == RDF_DESCRIPTION => { 112 // creates a new element 113 let mut elem = BytesStart::owned(RDF_DESCRIPTION.to_vec(), RDF_DESCRIPTION.len()); 114 115 for attr in e.attributes() { 116 match attr { 117 Ok(attr) => { 118 if attr.key == key.as_bytes() { 119 // replace the key/value if it exists 120 elem.push_attribute((key, value)); 121 added = true; 122 } else { 123 // add all other existing elements 124 elem.extend_attributes([attr]); 125 } 126 } 127 Err(e) => { 128 error!("Error at position {}", reader.buffer_position()); 129 return Err(Error::XmpReadError(e.to_string())); 130 } 131 } 132 } 133 if !added { 134 // didn't exist, so add it 135 elem.push_attribute((key, value)); 136 } 137 // writes the event to the writer 138 writer 139 .write_event(Event::Start(elem)) 140 .map_err(|e| Error::XmpWriteError(e.to_string()))?; 141 } 142 Event::Empty(ref e) if e.name() == RDF_DESCRIPTION => { 143 // creates a new element 144 let mut elem = BytesStart::owned(RDF_DESCRIPTION.to_vec(), RDF_DESCRIPTION.len()); 145 for attr in e.attributes() { 146 match attr { 147 Ok(attr) => { 148 if attr.key == key.as_bytes() { 149 // replace the key/value if it exists 150 elem.push_attribute((key, value)); 151 added = true; 152 } else { 153 // add all other existing elements 154 elem.extend_attributes([attr]); 155 } 156 } 157 Err(e) => { 158 error!("Error at position {}", reader.buffer_position()); 159 return Err(Error::XmpReadError(e.to_string())); 160 } 161 } 162 } 163 if !added { 164 // didn't exist, so add it 165 elem.push_attribute((key, value)); 166 } 167 // writes the event to the writer 168 writer 169 .write_event(Event::Empty(elem)) 170 .map_err(|e| Error::XmpWriteError(e.to_string()))?; 171 } 172 Event::Eof => break, 173 e => { 174 writer 175 .write_event(e) 176 .map_err(|e| Error::XmpWriteError(e.to_string()))?; 177 } 178 } 179 } 180 buf.clear(); 181 let result = writer.into_inner().into_inner(); 182 String::from_utf8(result).map_err(|e| Error::XmpWriteError(e.to_string())) 183 } 184 185 /// extract the dc:provenance value from xmp 186 pub fn extract_provenance(xmp: &str) -> Option<String> { 187 extract_xmp_key(xmp, "dcterms:provenance") 188 } 189 190 /// extract the xmpMM:InstanceID value from xmp 191 fn extract_instance_id(xmp: &str) -> Option<String> { 192 extract_xmp_key(xmp, "xmpMM:InstanceID") 193 } 194 195 /// extract the "xmpMM:DocumentID" value from xmp 196 fn extract_document_id(xmp: &str) -> Option<String> { 197 extract_xmp_key(xmp, "xmpMM:DocumentID") 198 } 199 200 /// add or replace a dc:provenance value to xmp, including dc:terms if needed 201 pub fn add_provenance(xmp: &str, provenance: &str) -> Result<String> { 202 let xmp = add_xmp_key(xmp, "xmlns:dcterms", "http://purl.org/dc/terms/")?; 203 add_xmp_key(&xmp, "dcterms:provenance", provenance) 204 } 205 206 #[cfg(test)] 207 mod tests { 208 #![allow(clippy::expect_used)] 209 #![allow(clippy::unwrap_used)] 210 211 //use env_logger; 212 use super::*; 213 214 const XMP_DATA: &str = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> 215 <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="contentauth"> 216 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> 217 <rdf:Description rdf:about="" 218 xmlns:xmp="http://ns.adobe.com/xap/1.0/" 219 xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" 220 xmlns:dc="http://purl.org/dc/elements/1.1/" 221 xmlns:dcterms="http://purl.org/dc/terms/" 222 xmpMM:DocumentID="xmp.did:cb9f5498-bb58-4572-8043-8c369e6bfb9b" 223 xmpMM:InstanceID="xmp.iid:cb9f5498-bb58-4572-8043-8c369e6bfb9b" 224 dcterms:provenance="self#jumbf=c2pa/contentauth:urn:uuid:a58065fb-79ae-4eb3-87b9-a19830860059/c2pa.claim" 225 dc:format="image/jpeg"> 226 </rdf:Description> 227 </rdf:RDF> 228 </x:xmpmeta>"#; 229 230 const PROVENANCE: &str = 231 "self#jumbf=c2pa/contentauth:urn:uuid:a58065fb-79ae-4eb3-87b9-a19830860059/c2pa.claim"; 232 233 #[test] 234 fn read_xmp() { 235 let provenance = extract_provenance(XMP_DATA); 236 assert_eq!(provenance, Some("self#jumbf=c2pa/contentauth:urn:uuid:a58065fb-79ae-4eb3-87b9-a19830860059/c2pa.claim".to_owned())); 237 let document_id = extract_document_id(XMP_DATA); 238 assert_eq!( 239 document_id, 240 Some("xmp.did:cb9f5498-bb58-4572-8043-8c369e6bfb9b".to_owned()) 241 ); 242 let instance_id = extract_instance_id(XMP_DATA); 243 assert_eq!( 244 instance_id, 245 Some("xmp.iid:cb9f5498-bb58-4572-8043-8c369e6bfb9b".to_owned()) 246 ); 247 let unicorn = extract_xmp_key(XMP_DATA, "unicorn"); 248 assert_eq!(unicorn, None); 249 let bad_xmp = extract_xmp_key("bad xmp", "unicorn"); 250 assert_eq!(bad_xmp, None); 251 } 252 253 #[test] 254 fn add_xmp() { 255 let xmp = add_provenance(XMP_DATA, PROVENANCE).expect("adding provenance"); 256 let unicorn = extract_provenance(&xmp); 257 println!("{xmp}"); 258 assert_eq!(unicorn, Some(PROVENANCE.to_string())); 259 260 let xmp = add_provenance(MIN_XMP, PROVENANCE).expect("adding provenance"); 261 let unicorn = extract_provenance(&xmp); 262 println!("{xmp}"); 263 assert_eq!(unicorn, Some(PROVENANCE.to_string())); 264 } 265 }