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 166965ebbebc7d85048193a7e9e84cbee30ec918
parent 136831e4f4c40b1776b5923a77da6477536addea
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Thu, 20 Apr 2023 08:57:43 -0700

Ingredient async and thumbnail support (#240)

Ingredient thumbnail fix
allow override of valid manifest thumb
Don't write to input folder
Debug output cleanup
Add Ingredient::from_memory_async
and Ingredient::from_stream_async
Diffstat:
Msdk/src/assertions/actions.rs | 1-
Msdk/src/assertions/creative_work.rs | 3---
Msdk/src/ingredient.rs | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msdk/src/manifest.rs | 13++++++++++---
Msdk/src/resource_store.rs | 1-
5 files changed, 176 insertions(+), 62 deletions(-)

diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs @@ -391,7 +391,6 @@ pub mod tests { .set_data_source(DataSource::new(GENERATOR_REE)), ); - dbg!(&original); assert_eq!(original.actions.len(), 2); let assertion = original.to_assertion().expect("build_assertion"); assert_eq!(assertion.mime_type(), "application/cbor"); diff --git a/sdk/src/assertions/creative_work.rs b/sdk/src/assertions/creative_work.rs @@ -182,7 +182,6 @@ pub mod tests { assert_eq!(assertion.mime_type(), "application/json"); assert_eq!(assertion.label(), CreativeWork::LABEL); let result = CreativeWork::from_assertion(&assertion).expect("extract_assertion"); - dbg!(serde_json::to_string(&result).unwrap()); assert_eq!( original.author().unwrap()[0].name(), result.author().unwrap()[0].name() @@ -192,7 +191,6 @@ pub mod tests { #[test] fn from_creative_work_sample() { let original = CreativeWork::from_json_str(SAMPLE_CREATIVE_WORK).expect("from_json_str"); - dbg!(&original); let original_publisher: SchemaDotOrgPerson = original.get("publisher").unwrap(); let assertion = original.to_assertion().expect("build_assertion"); assert_eq!(assertion.mime_type(), "application/json"); @@ -207,7 +205,6 @@ pub mod tests { #[test] fn from_creative_work_stock() { let original = CreativeWork::from_json_str(STOCK_CREATIVE_WORK).expect("from_json_str"); - dbg!(&original); let original_url: String = original.get("url").unwrap(); let assertion = original.to_assertion().expect("build_assertion"); assert_eq!(assertion.mime_type(), "application/json"); diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -292,6 +292,28 @@ impl Ingredient { Ok(self) } + /// Sets the thumbnail format and image data only in memory + /// + /// This is only used for internally generated thumbnails - when + /// reading thumbnails from files, we don't want to write these to file + /// So this ensures they stay in memory unless written out. + pub fn set_memory_thumbnail<S: Into<String>, B: Into<Vec<u8>>>( + &mut self, + format: S, + bytes: B, + ) -> Result<&mut Self> { + // Do not write this as a file when reading from files + #[cfg(feature = "file_io")] + let base_path = self.resources_mut().take_base_path(); + let base_id = self.instance_id().to_string(); + self.thumbnail = Some(self.resources.add_with(&base_id, &format.into(), bytes)?); + #[cfg(feature = "file_io")] + if let Some(path) = base_path { + self.resources_mut().set_base_path(path) + } + Ok(self) + } + /// Sets the hash value generated from the entire asset. pub fn set_hash<S: Into<String>>(&mut self, hash: S) -> &mut Self { self.hash = Some(hash.into()); @@ -399,25 +421,40 @@ impl Ingredient { /// [`ManifestStore`]: crate::ManifestStore #[cfg(feature = "file_io")] pub fn from_file_info<P: AsRef<Path>>(path: P) -> Self { - fn make_id(id_type: &str) -> String { - let uuid = Uuid::new_v4(); - //warn!("Generating fake id {}", uuid); - format!("xmp:{id_type}id:{uuid}") - } - // get required information from the file path let (title, _, format) = Self::get_path_info(path.as_ref()); // if we can open the file try tto get xmp info - let xmp_info = match std::fs::File::open(path).map_err(Error::IoError) { - Ok(mut file) => XmpInfo::from_source(&mut file, &format), - Err(_) => XmpInfo::default(), + match std::fs::File::open(path).map_err(Error::IoError) { + Ok(mut file) => Self::from_stream_info(&mut file, &format, &title), + Err(_) => Self { + title, + format, + ..Default::default() + }, + } + } + + /// Generates an `Ingredient` from a stream, including XMP info + pub fn from_stream_info<F, S>(stream: &mut dyn CAIRead, format: F, title: S) -> Self + where + F: Into<String>, + S: Into<String>, + { + let format = format.into(); + // if we can open the file try tto get xmp info + let xmp_info = XmpInfo::from_source(stream, &format); + + let mut ingredient = Self { + title: title.into(), + format, + ..Default::default() }; - // instance id is required so generate one if we don't have one - let instance_id = xmp_info.instance_id.unwrap_or_else(|| make_id("i")); + if let Some(instance_id) = xmp_info.instance_id { + ingredient.instance_id = instance_id; + } - let mut ingredient = Self::new(&title, &format, &instance_id); ingredient.document_id = xmp_info.document_id; // use document id if one exists ingredient.provenance = xmp_info.provenance; @@ -425,6 +462,7 @@ impl Ingredient { } // utility method to set the validation status from store result and log + // also sets the thumbnail from the claim if valid and it exists fn update_validation_status( &mut self, result: Result<Store>, @@ -447,7 +485,7 @@ impl Ingredient { { let (format, image) = Self::thumbnail_from_assertion(claim_assertion.assertion()); - self.set_thumbnail(format, image)?; + self.set_memory_thumbnail(format, image)?; } } self.active_manifest = Some(claim.label().to_string()); @@ -587,7 +625,7 @@ impl Ingredient { // create a thumbnail if we don't already have a manifest with a thumb we can use if ingredient.thumbnail.is_none() { if let Some((format, image)) = options.thumbnail(path) { - ingredient.set_thumbnail(format, image)?; + ingredient.set_memory_thumbnail(format, image)?; } } @@ -608,23 +646,8 @@ impl Ingredient { /// This does not set title or hash /// Thumbnail will be set only if one can be retrieved from a previous valid manifest pub fn from_stream(format: &str, stream: &mut dyn CAIRead) -> Result<Self> { - fn make_id(id_type: &str) -> String { - let uuid = Uuid::new_v4(); - format!("xmp:{id_type}id:{uuid}") - } - - let xmp_info = XmpInfo::from_source(stream, format); - - let title = "untitled"; - // instance id is required so generate one if we don't have one - let instance_id = xmp_info.instance_id.unwrap_or_else(|| make_id("i")); - - let mut ingredient = Self::new(title, format, instance_id.as_str()); - ingredient.document_id = xmp_info.document_id; // use document id if one exists - ingredient.provenance = xmp_info.provenance; - - // optionally generate a hash so we know if the file has changed - //ingredient.hash = options.hash(path); + let mut ingredient = Self::from_stream_info(stream, format, "untitled"); + stream.rewind()?; let mut validation_log = DetailedStatusTracker::new(); @@ -670,7 +693,79 @@ impl Ingredient { ingredient.set_thumbnail(format, image)?; } Err(err) => { - dbg!(&err); + log::warn!("Could not create thumbnail. {err}"); + } + } + } + + Ok(ingredient) + } + + /// Creates an `Ingredient` from a memory buffer (async version). + /// + /// This does not set title or hash + /// Thumbnail will be set only if one can be retrieved from a previous valid manifest + pub async fn from_memory_async(format: &str, buffer: &[u8]) -> Result<Self> { + let mut stream = Cursor::new(buffer); + Self::from_stream_async(format, &mut stream).await + } + + /// Creates an `Ingredient` from a stream (async version). + /// + /// This does not set title or hash + /// Thumbnail will be set only if one can be retrieved from a previous valid manifest + pub async fn from_stream_async(format: &str, stream: &mut dyn CAIRead) -> Result<Self> { + let mut ingredient = Self::from_stream_info(stream, format, "untitled"); + stream.rewind()?; + + let mut validation_log = DetailedStatusTracker::new(); + + // retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found + let (result, manifest_bytes) = match load_jumbf_from_stream(format, stream) { + Ok(manifest_bytes) => { + ( + // generate a store from the buffer and then validate from the asset path + match Store::from_jumbf(&manifest_bytes, &mut validation_log) { + Ok(store) => { + // verify the store + //todo, change this when we have a stream version of verify + let mut buf: Vec<u8> = Vec::new(); + stream.rewind()?; + stream.read_to_end(&mut buf).map_err(Error::IoError)?; + Store::verify_store_async(&store, &buf, &mut validation_log) + .await + .map(|_| store) + } + Err(e) => { + validation_log.log_silent( + log_item!( + "asset", + "error loading asset", + "Ingredient::from_stream_async" + ) + .set_error(&e), + ); + Err(e) + } + }, + Some(manifest_bytes), + ) + } + Err(err) => (Err(err), None), + }; + + // set validation status from result and log + ingredient.update_validation_status(result, manifest_bytes, &mut validation_log)?; + + // create a thumbnail if we don't already have a manifest with a thumb we can use + #[cfg(feature = "add_thumbnails")] + if ingredient.thumbnail.is_none() { + stream.rewind()?; + match crate::utils::thumbnail::make_thumbnail_from_stream(format, stream) { + Ok((format, image)) => { + ingredient.set_thumbnail(format, image)?; + } + Err(err) => { log::warn!("Could not create thumbnail. {err}"); } } @@ -836,16 +931,14 @@ impl Ingredient { None => None, }; - // add ingredient thumbnail assertion if one is given and we don't already have one from the parent claim - if thumbnail.is_none() { - if let Some((format, data)) = self.thumbnail() { - let hash_url = claim.add_assertion(&Thumbnail::new( - &labels::add_thumbnail_format(labels::INGREDIENT_THUMBNAIL, format), - data.to_vec(), - ))?; - - thumbnail = Some(hash_url); - } + // if the ingredient defines a thumbnail, add it to the claim + // otherwise use the parent claim thumbnail if available + if let Some((format, data)) = self.thumbnail() { + let hash_url = claim.add_assertion(&Thumbnail::new( + &labels::add_thumbnail_format(labels::INGREDIENT_THUMBNAIL, format), + data.to_vec(), + ))?; + thumbnail = Some(hash_url); } let mut ingredient_assertion = assertions::Ingredient::new( @@ -995,29 +1088,46 @@ mod tests { #[cfg_attr(not(target_arch = "wasm32"), actix::test)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] - async fn test_stream_jpg() { + async fn test_stream_async_jpg() { let image_bytes = include_bytes!("../tests/fixtures/CA.jpg"); let title = "Test Image"; let format = "image/jpeg"; - let mut ingredient = Ingredient::from_memory(format, image_bytes).expect("from_memory"); + let mut ingredient = Ingredient::from_memory_async(format, image_bytes) + .await + .expect("from_memory"); ingredient.set_title(title); - // #[cfg(target_arch = "wasm32")] - // console_log::init_with_level(log::Level::Debug).expect("init log"); + println!("ingredient = {ingredient}"); + assert_eq!(&ingredient.title, title); + assert_eq!(ingredient.format(), format); + assert!(ingredient.provenance().is_some()); + assert!(ingredient.manifest_data().is_some()); + assert!(ingredient.metadata().is_none()); + #[cfg(target_arch = "wasm32")] + web_sys::console::debug_2( + &"ingredient_from_memory_async:".into(), + &ingredient.to_string().into(), + ); + assert!(ingredient.validation_status().is_none()); + } - // log::debug!( - // "ingredient = {}", - // ingredient - // ); + #[cfg_attr(not(target_arch = "wasm32"), test)] + // Note this does not work from wasm32, due to validation issues + #[cfg(not(target_arch = "wasm32"))] + fn test_stream_jpg() { + let image_bytes = include_bytes!("../tests/fixtures/CA.jpg"); + let title = "Test Image"; + let format = "image/jpeg"; + let mut ingredient = Ingredient::from_memory(format, image_bytes).expect("from_memory"); + ingredient.set_title(title); println!("ingredient = {ingredient}"); assert_eq!(&ingredient.title, title); assert_eq!(ingredient.format(), format); - //assert!(ingredient.thumbnail().is_some()); // we don't generate this thumbnail assert!(ingredient.provenance().is_some()); assert!(ingredient.manifest_data().is_some()); assert!(ingredient.metadata().is_none()); - //assert!(ingredient.validation_status().is_some()); + assert!(ingredient.validation_status().is_none()); } #[cfg_attr(not(target_arch = "wasm32"), actix::test)] @@ -1026,7 +1136,9 @@ mod tests { let image_bytes = include_bytes!("../tests/fixtures/XCA.jpg"); let title = "XCA.jpg"; let format = "image/jpeg"; - let mut ingredient = Ingredient::from_memory(format, image_bytes).expect("from_memory"); + let mut ingredient = Ingredient::from_memory_async(format, image_bytes) + .await + .expect("from_memory"); ingredient.set_title(title); println!("ingredient = {ingredient}"); @@ -1185,7 +1297,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/manifest.rs b/sdk/src/manifest.rs @@ -1032,12 +1032,12 @@ pub(crate) mod tests { status_tracker::{DetailedStatusTracker, StatusTracker}, store::Store, utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG}, - validation_status, Ingredient, + validation_status, }; use crate::{ assertions::{c2pa_action, Action, Actions}, utils::test::{temp_signer, TEST_VC}, - Manifest, Result, + Ingredient, Manifest, Result, }; // example of random data structure as an assertion @@ -1481,6 +1481,13 @@ pub(crate) mod tests { )) .unwrap(); + // add a parent ingredient + let mut ingredient = Ingredient::from_memory_async("jpeg", image) + .await + .expect("from_stream_async"); + ingredient.set_title("parent.jpg"); + manifest.set_parent(ingredient).expect("set_parent"); + let signer = MyRemoteSigner {}; // Embed a manifest using the signer. @@ -1761,7 +1768,7 @@ pub(crate) mod tests { #[test] #[cfg(feature = "file_io")] - fn test_crate_file_based_ingredient() { + fn test_create_file_based_ingredient() { let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); folder.push("tests/fixtures"); let mut manifest = Manifest::new("claim_generator"); diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs @@ -92,7 +92,6 @@ impl ResourceStore { id = format!("{id_base}-{count}{ext}"); count += 1; } - dbg!(&id); id }