commit f2af393d8c736e9d0f12dac34c03712ccfe5be90
parent 86e928e0739a5f7d4f2402fdb65b46c12a33239b
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Tue, 17 Oct 2023 09:26:52 -0700
(MINOR) Reuse claim thumbnail as ingredient thumbnail if the store is valid (#322)
* Use jumbf uris for ManifestStore identifiers
* New generated file paths with
* Manifest label folders written to base_path
* Added ResourceStore set_label().
* References a claim thumbnail as the ingredient thumbnail instead of duplicating it if there are no validation errors.
* Changes Manifest.fromStore to iterate over assertions in the claim rather than the assertion store.
* Removes resource exists test from Ingredient.set_thumbnail
* Adds optional alg and hash fields to ResourceRef.
* Adds to_absolute_url internal method
* Added more ResourceRef documentation.
* c2pa manifest format is now application/c2pa instead of c2pa
* Add since (version) attribute to deprecated annotation
* (MINOR) This PR needs a minor version bump
* Documentation edits
---------
Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
4 files changed, 114 insertions(+), 56 deletions(-)
diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs
@@ -39,7 +39,7 @@ use crate::{
resource_store::{skip_serializing_resources, ResourceRef, ResourceStore},
status_tracker::{log_item, DetailedStatusTracker, StatusTracker},
store::Store,
- utils::xmp_inmemory_utils::XmpInfo,
+ utils::{base64, xmp_inmemory_utils::XmpInfo},
validation_status::{self, status_for_store, ValidationStatus},
};
@@ -360,10 +360,6 @@ impl Ingredient {
/// Sets the thumbnail from a ResourceRef.
pub fn set_thumbnail_ref(&mut self, thumbnail: ResourceRef) -> Result<&mut Self> {
- // verify the resource referenced exists
- if !self.resources.exists(&thumbnail.identifier) {
- return Err(Error::NotFound);
- };
self.thumbnail = Some(thumbnail);
Ok(self)
}
@@ -442,7 +438,10 @@ impl Ingredient {
/// Sets the Manifest C2PA data for this ingredient with bytes
pub fn set_manifest_data(&mut self, data: Vec<u8>) -> Result<&mut Self> {
let base_id = self.instance_id().to_string();
- self.manifest_data = Some(self.resources.add_with(&base_id, "c2pa", data)?);
+ self.manifest_data = Some(
+ self.resources
+ .add_with(&base_id, "application/c2pa", data)?,
+ );
Ok(self)
}
@@ -585,15 +584,28 @@ impl Ingredient {
if let Some(claim) = store.provenance_claim() {
// if the parent claim is valid and has a thumbnail, use it
if statuses.is_empty() {
- // search claim to find a claim thumbnail assertion without knowing the format
- if let Some(claim_assertion) = claim
- .claim_assertion_store()
+ if let Some(hashed_uri) = claim
+ .assertions()
.iter()
- .find(|ca| ca.label_raw().starts_with(labels::CLAIM_THUMBNAIL))
+ .find(|hashed_uri| hashed_uri.url().contains(labels::CLAIM_THUMBNAIL))
{
- let (format, image) =
- Self::thumbnail_from_assertion(claim_assertion.assertion());
- self.set_thumbnail(format, image)?;
+ // We found a valid claim thumbnail so just reference it, we don't need to copy it
+ let thumb_manifest = manifest_label_from_uri(&hashed_uri.url())
+ .unwrap_or_else(|| claim.label().to_string());
+ let uri =
+ jumbf::labels::to_absolute_uri(&thumb_manifest, &hashed_uri.url());
+ // Try to determine the format from the assertion label in the URL
+ let format = hashed_uri
+ .url()
+ .rsplit_once('.')
+ .map(|(_, ext)| format!("image/{}", ext))
+ .unwrap_or_else(|| "image/jpeg".to_string()); // default to jpeg??
+ let mut thumb = crate::resource_store::ResourceRef::new(format, uri);
+ // keep track of the alg and hash for reuse
+ thumb.alg = hashed_uri.alg();
+ let hash = base64::encode(&hashed_uri.hash());
+ thumb.hash = Some(hash);
+ self.set_thumbnail_ref(thumb)?;
}
}
self.active_manifest = Some(claim.label().to_string());
@@ -1092,11 +1104,26 @@ impl Ingredient {
// if the ingredient defines a thumbnail, add it to the claim
// otherwise use the parent claim thumbnail if available
if let Some(thumb_ref) = self.thumbnail_ref() {
- let data = self.thumbnail_bytes()?;
- let hash_url = claim.add_assertion(&Thumbnail::new(
- &labels::add_thumbnail_format(labels::INGREDIENT_THUMBNAIL, &thumb_ref.format),
- data.into_owned(),
- ))?;
+ let hash_url = match manifest_label_from_uri(&thumb_ref.identifier) {
+ Some(_) => {
+ let hash = match thumb_ref.hash.as_ref() {
+ Some(h) => base64::decode(h)
+ .map_err(|_e| Error::BadParam("Invalid hash".to_string()))?,
+ None => return Err(Error::BadParam("hash is missing".to_string())), /* todo: add hash missing error */
+ };
+ HashedUri::new(thumb_ref.identifier.clone(), thumb_ref.alg.clone(), &hash)
+ }
+ None => {
+ let data = self.thumbnail_bytes()?;
+ claim.add_assertion(&Thumbnail::new(
+ &labels::add_thumbnail_format(
+ labels::INGREDIENT_THUMBNAIL,
+ &thumb_ref.format,
+ ),
+ data.into_owned(),
+ ))?
+ }
+ };
thumbnail = Some(hash_url);
}
@@ -1567,7 +1594,7 @@ mod tests_file_io {
println!("ingredient = {ingredient}");
assert_eq!(&ingredient.title, MANIFEST_JPEG);
assert_eq!(ingredient.format(), "image/jpeg");
- assert!(ingredient.thumbnail().is_some()); // we don't generate this thumbnail
+ assert!(ingredient.thumbnail_ref().is_some()); // we don't generate this thumbnail
assert!(ingredient.manifest_data().is_some());
assert!(ingredient.metadata().is_none());
}
@@ -1613,16 +1640,15 @@ mod tests_file_io {
}
}
- let ap = fixture_path(MANIFEST_JPEG);
+ let ap = fixture_path(NO_MANIFEST_JPEG);
let ingredient = Ingredient::from_file_with_options(ap, &MyOptions {}).expect("from_file");
stats(&ingredient);
- println!("ingredient = {ingredient}");
assert_eq!(ingredient.title(), "MyTitle");
assert_eq!(ingredient.format(), "image/jpeg");
- assert!(ingredient.hash().is_some());
- assert!(ingredient.thumbnail().is_some()); // always generated
- assert!(ingredient.manifest_data().is_some());
+ assert_eq!(ingredient.hash(), Some("1234568abcdef"));
+ assert_eq!(ingredient.thumbnail_ref().unwrap().format, "image/foo"); // always generated
+ assert!(ingredient.manifest_data().is_none());
assert!(ingredient.metadata().is_none());
}
@@ -1709,24 +1735,19 @@ mod tests_file_io {
let ap = fixture_path("CA.jpg");
let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
folder.push("../target/tmp/ingredient");
- let mut ingredient = Ingredient::from_file_with_folder(ap, folder).expect("from_file");
+ let ingredient = Ingredient::from_file_with_folder(ap, folder).expect("from_file");
println!("ingredient = {ingredient}");
assert_eq!(ingredient.validation_status(), None);
- // verify we can't set references that don't exist
- assert!(ingredient
- .set_thumbnail_ref(ResourceRef::new("Foo", "bar"))
- .is_err());
- assert!(ingredient
- .set_manifest_data_ref(ResourceRef::new("Foo", "bar"))
- .is_err());
- // verify we can set a references that do exist
+ // verify ingredient thumbnail is an absolute url reference to a claim thumbnail
assert!(ingredient
- .set_thumbnail_ref(ResourceRef::new("Foo", "bar"))
- .is_err());
- assert!(ingredient
- .set_manifest_data_ref(ResourceRef::new("Foo", "bar"))
- .is_err());
+ .thumbnail_ref()
+ .unwrap()
+ .identifier
+ .contains(labels::JPEG_CLAIM_THUMBNAIL));
+
+ // verify manifest_data exists
+ assert!(ingredient.manifest_data_ref().is_some());
}
#[test]
@@ -1736,22 +1757,19 @@ mod tests_file_io {
folder.push("tests/fixtures");
let mut ingredient = Ingredient::new("title", "format", "instance_id");
ingredient.resources.set_base_path(folder);
- // verify we can't set a references that don't exist
- assert!(ingredient
- .set_thumbnail_ref(ResourceRef::new("image/jpg", "foo"))
- .is_err());
+
assert!(ingredient.thumbnail_ref().is_none());
assert!(ingredient
.set_manifest_data_ref(ResourceRef::new("image/jpg", "foo"))
.is_err());
assert!(ingredient.manifest_data_ref().is_none());
- // verify we can set a references that do exist
+ // verify we can set a reference
assert!(ingredient
.set_thumbnail_ref(ResourceRef::new("image/jpg", "C.jpg"))
.is_ok());
assert!(ingredient.thumbnail_ref().is_some());
assert!(ingredient
- .set_manifest_data_ref(ResourceRef::new("c2pa", "cloud_manifest.c2pa"))
+ .set_manifest_data_ref(ResourceRef::new("application/c2pa", "cloud_manifest.c2pa"))
.is_ok());
assert!(ingredient.manifest_data_ref().is_some());
}
diff --git a/sdk/src/jumbf/labels.rs b/sdk/src/jumbf/labels.rs
@@ -122,6 +122,17 @@ pub(crate) fn to_normalized_uri(uri: &str) -> String {
}
}
+// Converts a possibly relative JUMBF URI to an absolute URI to the manifest store.
+pub(crate) fn to_absolute_uri(manifest_label: &str, uri: &str) -> String {
+ let raw_uri = to_normalized_uri(uri);
+ let parts: Vec<&str> = raw_uri.split('/').collect();
+ if parts.len() > 2 && parts[1] == MANIFEST_STORE {
+ uri.to_string()
+ } else {
+ format!("{}/{}", to_manifest_uri(manifest_label), raw_uri)
+ }
+}
+
// Converts an absolute JUMBF URI to a URI relative to the manifest store.
pub(crate) fn to_relative_uri(uri: &str) -> String {
let raw_uri = to_normalized_uri(uri);
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -554,7 +554,10 @@ impl Manifest {
manifest.set_format(claim.format());
manifest.set_instance_id(claim.instance_id());
- for claim_assertion in claim.claim_assertion_store().iter() {
+ for assertion in claim.assertions() {
+ let claim_assertion = store.get_claim_assertion_from_uri(
+ &jumbf::labels::to_absolute_uri(claim.label(), &assertion.url()),
+ )?;
let assertion = claim_assertion.assertion();
let label = claim_assertion.label();
let base_label = assertion.label();
@@ -1659,8 +1662,8 @@ pub(crate) mod tests {
.expect("embed");
//let manifest_store = crate::ManifestStore::from_file(&sidecar).expect("from_file");
- let manifest_store =
- crate::ManifestStore::from_bytes("c2pa", &c2pa_data, true).expect("from_bytes");
+ let manifest_store = crate::ManifestStore::from_bytes("application/c2pa", &c2pa_data, true)
+ .expect("from_bytes");
assert_eq!(manifest_store.active_label(), Some("MyLabel"));
assert_eq!(
manifest_store.get_active().unwrap().title().unwrap(),
diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs
@@ -84,15 +84,31 @@ impl From<HashedUri> for UriOrResource {
}
}
-/// A reference to a resource to be used in JSON serialization
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
-
+/// A reference to a resource to be used in JSON serialization.
pub struct ResourceRef {
+ /// The mime type of the referenced resource.
pub format: String,
+
+ /// A URI that identifies the resource as referenced from the manifest.
+ ///
+ /// This may be a JUMBF URI, a file path, a URL or any other string.
+ /// Relative JUMBF URIs will be resolved with the manifest label.
+ /// Relative file paths will be resolved with the base path if provided.
pub identifier: String,
#[serde(skip_serializing_if = "Option::is_none")]
+
+ /// More detailed data types as defined in the C2PA spec.
pub data_types: Option<Vec<AssetType>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+
+ /// The algorithm used to hash the resource (if applicable).
+ pub alg: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+
+ /// The hash of the resource (if applicable).
+ pub hash: Option<String>,
}
impl ResourceRef {
@@ -101,6 +117,8 @@ impl ResourceRef {
format: format.into(),
identifier: identifier.into(),
data_types: None,
+ alg: None,
+ hash: None,
}
}
}
@@ -118,6 +136,7 @@ pub struct ResourceStore {
}
impl ResourceStore {
+ /// Create a new resource reference.
pub fn new() -> Self {
ResourceStore {
resources: HashMap::new(),
@@ -127,33 +146,39 @@ impl ResourceStore {
}
}
+ /// Set a manifest label for this store used to resolve relative JUMBF URIs.
pub fn set_label<S: Into<String>>(&mut self, label: S) -> &Self {
self.label = Some(label.into());
self
}
#[cfg(feature = "file_io")]
+ // Returns the base path for relative file paths if it is set.
pub fn base_path(&self) -> Option<&Path> {
self.base_path.as_deref()
}
#[cfg(feature = "file_io")]
+ /// Sets a base path for relative file paths.
+ ///
+ /// Identifiers will be interpreted as file paths and resources will be written to files if this is set.
pub fn set_base_path<P: Into<PathBuf>>(&mut self, base_path: P) {
self.base_path = Some(base_path.into());
}
#[cfg(feature = "file_io")]
+ /// Returns and removes the base path.
pub fn take_base_path(&mut self) -> Option<PathBuf> {
self.base_path.take()
}
- /// generate a unique id for a given content type (adds a file extension)
+ /// Generates a unique ID for a given content type (adds a file extension).
pub fn id_from(&self, key: &str, format: &str) -> String {
let ext = match format {
"jpg" | "jpeg" | "image/jpeg" => ".jpg",
"png" | "image/png" => ".png",
//make "svg" | "image/svg+xml" => ".svg",
- "c2pa" | "application/x-c2pa-manifest-store" => ".c2pa",
+ "c2pa" | "application/x-c2pa-manifest-store" | "application/c2pa" => ".c2pa",
_ => "",
};
// clean string for possible filesystem use
@@ -169,9 +194,9 @@ impl ResourceStore {
id
}
- /// Adds a resource, generating a resource ref from a key and format.
+ /// Adds a resource, generating a [`ResourceRef`] from a key and format.
///
- /// The generated identifier may be different from the key
+ /// The generated identifier may be different from the key.
pub fn add_with<R>(&mut self, key: &str, format: &str, value: R) -> crate::Result<ResourceRef>
where
R: Into<Vec<u8>>,
@@ -235,13 +260,14 @@ impl ResourceStore {
Ok(self)
}
+ /// Returns a [`HashMap`] of internal resources.
pub fn resources(&self) -> &HashMap<String, Vec<u8>> {
&self.resources
}
/// Returns a copy on write reference to the resource if found.
///
- /// returns Error::NotFound if it cannot find a resource matching that id
+ /// Returns [`Error::ResourceNotFound`] if it cannot find a resource matching that ID.
pub fn get(&self, id: &str) -> Result<Cow<Vec<u8>>> {
#[cfg(feature = "file_io")]
if !self.resources.contains_key(id) {
@@ -264,7 +290,7 @@ impl ResourceStore {
)
}
- /// Returns true if the resource has been added or exists as file.
+ /// Returns `true` if the resource has been added or exists as file.
pub fn exists(&self, id: &str) -> bool {
if !self.resources.contains_key(id) {
#[cfg(feature = "file_io")]
@@ -283,7 +309,7 @@ impl ResourceStore {
}
#[cfg(feature = "file_io")]
- // return the full path for an id
+ // Returns the full path for an ID.
pub fn path_for_id(&self, id: &str) -> Option<PathBuf> {
self.base_path.as_ref().map(|base| base.join(id))
}