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:
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
}