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

commit b3e85c70b61f511afca9be871a7f6a477e2a9e03
parent cde7927e9b29220fefc906101e0d420db9a84c0a
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Wed, 27 Mar 2024 11:10:39 -0700

Implements embed_reference_to_stream for jpeg (#430)

* Implements embed_reference_to_stream for jpeg
This works without xmp_toolkit

* Share XMP_MIN and add stream based wasm test

* remove debug print
Diffstat:
Msdk/src/asset_handlers/jpeg_io.rs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msdk/src/utils/xmp_inmemory_utils.rs | 2+-
2 files changed, 87 insertions(+), 5 deletions(-)

diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs @@ -37,6 +37,7 @@ use crate::{ RemoteRefEmbedType, }, error::{Error, Result}, + utils::xmp_inmemory_utils::{add_provenance, MIN_XMP}, }; static SUPPORTED_TYPES: [&str; 3] = ["jpg", "jpeg", "image/jpeg"]; @@ -574,11 +575,52 @@ impl RemoteRefEmbed for JpegIO { fn embed_reference_to_stream( &self, - _source_stream: &mut dyn CAIRead, - _output_stream: &mut dyn CAIReadWrite, - _embed_ref: RemoteRefEmbedType, + source_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, + embed_ref: RemoteRefEmbedType, ) -> Result<()> { - Err(Error::UnsupportedType) + match embed_ref { + crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { + let mut buf = Vec::new(); + // read the whole asset + source_stream.rewind()?; + source_stream + .read_to_end(&mut buf) + .map_err(Error::IoError)?; + let mut jpeg = + Jpeg::from_bytes(buf.into()).map_err(|_err| Error::EmbeddingError)?; + + // first extract the xmp from APP1 markers + let app1_segs = jpeg.segments_by_marker(markers::APP1); + let mut xmp: String = app1_segs.filter_map(extract_xmp).collect(); + + // remove existing XMP segments + let segments = jpeg.segments_mut(); + segments.retain(|seg| { + !(seg.marker() == markers::APP1 && seg.contents().starts_with(XMP_SIGNATURE)) + }); + + if xmp.is_empty() { + // Init with minimal xmp segment + // JPEG APP1 segment with XMP are defined with this null terminated string + // todo: add format and other minimal metadata to xmp ? + xmp = format!("http://ns.adobe.com/xap/1.0/\0 {}", MIN_XMP); + }; + let xmp = add_provenance(&xmp, &manifest_uri)?; + let xmp_bytes = Bytes::from(xmp); + let segment = JpegSegment::new_with_contents(markers::APP1, xmp_bytes); + segments.insert(1, segment); + + output_stream.rewind()?; + jpeg.encoder() + .write_to(output_stream) + .map_err(|_err| Error::InvalidAsset("JPEG write error".to_owned()))?; + Ok(()) + } + crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType), + crate::asset_io::RemoteRefEmbedType::StegoB(_) => Err(Error::UnsupportedType), + crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType), + } } } @@ -1035,6 +1077,9 @@ pub mod tests { use std::io::{Read, Seek}; + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::*; + use super::*; #[test] fn test_extract_xmp() { @@ -1129,6 +1174,43 @@ pub mod tests { assert!(read_xmp.contains(test_msg)); } + #[cfg_attr(not(target_arch = "wasm32"), actix::test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + async fn test_xmp_read_write_stream() { + let source_bytes = include_bytes!("../../tests/fixtures/CA.jpg"); + + let test_msg = "this some test xmp data"; + 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()); + 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)); + } + #[test] fn test_embeddable_manifest() { let jpeg_io = JpegIO {}; diff --git a/sdk/src/utils/xmp_inmemory_utils.rs b/sdk/src/utils/xmp_inmemory_utils.rs @@ -26,7 +26,7 @@ use crate::{ const RDF_DESCRIPTION: &[u8] = b"rdf:Description"; -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> "#; +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> "#; #[derive(Default)] pub struct XmpInfo {