c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

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 }