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 8f555302c2be26f258b3b9019cdd01f88056e447
parent c3eb198d4e11f9f6cf2e2d7fc6574ef5598cf091
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Thu, 18 May 2023 09:40:19 -0400

Riff streaming support (#248)

* Initial RIFF implementation

* AVI test

* webp support

* Disable XMP default writting

* Fix broken hashing

* Refactor asset handlers to control supported features

* Fix clippy errors

* Moved XMP generation to the asset handlers

* Remove XMP stuff from store since it is now performed by handlders

* XMP restrict some file types
BMFF generation bug for remote manifests

* Code cleanup

* Update version

* format issues

* more formatting fixes

* cleanup

* Bump major number since new error codes are available

* Fix PR comments

* Minor cleanup

* Support HEIC, HEIF, & AVIF

* PR comment fixes

* Add comment for future enhancement

* Streaming writes

* Clippy cleanup

* Add XMP generation for WEBP

* Fix for typo

* Unit test fixes/changes

* cargo formatting

* More formatting

* format again

* PR fixes

* Changes based on PR comments
Diffstat:
Msdk/src/asset_handlers/jpeg_io.rs | 7+++----
Msdk/src/asset_handlers/png_io.rs | 7+++----
Msdk/src/asset_handlers/riff_io.rs | 579+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msdk/src/asset_io.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdk/src/ingredient.rs | 2+-
Msdk/src/jumbf_io.rs | 1-
Msdk/src/store.rs | 14++++++++------
Msdk/src/utils/xmp_inmemory_utils.rs | 11+++--------
Asdk/tests/fixtures/test.webp | 0
Asdk/tests/fixtures/test_lossless.webp | 0
Asdk/tests/fixtures/test_xmp.webp | 0
11 files changed, 566 insertions(+), 108 deletions(-)

diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs @@ -93,11 +93,10 @@ fn add_required_segs_to_stream( let aio = JpegIO {}; aio.write_cai(input_stream, output_stream, &no_bytes)?; } else { - // just move input to output - let mut buf: Vec<u8> = Vec::new(); + // just clone input_stream.rewind()?; - input_stream.read_to_end(&mut buf).map_err(Error::IoError)?; - output_stream.write_all(&buf)?; + output_stream.rewind()?; + std::io::copy(input_stream, output_stream)?; } } else { return Err(Error::UnsupportedType); diff --git a/sdk/src/asset_handlers/png_io.rs b/sdk/src/asset_handlers/png_io.rs @@ -159,11 +159,10 @@ fn add_required_chunks_to_stream( let aio = PngIO {}; aio.write_cai(input_stream, output_stream, &no_bytes)?; } else { - // just move input to output - let mut buf: Vec<u8> = Vec::new(); + // just clone input_stream.rewind()?; - input_stream.read_to_end(&mut buf).map_err(Error::IoError)?; - output_stream.write_all(&buf)?; + output_stream.rewind()?; + std::io::copy(input_stream, output_stream)?; } } else { return Err(Error::UnsupportedType); diff --git a/sdk/src/asset_handlers/riff_io.rs b/sdk/src/asset_handlers/riff_io.rs @@ -17,16 +17,17 @@ use std::{ path::Path, }; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use conv::ValueFrom; use riff::*; use crate::{ asset_io::{ - AssetIO, AssetPatch, CAIRead, CAIReadWrite, CAIReader, HashBlockObjectType, - HashObjectPositions, RemoteRefEmbed, RemoteRefEmbedType, + AssetIO, AssetPatch, CAIRead, CAIReadWrapper, CAIReadWrite, CAIReadWriteWrapper, CAIReader, + CAIWriter, HashBlockObjectType, HashObjectPositions, RemoteRefEmbed, RemoteRefEmbedType, }, error::{Error, Result}, - jumbf_io::get_file_extension, + utils::xmp_inmemory_utils::{add_provenance, MIN_XMP}, }; static SUPPORTED_TYPES: [&str; 9] = [ @@ -50,6 +51,24 @@ const C2PA_CHUNK_ID: ChunkId = ChunkId { value: [0x43, 0x32, 0x50, 0x41], }; // C2PA +const VP8X_ID: ChunkId = ChunkId { + value: [0x56, 0x50, 0x38, 0x58], +}; // VP8X chunk to hold auxiliary info + +const VP8_ID: ChunkId = ChunkId { + value: [0x56, 0x50, 0x38, 0x20], +}; // VP8 chunk + +const VP8L_ID: ChunkId = ChunkId { + value: [0x56, 0x50, 0x38, 0x4c], +}; // VP8L chunk + +const XMP_CHUNK_ID: ChunkId = ChunkId { + value: [0x58, 0x4d, 0x50, 0x20], +}; // XMP + +const XMP_FLAG: u32 = 4; + fn read_items<T>(iter: &mut T) -> Vec<T::Item> where T: Iterator, @@ -61,54 +80,159 @@ where vec } -fn inject_c2pa<T>(chunk: &Chunk, file: &mut T, data: &[u8], format: &str) -> Result<ChunkContents> +fn get_height_and_width(chunk_contents: &[ChunkContents]) -> Result<(u16, u16)> { + if let Some(ChunkContents::Data(_id, chunk_data)) = chunk_contents.iter().find(|c| match c { + ChunkContents::Data(id, _) => *id == VP8L_ID, + _ => false, + }) { + let mut chunk_stream = Cursor::new(chunk_data); + chunk_stream.seek(SeekFrom::Start(1))?; // skip signature byte + + // width and length are 12 bits packed together + let first_bytes = chunk_stream.read_u16::<LittleEndian>()?; + let width = 1 + (first_bytes & 0x3fff); // add 1 for VP8L + let last_two = (first_bytes & 0xc000) >> 14; // last two bits of first bytes are first 2 of height + let height = 1 + (((chunk_stream.read_u16::<LittleEndian>()? & 0xfff) << 2) | last_two); + + return Ok((height, width)); + } + + if let Some(ChunkContents::Data(_id, chunk_data)) = chunk_contents.iter().find(|c| match c { + ChunkContents::Data(id, _) => *id == VP8_ID, + _ => false, + }) { + let mut chunk_stream = Cursor::new(chunk_data); + chunk_stream.seek(SeekFrom::Start(6))?; // skip frame tag and start code + + let width = chunk_stream.read_u16::<LittleEndian>()? & 0x3fff; + let height = chunk_stream.read_u16::<LittleEndian>()? & 0x3fff; + + return Ok((height, width)); + } + + Err(Error::InvalidAsset( + "WEBP missing VP8 or VP8L segment".to_string(), + )) +} + +fn inject_c2pa<T>( + chunk: &Chunk, + stream: &mut T, + data: &[u8], + xmp_data: Option<&[u8]>, + format: &str, +) -> Result<ChunkContents> where T: std::io::Seek + std::io::Read, { let id = chunk.id(); let is_riff_chunk: bool = id == riff::RIFF_ID; + stream.rewind()?; if is_riff_chunk || id == riff::LIST_ID { - let chunk_type = chunk.read_type(file).map_err(|_| { + let chunk_type = chunk.read_type(stream).map_err(|_| { Error::InvalidAsset("RIFF handler could not parse file format {format}".to_string()) })?; - let mut children = read_items(&mut chunk.iter(file)); + let mut children = read_items(&mut chunk.iter(stream)); let mut children_contents: Vec<ChunkContents> = Vec::new(); - if is_riff_chunk { + if is_riff_chunk && !data.is_empty() { // remove c2pa manifest store in RIFF chunk children.retain(|c| c.id() != C2PA_CHUNK_ID); } - // for non webp we can place at the front - // add c2pa manifest - if is_riff_chunk && !data.is_empty() && !format.contains("webp") { - children_contents.push(ChunkContents::Data(C2PA_CHUNK_ID, data.to_vec())); + if is_riff_chunk && xmp_data.is_some() { + // remove XMP in RIFF chunk so we can replace + children.retain(|c| c.id() != XMP_CHUNK_ID); } + // duplicate all top level children for child in children { - children_contents.push(inject_c2pa(&child, file, data, format)?); + children_contents.push(inject_c2pa(&child, stream, data, xmp_data, format)?); + } + + // add XMP if needed + if let Some(xmp) = xmp_data { + if is_riff_chunk && !xmp.is_empty() { + // if this is a webp doc we must also update VP8X + if format == "webp" { + // if already present we can patch otherwise add + if let Some(ChunkContents::Data(_id, chunk_data)) = + children_contents.iter_mut().find(|c| match c { + ChunkContents::Data(id, _) => *id == VP8X_ID, + _ => false, + }) + { + let mut chunk_stream = Cursor::new(chunk_data); + + let mut flags = chunk_stream.read_u32::<LittleEndian>()?; + + // add in XMP flag + flags |= XMP_FLAG; + + chunk_stream.rewind()?; + + // write back changes + chunk_stream.write_u32::<LittleEndian>(flags)?; + } else { + // add new VP8X + + // get height and width from VBL + if let Ok((height, width)) = get_height_and_width(&children_contents) { + let data: Vec<u8> = Vec::new(); + let mut chunk_writer = Cursor::new(data); + + let flags: u32 = XMP_FLAG; + let vp8x_height = height as u32 - 1; + let vp8x_width = width as u32 - 1; + + // write flags + chunk_writer.write_u32::<LittleEndian>(flags)?; + + // write width then height + chunk_writer.write_u24::<LittleEndian>(vp8x_width)?; + chunk_writer.write_u24::<LittleEndian>(vp8x_height)?; + + // make new VP8X chunk and prepend to children list + let mut tmp_vec: Vec<ChunkContents> = Vec::new(); + tmp_vec.push(ChunkContents::Data(VP8X_ID, chunk_writer.into_inner())); + tmp_vec.extend(children_contents); + children_contents = tmp_vec; + } else { + return Err(Error::InvalidAsset( + "Could not parse VP8 or VP8L".to_string(), + )); + } + } + } + + children_contents.push(ChunkContents::Data(XMP_CHUNK_ID, xmp.to_vec())); + } } - // for non webp we can place at the front - // add c2pa manifest - if is_riff_chunk && !data.is_empty() && format.contains("webp") { - children_contents.push(ChunkContents::Data(C2PA_CHUNK_ID, data.to_vec())); + // place at the end for maximum compatibility + if is_riff_chunk && !data.is_empty() { + let mut d = data.to_vec(); + if d.len() % 2 == 1 { + // must be even + d.push(0); + } + children_contents.push(ChunkContents::Data(C2PA_CHUNK_ID, d)); } Ok(ChunkContents::Children(id, chunk_type, children_contents)) } else if id == riff::SEQT_ID { - let children = read_items(&mut chunk.iter_no_type(file)); + let children = read_items(&mut chunk.iter_no_type(stream)); let mut children_contents: Vec<ChunkContents> = Vec::new(); for child in children { - children_contents.push(inject_c2pa(&child, file, data, format)?); + children_contents.push(inject_c2pa(&child, stream, data, xmp_data, format)?); } Ok(ChunkContents::ChildrenNoType(id, children_contents)) } else { let contents = chunk - .read_contents(file) + .read_contents(stream) .map_err(|_| Error::InvalidAsset("RIFF handler could not parse file".to_string()))?; Ok(ChunkContents::Data(id, contents)) } @@ -134,12 +258,10 @@ fn get_manifest_pos(reader: &mut dyn CAIRead) -> Option<(u64, u32)> { } impl CAIReader for RiffIO { - fn read_cai(&self, reader: &mut dyn CAIRead) -> Result<Vec<u8>> { - let mut asset: Vec<u8> = Vec::new(); - reader.rewind()?; - reader.read_to_end(&mut asset)?; - - let mut chunk_reader = Cursor::new(asset); + fn read_cai(&self, input_stream: &mut dyn CAIRead) -> Result<Vec<u8>> { + let mut chunk_reader = CAIReadWrapper { + reader: input_stream, + }; let top_level_chunks = riff::Chunk::read(&mut chunk_reader, 0)?; @@ -149,7 +271,13 @@ impl CAIReader for RiffIO { for c in top_level_chunks.iter(&mut chunk_reader) { if c.id() == C2PA_CHUNK_ID { - let output = c.read_contents(&mut chunk_reader)?; + let mut output = c.read_contents(&mut chunk_reader)?; + // the data may have been padded to account for even boundary requirement + if let Some(last_byte) = output.last() { + if *last_byte == 0 { + output.pop(); + } + } return Ok(output); } } @@ -158,18 +286,54 @@ impl CAIReader for RiffIO { } // Get XMP block - fn read_xmp(&self, _asset_reader: &mut dyn CAIRead) -> Option<String> { - None // todo: figure out where XMP is stored for supported formats + fn read_xmp(&self, input_stream: &mut dyn CAIRead) -> Option<String> { + let top_level_chunks = { + let mut reader = CAIReadWrapper { + reader: input_stream, + }; + Chunk::read(&mut reader, 0).ok()? + }; + + if top_level_chunks.id() != RIFF_ID { + return None; + } + + let mut chunk_reader = CAIReadWrapper { + reader: input_stream, + }; + + for c in top_level_chunks.iter(&mut chunk_reader) { + if c.id() == XMP_CHUNK_ID { + let output = c.read_contents(&mut chunk_reader).ok()?; + let output_string = String::from_utf8_lossy(&output); + + return Some(output_string.to_string()); + } + } + + None } } -fn add_required_chunks(asset_path: &std::path::Path) -> Result<()> { - let mut f = File::open(asset_path)?; - let aio = RiffIO::new(&get_file_extension(asset_path).ok_or(Error::UnsupportedType)?); - - match aio.read_cai(&mut f) { - Ok(_) => Ok(()), - Err(_) => aio.save_cai_store(asset_path, &[1, 2, 3, 4]), // save arbitrary data +fn add_required_chunks( + asset_type: &str, + input_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, +) -> Result<()> { + let aio = RiffIO::new(asset_type); + + match aio.read_cai(input_stream) { + Ok(_) => { + // just clone + input_stream.rewind()?; + output_stream.rewind()?; + std::io::copy(input_stream, output_stream)?; + Ok(()) + } + Err(_) => { + input_stream.rewind()?; + aio.write_cai(input_stream, output_stream, &[1, 2, 3, 4]) // save arbitrary data + } } } @@ -188,6 +352,10 @@ impl AssetIO for RiffIO { self } + fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> { + Some(Box::new(RiffIO::new(asset_type))) + } + fn asset_patch_ref(&self) -> Option<&dyn AssetPatch> { Some(self) } @@ -198,46 +366,94 @@ impl AssetIO for RiffIO { } fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> { - let asset = std::fs::read(asset_path)?; - let mut chunk_reader = Cursor::new(asset); + let mut input_stream = File::open(asset_path)?; + + let mut output_stream = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(asset_path) + .map_err(Error::IoError)?; + + self.write_cai(&mut input_stream, &mut output_stream, store_bytes) + } + + fn get_object_locations( + &self, + asset_path: &std::path::Path, + ) -> Result<Vec<HashObjectPositions>> { + let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?; + + self.get_object_locations_from_stream(&mut f) + } + + fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { + self.save_cai_store(asset_path, &[]) + } + + fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { + Some(self) + } - let top_level_chunks = Chunk::read(&mut chunk_reader, 0)?; + fn supported_types(&self) -> &[&str] { + &SUPPORTED_TYPES + } +} + +impl CAIWriter for RiffIO { + fn write_cai( + &self, + input_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, + store_bytes: &[u8], + ) -> Result<()> { + let top_level_chunks = { + let mut reader = CAIReadWrapper { + reader: input_stream, + }; + Chunk::read(&mut reader, 0)? + }; if top_level_chunks.id() != RIFF_ID { return Err(Error::InvalidAsset("Invalid RIFF format".to_string())); } + let mut reader = CAIReadWrapper { + reader: input_stream, + }; + // replace/add manifest in memory let new_contents = inject_c2pa( &top_level_chunks, - &mut chunk_reader, + &mut reader, store_bytes, + None, &self.riff_format, )?; + let mut writer = CAIReadWriteWrapper { + reader_writer: output_stream, + }; + // save contents - let mut output = OpenOptions::new() - .read(true) - .write(true) - .open(asset_path) - .map_err(Error::IoError)?; - match new_contents.write(&mut output) { - Ok(_) => Ok(()), - Err(e) => Err(Error::IoError(e)), - } + new_contents + .write(&mut writer) + .map_err(|_e| Error::EmbeddingError)?; + Ok(()) } - fn get_object_locations( + fn get_object_locations_from_stream( &self, - asset_path: &std::path::Path, + input_stream: &mut dyn CAIRead, ) -> Result<Vec<HashObjectPositions>> { - add_required_chunks(asset_path)?; + let output_buf: Vec<u8> = Vec::new(); + let mut output_stream = Cursor::new(output_buf); - let mut f = std::fs::File::open(asset_path).map_err(|_err| Error::EmbeddingError)?; + add_required_chunks(&self.riff_format, input_stream, &mut output_stream)?; let mut positions: Vec<HashObjectPositions> = Vec::new(); - let (manifest_pos, manifest_len) = get_manifest_pos(&mut f).ok_or(Error::EmbeddingError)?; + let (manifest_pos, manifest_len) = + get_manifest_pos(&mut output_stream).ok_or(Error::EmbeddingError)?; positions.push(HashObjectPositions { offset: usize::value_from(manifest_pos) @@ -260,7 +476,7 @@ impl AssetIO for RiffIO { .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))? + u64::value_from(manifest_len) .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?; - let file_end = f.metadata()?.len(); + let file_end = output_stream.seek(SeekFrom::End(0))?; positions.push(HashObjectPositions { offset: usize::value_from(end) .map_err(|_err| Error::InvalidAsset("value out of range".to_string()))?, // len of cai @@ -272,16 +488,12 @@ impl AssetIO for RiffIO { Ok(positions) } - fn remove_cai_store(&self, asset_path: &Path) -> Result<()> { - self.save_cai_store(asset_path, &[]) - } - - fn remote_ref_writer_ref(&self) -> Option<&dyn RemoteRefEmbed> { - Some(self) - } - - fn supported_types(&self) -> &[&str] { - &SUPPORTED_TYPES + fn remove_cai_store_from_stream( + &self, + input_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, + ) -> Result<()> { + self.write_cai(input_stream, output_stream, &[]) } } @@ -315,21 +527,109 @@ impl RemoteRefEmbed for RiffIO { asset_path: &Path, embed_ref: crate::asset_io::RemoteRefEmbedType, ) -> Result<()> { + let mut input_stream = File::open(asset_path)?; + + let mut output_stream = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(asset_path) + .map_err(Error::IoError)?; + + self.embed_reference_to_stream(&mut input_stream, &mut output_stream, embed_ref) + } + + fn embed_reference_to_stream( + &self, + input_stream: &mut dyn CAIRead, + output_stream: &mut dyn CAIReadWrite, + embed_ref: RemoteRefEmbedType, + ) -> Result<()> { match embed_ref { crate::asset_io::RemoteRefEmbedType::Xmp(manifest_uri) => { - #[cfg(feature = "xmp_write")] - { - match self.riff_format.as_ref() { - "avi" | "wav" => { - crate::embedded_xmp::add_manifest_uri_to_file(asset_path, &manifest_uri) + match self.riff_format.as_ref() { + "avi" | "wav" | "webp" => { + if let Some(curr_xmp) = self.read_xmp(input_stream) { + let mut new_xmp = add_provenance(&curr_xmp, &manifest_uri)?; + if new_xmp.len() % 2 == 1 { + // pad if needed to even length + new_xmp.push(' '); + } + + let top_level_chunks = { + let mut reader = CAIReadWrapper { + reader: input_stream, + }; + Chunk::read(&mut reader, 0)? + }; + + if top_level_chunks.id() != RIFF_ID { + return Err(Error::InvalidAsset("Invalid RIFF format".to_string())); + } + + let mut reader = CAIReadWrapper { + reader: input_stream, + }; + + // replace/add manifest in memory + let new_contents = inject_c2pa( + &top_level_chunks, + &mut reader, + &[], + Some(new_xmp.as_bytes()), + &self.riff_format, + )?; + + // save contents + let mut writer = CAIReadWriteWrapper { + reader_writer: output_stream, + }; + new_contents + .write(&mut writer) + .map_err(|_e| Error::EmbeddingError)?; + Ok(()) + } else { + let mut new_xmp = add_provenance(MIN_XMP, &manifest_uri)?; + + if new_xmp.len() % 2 == 1 { + // pad if needed to even length + new_xmp.push(' '); + } + + let top_level_chunks = { + let mut reader = CAIReadWrapper { + reader: input_stream, + }; + Chunk::read(&mut reader, 0)? + }; + + if top_level_chunks.id() != RIFF_ID { + return Err(Error::InvalidAsset("Invalid RIFF format".to_string())); + } + + let mut reader = CAIReadWrapper { + reader: input_stream, + }; + + // replace/add manifest in memory + let new_contents = inject_c2pa( + &top_level_chunks, + &mut reader, + &[], + Some(new_xmp.as_bytes()), + &self.riff_format, + )?; + + // save contents + let mut writer = CAIReadWriteWrapper { + reader_writer: output_stream, + }; + new_contents + .write(&mut writer) + .map_err(|_e| Error::EmbeddingError)?; + Ok(()) } - _ => Err(Error::XmpNotSupported), } - } - - #[cfg(not(feature = "xmp_write"))] - { - Err(crate::error::Error::MissingFeature("xmp_write".to_string())) + _ => Err(Error::UnsupportedType), } } crate::asset_io::RemoteRefEmbedType::StegoS(_) => Err(Error::UnsupportedType), @@ -337,15 +637,6 @@ impl RemoteRefEmbed for RiffIO { crate::asset_io::RemoteRefEmbedType::Watermark(_) => Err(Error::UnsupportedType), } } - - fn embed_reference_to_stream( - &self, - _source_stream: &mut dyn CAIRead, - _output_stream: &mut dyn CAIReadWrite, - _embed_ref: RemoteRefEmbedType, - ) -> Result<()> { - Err(Error::UnsupportedType) - } } #[cfg(test)] @@ -360,6 +651,7 @@ pub mod tests { use crate::utils::{ hash_utils::vec_compare, test::{fixture_path, temp_dir_path}, + xmp_inmemory_utils::extract_provenance, }; #[test] @@ -434,4 +726,123 @@ pub mod tests { _ => unreachable!(), } } + + #[test] + fn test_read_xmp() { + let source = fixture_path("test_xmp.webp"); + let mut reader = std::fs::File::open(source).unwrap(); + + let riff_io = RiffIO::new("webp"); + + let xmp = riff_io.read_xmp(&mut reader).unwrap(); + println!("XMP: {xmp}"); + } + + #[test] + fn test_write_xmp() { + let more_data = "some more test data"; + let source = fixture_path("test_xmp.webp"); + + let mut success = false; + if let Ok(temp_dir) = tempdir() { + let output = temp_dir_path(&temp_dir, "test_xmp.webp"); + + std::fs::copy(source, &output).unwrap(); + + let riff_io = RiffIO::new("webp"); + + if let Some(embed_handler) = riff_io.remote_ref_writer_ref() { + if let Ok(()) = embed_handler.embed_reference( + output.as_path(), + RemoteRefEmbedType::Xmp(more_data.to_string()), + ) { + let mut output_stream = std::fs::File::open(&output).unwrap(); + + // check the xmp + if let Some(xmp) = riff_io.read_xmp(&mut output_stream) { + println!("XMP: {xmp}"); + + if let Some(xmp_val) = extract_provenance(&xmp) { + if xmp_val == more_data { + success = true; + } + } + } + } + } + } + assert!(success) + } + + #[test] + fn test_insert_xmp() { + let more_data = "some more test data"; + let source = fixture_path("test.webp"); + + let mut success = false; + if let Ok(temp_dir) = tempdir() { + let output = temp_dir_path(&temp_dir, "test.webp"); + + std::fs::copy(source, &output).unwrap(); + + let riff_io = RiffIO::new("webp"); + + if let Some(embed_handler) = riff_io.remote_ref_writer_ref() { + if let Ok(()) = embed_handler.embed_reference( + output.as_path(), + RemoteRefEmbedType::Xmp(more_data.to_string()), + ) { + let mut output_stream = std::fs::File::open(&output).unwrap(); + + // check the xmp + if let Some(xmp) = riff_io.read_xmp(&mut output_stream) { + println!("XMP: {xmp}"); + + if let Some(xmp_val) = extract_provenance(&xmp) { + if xmp_val == more_data { + success = true; + } + } + } + } + } + } + assert!(success) + } + + #[test] + fn test_insert_xmp_lossless() { + let more_data = "some more test data"; + let source = fixture_path("test_lossless.webp"); + + let mut success = false; + if let Ok(temp_dir) = tempdir() { + let output = temp_dir_path(&temp_dir, "test_lossless.webp"); + + std::fs::copy(source, &output).unwrap(); + + let riff_io = RiffIO::new("webp"); + + if let Some(embed_handler) = riff_io.remote_ref_writer_ref() { + if let Ok(()) = embed_handler.embed_reference( + output.as_path(), + RemoteRefEmbedType::Xmp(more_data.to_string()), + ) { + let mut output_stream = std::fs::File::open(&output).unwrap(); + + // check the xmp + if let Some(xmp) = riff_io.read_xmp(&mut output_stream) { + println!("XMP: {xmp}"); + + if let Some(xmp_val) = extract_provenance(&xmp) { + if xmp_val == more_data { + success = true; + } + } + } + } + } + } + assert!(success) + } } diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs @@ -48,6 +48,27 @@ impl CAIRead for std::io::Cursor<&mut [u8]> {} impl CAIRead for std::io::Cursor<Vec<u8>> {} impl CAIRead for NamedTempFile {} +// Helper struct to create a concrete type for CAIRead when +// that is required. For example a function defined like this +// pub fn read<T>(&self, reader: &mut T) cannot currently accept +// a CAIRead trait because it is not Sized (bound to a object). +// This will likely change in a future version of Rust. +pub(crate) struct CAIReadWrapper<'a> { + pub reader: &'a mut dyn CAIRead, +} + +impl Read for CAIReadWrapper<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + self.reader.read(buf) + } +} + +impl Seek for CAIReadWrapper<'_> { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { + self.reader.seek(pos) + } +} + pub trait CAIReadWrite: CAIRead + Write {} impl CAIReadWrite for std::fs::File {} @@ -55,6 +76,38 @@ impl CAIReadWrite for std::io::Cursor<&mut [u8]> {} impl CAIReadWrite for std::io::Cursor<Vec<u8>> {} impl CAIReadWrite for NamedTempFile {} +// Helper struct to create a concrete type for CAIReadWrite when +// that is required. For example a function defined like this +// pub fn write<T>(&self, writer: &mut T) cannot currently accept +// a CAIReadWrite trait because it is not Sized (bound to a object). +// This will likely change in a future version of Rust. +// go away in future revisions of Rust. +pub(crate) struct CAIReadWriteWrapper<'a> { + pub reader_writer: &'a mut dyn CAIReadWrite, +} + +impl Read for CAIReadWriteWrapper<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + self.reader_writer.read(buf) + } +} + +impl Write for CAIReadWriteWrapper<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + self.reader_writer.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.reader_writer.flush() + } +} + +impl Seek for CAIReadWriteWrapper<'_> { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> { + self.reader_writer.seek(pos) + } +} + // Interface for in memory CAI reading pub trait CAIReader: Sync + Send { // Return entire CAI block as Vec<u8> diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -1306,7 +1306,7 @@ mod tests_file_io { let ingredient = Ingredient::from_file(ap).expect("from_file"); stats(&ingredient); - println!("ingredient = {}", ingredient); + println!("ingredient = {ingredient}"); assert_eq!(ingredient.title(), BAD_SIGNATURE_JPEG); assert_eq!(ingredient.format(), "image/jpeg"); test_thumbnail(&ingredient, "image/jpeg"); diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs @@ -358,7 +358,6 @@ pub mod tests { let handlers: Vec<Box<dyn AssetIO>> = vec![ Box::new(C2paIO::new("")), Box::new(BmffIO::new("")), - Box::new(RiffIO::new("")), Box::new(TiffIO::new("")), ]; diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -1381,19 +1381,21 @@ impl Store { } } - if block_end as u64 > stream_len { - return Err(Error::BadParam( - "data hash exclusions out of range".to_string(), - )); - } - if found_jumbf { // add exclusion hash for bytes before and after jumbf let mut dh = DataHash::new("jumbf manifest", alg, None); if block_end > block_start { dh.add_exclusion(Exclusion::new(block_start, block_end - block_start)); } + if calc_hashes { + // this check is only valid on the final sized asset + if block_end as u64 > stream_len { + return Err(Error::BadParam( + "data hash exclusions out of range".to_string(), + )); + } + dh.gen_hash_from_stream(stream)?; } else { match alg { diff --git a/sdk/src/utils/xmp_inmemory_utils.rs b/sdk/src/utils/xmp_inmemory_utils.rs @@ -26,6 +26,8 @@ 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> "#; + #[derive(Default)] pub struct XmpInfo { pub document_id: Option<String>, @@ -160,8 +162,7 @@ fn extract_document_id(xmp: &str) -> Option<String> { } /// add or replace a dc:provenance value to xmp, including dc:terms if needed -#[allow(dead_code)] // keep for future -fn add_provenance(xmp: &str, provenance: &str) -> Result<String> { +pub fn add_provenance(xmp: &str, provenance: &str) -> Result<String> { let xmp = add_xmp_key(xmp, "xmlns:dcterms", "http://purl.org/dc/terms/")?; add_xmp_key(&xmp, "dcterms:provenance", provenance) } @@ -190,12 +191,6 @@ mod tests { </rdf:RDF> </x:xmpmeta>"#; - 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> "#; - const PROVENANCE: &str = "self#jumbf=c2pa/contentauth:urn:uuid:a58065fb-79ae-4eb3-87b9-a19830860059/c2pa.claim"; diff --git a/sdk/tests/fixtures/test.webp b/sdk/tests/fixtures/test.webp Binary files differ. diff --git a/sdk/tests/fixtures/test_lossless.webp b/sdk/tests/fixtures/test_lossless.webp Binary files differ. diff --git a/sdk/tests/fixtures/test_xmp.webp b/sdk/tests/fixtures/test_xmp.webp Binary files differ.