diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs
index 9155a8ad8..c23a180b2 100644
--- a/sdk/src/asset_handlers/jpeg_io.rs
+++ b/sdk/src/asset_handlers/jpeg_io.rs
@@ -620,7 +620,7 @@ impl RemoteRefEmbed for JpegIO {
.unwrap_or((None, MIN_XMP));
// add provenance and JPEG XMP prefix
- let xmp = format!("{XMP_SIGNATURE}\0 {}", add_provenance(xmp, &manifest_uri)?);
+ let xmp = format!("{XMP_SIGNATURE}\0{}", add_provenance(xmp, &manifest_uri)?);
let segment = JpegSegment::new_with_contents(markers::APP1, Bytes::from(xmp));
// insert or add the segment
match xmp_index {
diff --git a/sdk/src/utils/xmp_inmemory_utils.rs b/sdk/src/utils/xmp_inmemory_utils.rs
index e1addf7c5..44f4ae8c3 100644
--- a/sdk/src/utils/xmp_inmemory_utils.rs
+++ b/sdk/src/utils/xmp_inmemory_utils.rs
@@ -23,6 +23,7 @@ use quick_xml::{
use crate::{asset_io::CAIRead, jumbf_io::get_cailoader_handler, Error, Result};
const RDF_DESCRIPTION: &[u8] = b"rdf:Description";
+const XMP_END: &[u8] = b"";
pub const MIN_XMP: &str = r#" "#;
@@ -90,11 +91,30 @@ fn extract_xmp_key(xmp: &str, key: &str) -> Option {
}
// writes the event to the writer)
-/// Add a value to XMP using a key, replaces the value if the key exists
+/// Add a value to XMP using a key, replaces the value if the key exists, or adds it only once
fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result {
- let mut reader = Reader::from_str(xmp);
- reader.config_mut().trim_text(true);
- let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2);
+ let orig_length = xmp.len();
+
+ // Minimal padding should be 2 KB to 4 KB. This is used if no XMP packet end is found.
+ // If no packet end is found and the original length is less than 4096, the packet will be
+ // extended.
+ let mut target_length = orig_length.max(4096);
+ // Remove the ending xpacket if present for easier manipulation
+ let xpacket_end = " Result {
.map_err(|e| Error::XmpReadError(e.to_string()))?;
// println!("{:?}", event);
match event {
- Event::Start(ref e) if e.name() == QName(RDF_DESCRIPTION) => {
+ Event::Start(ref e) if e.name() == QName(RDF_DESCRIPTION) && !added => {
// creates a new element
let mut elem = BytesStart::from_content(
String::from_utf8_lossy(RDF_DESCRIPTION),
@@ -130,13 +150,14 @@ fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result {
if !added {
// didn't exist, so add it
elem.push_attribute((key, value));
+ added = true;
}
// writes the event to the writer
writer
.write_event(Event::Start(elem))
.map_err(|e| Error::XmpWriteError(e.to_string()))?;
}
- Event::Empty(ref e) if e.name() == QName(RDF_DESCRIPTION) => {
+ Event::Empty(ref e) if e.name() == QName(RDF_DESCRIPTION) && !added => {
// creates a new element
let mut elem = BytesStart::from_content(
String::from_utf8_lossy(RDF_DESCRIPTION),
@@ -163,6 +184,7 @@ fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result {
if !added {
// didn't exist, so add it
elem.push_attribute((key, value));
+ added = true;
}
// writes the event to the writer
writer
@@ -177,6 +199,9 @@ fn add_xmp_key(xmp: &str, key: &str, value: &str) -> Result {
}
}
}
+ // Maintain XMP packet length with padding if possible.
+ let padding_length = target_length.saturating_sub(writer.get_ref().get_ref().len());
+ write_xmp_padding(&mut writer.get_mut(), padding_length)?;
let result = writer.into_inner().into_inner();
String::from_utf8(result).map_err(|e| Error::XmpWriteError(e.to_string()))
}
@@ -202,13 +227,46 @@ pub fn add_provenance(xmp: &str, provenance: &str) -> Result {
add_xmp_key(&xmp, "dcterms:provenance", provenance)
}
+// According to the XMP Spec, the recommended practice is to use the ASCII space
+// character (U+0020) in the appropriate encoding, with a newline about every 100 characters
+fn write_xmp_padding(writer: &mut W, len: usize) -> std::io::Result<()> {
+ let mut remaining = len;
+ // Add newline before trailer no matter what.
+ writer.write_all(b"\n")?;
+ remaining = remaining.saturating_sub(1);
+
+ if remaining > 0 {
+ // Account for final newline since we are adding whitespace.
+ remaining -= 1;
+ while remaining > 0 {
+ let chunk = remaining.min(99);
+ writer.write_all(&vec![b' '; chunk])?;
+ remaining -= chunk;
+ if remaining > 0 {
+ writer.write_all(b"\n")?;
+ remaining -= 1;
+ }
+ }
+ writer.write_all(b"\n")?;
+ }
+ writer.write_all(XMP_END)?;
+ Ok(())
+}
+
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
//use env_logger;
+ #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
+ use wasm_bindgen_test::wasm_bindgen_test;
+
use super::*;
+ use crate::{
+ asset_handlers::jpeg_io::JpegIO,
+ asset_io::{AssetIO, RemoteRefEmbedType},
+ };
const XMP_DATA: &str = r#"
@@ -261,4 +319,54 @@ mod tests {
println!("{xmp}");
assert_eq!(unicorn, Some(PROVENANCE.to_string()));
}
+
+ #[cfg_attr(not(target_arch = "wasm32"), actix::test)]
+ #[cfg_attr(
+ all(target_arch = "wasm32", not(target_os = "wasi")),
+ wasm_bindgen_test
+ )]
+ #[cfg_attr(target_os = "wasi", wstd::test)]
+ async fn test_broken_xmp_read_write_stream() {
+ let source_bytes = include_bytes!("../../tests/fixtures/IMG_0003.jpg");
+ let test_msg = "https://cai-manifests-stage.adobe.com/manifests/urn-c2pa-0ab6e8b8-5c28-4ef1-8f58-86c21f0349bf-adobe";
+
+ let handler = JpegIO::new("");
+
+ let assetio_handler = handler.get_handler("jpg");
+
+ let remote_ref_handler = assetio_handler.remote_ref_writer_ref().unwrap();
+
+ let mut source_stream = Cursor::new(source_bytes.to_vec());
+ let mut output_stream = Cursor::new(Vec::new());
+
+ let original_xmp_length = assetio_handler
+ .get_reader()
+ .read_xmp(&mut source_stream)
+ .unwrap()
+ .len();
+ source_stream.set_position(0);
+
+ remote_ref_handler
+ .embed_reference_to_stream(
+ &mut source_stream,
+ &mut output_stream,
+ RemoteRefEmbedType::Xmp(test_msg.to_string()),
+ )
+ .unwrap();
+
+ output_stream.set_position(0);
+
+ // read back in XMP
+ let read_xmp = assetio_handler
+ .get_reader()
+ .read_xmp(&mut output_stream)
+ .unwrap();
+
+ output_stream.set_position(0);
+
+ //std::fs::write("../target/xmp_write.jpg", output_stream.into_inner()).unwrap();
+
+ assert!(read_xmp.contains(test_msg));
+ assert_eq!(read_xmp.len(), original_xmp_length);
+ }
}