commit 4dd6b596bc4d28b390238ceeb82cf21b0567c309
parent f2af393d8c736e9d0f12dac34c03712ccfe5be90
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Tue, 17 Oct 2023 14:58:11 -0700
(MINOR) Support databox thumbnails CAI-4142 (#325)
* Adds support for databox thumbnails
* Databox and icons now use jumbf urls in manifest store
* Adds mime.rs utility for extension conversion
* Removes id parameter from to_resource_ref
* Added documentation for mime format functions
Diffstat:
7 files changed, 158 insertions(+), 81 deletions(-)
diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs
@@ -702,7 +702,7 @@ impl Claim {
let mut databox_uri = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash);
databox_uri.add_salt(salt);
- // add credential to vcstore
+ // add databox to databox store
self.data_boxes.push((databox_uri.clone(), new_db));
Ok(databox_uri)
diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs
@@ -31,9 +31,7 @@ use crate::{
hashed_uri::HashedUri,
jumbf::{
self,
- labels::{
- assertion_label_from_uri, manifest_label_from_uri, to_assertion_uri, to_relative_uri,
- },
+ labels::{manifest_label_from_uri, to_assertion_uri},
},
jumbf_io::load_jumbf_from_stream,
resource_store::{skip_serializing_resources, ResourceRef, ResourceStore},
@@ -191,6 +189,15 @@ impl Ingredient {
}
}
+ // try to determine if this is a V2 ingredient
+ pub(crate) fn is_v2(&self) -> bool {
+ self.instance_id.is_none()
+ || self.data.is_some()
+ || self.description.is_some()
+ || self.informational_uri.is_some()
+ || self.relationship == Relationship::InputTo
+ }
+
/// Returns a user-displayable title for this ingredient.
pub fn title(&self) -> &str {
self.title.as_str()
@@ -491,34 +498,9 @@ impl Ingredient {
.unwrap_or_else(|| "".into())
.to_lowercase();
- let format = match extension.as_ref() {
- "jpg" | "jpeg" => "image/jpeg",
- "png" => "image/png",
- "gif" => "image/gif",
- "psd" => "image/vnd.adobe.photoshop",
- "tiff" => "image/tiff",
- "svg" => "image/svg+xml",
- "ico" => "image/x-icon",
- "bmp" => "image/bmp",
- "webp" => "image/webp",
- "dng" => "image/dng",
- "heic" => "image/heic",
- "heif" => "image/heif",
- "mp2" | "mpa" | "mpe" | "mpeg" | "mpg" | "mpv2" => "video/mpeg",
- "mp4" => "video/mp4",
- "avif" => "image/avif",
- "mov" | "qt" => "video/quicktime",
- "m4a" => "audio/mp4",
- "mid" | "rmi" => "audio/mid",
- "mp3" => "audio/mpeg",
- "wav" => "audio/vnd.wav",
- "aif" | "aifc" | "aiff" => "audio/aiff",
- "ogg" => "audio/ogg",
- "pdf" => "application/pdf",
- "ai" => "application/postscript",
- _ => "application/octet-stream",
- }
- .to_owned();
+ let format = crate::utils::mime::extension_to_mime(&extension)
+ .unwrap_or("application/binary")
+ .to_owned();
(title, extension, format)
}
@@ -960,17 +942,32 @@ impl Ingredient {
Some(label) => label, // use the manifest from the thumbnail uri
None => claim_label.to_owned(), /* relative so use the whole url from the thumbnail assertion */
};
- match store.get_assertion_from_uri_and_claim(&hashed_uri.url(), &target_claim_label) {
- Some(assertion) => {
- let (format, image) = Self::thumbnail_from_assertion(assertion);
- let assertion_label = assertion_label_from_uri(&hashed_uri.url())
- .unwrap_or_else(|| "thumbnail".to_owned());
- // construct an id from the manifest and assertion labels
- let mut id = to_assertion_uri(&target_claim_label, &assertion_label);
- if target_claim_label == claim_label {
- id = to_relative_uri(&id);
- }
- ingredient.thumbnail = Some(ingredient.resources.add_uri(&id, &format, image)?);
+ let maybe_resource_ref = match hashed_uri.url() {
+ uri if uri.contains(jumbf::labels::ASSERTIONS) => {
+ // if this is a claim thumbnail, then use the label from the thumbnail uri
+ store
+ .get_assertion_from_uri_and_claim(&hashed_uri.url(), &target_claim_label)
+ .map(|assertion| {
+ let (format, image) = Self::thumbnail_from_assertion(assertion);
+ ingredient
+ .resources
+ .add_uri(&hashed_uri.url(), &format, image)
+ })
+ }
+ uri if uri.contains(jumbf::labels::DATABOXES) => store
+ .get_data_box_from_uri_and_claim(&hashed_uri.url(), &target_claim_label)
+ .map(|data_box| {
+ ingredient.resources.add_uri(
+ &hashed_uri.url(),
+ &data_box.format,
+ data_box.data.clone(),
+ )
+ }),
+ _ => None,
+ };
+ match maybe_resource_ref {
+ Some(data_ref) => {
+ ingredient.thumbnail = Some(data_ref?);
}
None => {
error!("failed to get {} from {}", hashed_uri.url(), ingredient_uri);
@@ -1076,11 +1073,10 @@ impl Ingredient {
.map(|t| {
// convert ingredient uris to absolute when adding them
// since this uri references a different manifest
- let assertion_label =
- jumbf::labels::assertion_label_from_uri(&t.url())
- .unwrap_or_default();
- let url =
- to_assertion_uri(&manifest_label, &assertion_label);
+ let url = jumbf::labels::to_absolute_uri(
+ &manifest_label,
+ &t.url(),
+ );
HashedUri::new(url, t.alg(), &t.hash())
});
}
@@ -1115,13 +1111,22 @@ impl Ingredient {
}
None => {
let data = self.thumbnail_bytes()?;
- claim.add_assertion(&Thumbnail::new(
- &labels::add_thumbnail_format(
- labels::INGREDIENT_THUMBNAIL,
+ if self.is_v2() {
+ // v2 ingredients use databoxes for thumbnails
+ claim.add_databox(
&thumb_ref.format,
- ),
- data.into_owned(),
- ))?
+ data.into_owned(),
+ thumb_ref.data_types.clone(),
+ )?
+ } else {
+ claim.add_assertion(&Thumbnail::new(
+ &labels::add_thumbnail_format(
+ labels::INGREDIENT_THUMBNAIL,
+ &thumb_ref.format,
+ ),
+ data.into_owned(),
+ ))?
+ }
}
};
thumbnail = Some(hash_url);
@@ -1816,7 +1821,7 @@ mod tests_file_io {
fn test_input_to_file_based_ingredient() {
let mut folder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
folder.push("tests/fixtures");
- let mut ingredient = Ingredient::new("title", "format", "instance_id");
+ let mut ingredient = Ingredient::new_v2("title", "format");
ingredient.resources.set_base_path(folder);
//let mut _data_ref = ResourceRef::new("image/jpg", "foo");
//data_ref.data_types = vec!["c2pa.types.dataset.pytorch".to_string()];
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -509,15 +509,10 @@ impl Manifest {
if let Some(info_vec) = claim.claim_generator_info() {
let mut generators = Vec::new();
- let id_base = manifest.instance_id().to_owned();
for claim_info in info_vec {
let mut info = claim_info.to_owned();
if let Some(icon) = claim_info.icon.as_ref() {
- info.set_icon(icon.to_resource_ref(
- manifest.resources_mut(),
- claim,
- &id_base,
- )?);
+ info.set_icon(icon.to_resource_ref(manifest.resources_mut(), claim)?);
}
generators.push(info);
}
@@ -565,18 +560,13 @@ impl Manifest {
match base_label.as_ref() {
base if base.starts_with(labels::ACTIONS) => {
let mut actions = Actions::from_assertion(assertion)?;
- let id = manifest.instance_id().to_owned();
for action in actions.actions_mut() {
if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) =
action.software_agent_mut()
{
if let Some(icon) = info.icon.as_mut() {
- let icon = icon.to_resource_ref(
- manifest.resources_mut(),
- claim,
- id.as_str(),
- )?;
+ let icon = icon.to_resource_ref(manifest.resources_mut(), claim)?;
info.set_icon(icon);
}
}
@@ -587,11 +577,9 @@ impl Manifest {
for template in templates {
// replace icon with resource ref
template.icon = match template.icon.take() {
- Some(icon) => Some(icon.to_resource_ref(
- manifest.resources_mut(),
- claim,
- id.as_str(),
- )?),
+ Some(icon) => {
+ Some(icon.to_resource_ref(manifest.resources_mut(), claim)?)
+ }
None => None,
};
@@ -599,11 +587,8 @@ impl Manifest {
template.software_agent = match template.software_agent.take() {
Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => {
if let Some(icon) = info.icon.as_mut() {
- let icon = icon.to_resource_ref(
- manifest.resources_mut(),
- claim,
- id.as_str(),
- )?;
+ let icon =
+ icon.to_resource_ref(manifest.resources_mut(), claim)?;
info.set_icon(icon);
}
Some(SoftwareAgent::ClaimGeneratorInfo(info))
diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs
@@ -58,14 +58,14 @@ impl UriOrResource {
&self,
resources: &mut ResourceStore,
claim: &Claim,
- id: &str,
) -> Result<UriOrResource> {
match self {
UriOrResource::ResourceRef(r) => Ok(UriOrResource::ResourceRef(r.clone())),
UriOrResource::HashedUri(h) => {
- let data_box = claim.find_databox(&h.url()).ok_or(Error::MissingDataBox)?;
+ let uri = crate::jumbf::labels::to_absolute_uri(claim.label(), &h.url());
+ let data_box = claim.find_databox(&uri).ok_or(Error::MissingDataBox)?;
let resource_ref =
- resources.add_with(id, &data_box.format, data_box.data.clone())?;
+ resources.add_with(&h.url(), &data_box.format, data_box.data.clone())?;
Ok(UriOrResource::ResourceRef(resource_ref))
}
}
@@ -235,6 +235,12 @@ impl ResourceStore {
id = format!("{}/{id}", label);
}
id = id.replace([':'], "_");
+ // add a file extension if it doesn't have one
+ if !(id.ends_with(".jpeg") || id.ends_with(".png")) {
+ if let Some(ext) = crate::utils::mime::format_to_extension(format) {
+ id = format!("{}.{}", id, ext);
+ }
+ }
}
if !self.exists(&id) {
self.add(&id, value)?;
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -39,7 +39,7 @@ use crate::{
jumbf::{
self,
boxes::*,
- labels::{ASSERTIONS, CREDENTIALS, DATABOXES, SIGNATURE},
+ labels::{to_absolute_uri, ASSERTIONS, CREDENTIALS, DATABOXES, SIGNATURE},
},
jumbf_io::{
get_assetio_handler, load_jumbf_from_stream, object_locations_from_stream,
@@ -352,6 +352,11 @@ impl Store {
None => self.get_claim(target_claim_label), // relative so use the target claim label
}
.and_then(|claim| {
+ let uri = if target_claim_label != self.label() {
+ to_absolute_uri(target_claim_label, uri)
+ } else {
+ uri.to_owned()
+ };
claim
.databoxes()
.iter()
diff --git a/sdk/src/utils/mime.rs b/sdk/src/utils/mime.rs
@@ -0,0 +1,74 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+/// Converts a file extension to a MIME type
+pub fn extension_to_mime(extension: &str) -> Option<&'static str> {
+ Some(match extension {
+ "jpg" | "jpeg" => "image/jpeg",
+ "png" => "image/png",
+ "gif" => "image/gif",
+ "psd" => "image/vnd.adobe.photoshop",
+ "tiff" => "image/tiff",
+ "svg" => "image/svg+xml",
+ "ico" => "image/x-icon",
+ "bmp" => "image/bmp",
+ "webp" => "image/webp",
+ "dng" => "image/dng",
+ "heic" => "image/heic",
+ "heif" => "image/heif",
+ "mp2" | "mpa" | "mpe" | "mpeg" | "mpg" | "mpv2" => "video/mpeg",
+ "mp4" => "video/mp4",
+ "avif" => "image/avif",
+ "mov" | "qt" => "video/quicktime",
+ "m4a" => "audio/mp4",
+ "mid" | "rmi" => "audio/mid",
+ "mp3" => "audio/mpeg",
+ "wav" => "audio/vnd.wav",
+ "aif" | "aifc" | "aiff" => "audio/aiff",
+ "ogg" => "audio/ogg",
+ "pdf" => "application/pdf",
+ "ai" => "application/postscript",
+ _ => return None,
+ })
+}
+
+/// Converts a format to a file extension
+pub fn format_to_extension(format: &str) -> Option<&'static str> {
+ Some(match format {
+ "jpg" | "jpeg" | "image/jpeg" => "jpg",
+ "png" | "image/png" => "png",
+ "gif" | "image/gif" => "gif",
+ "psd" | "image/vnd.adobe.photoshop" => "psd",
+ "tiff" | "image/tiff" => "tiff",
+ "svg" | "image/svg+xml" => "svg",
+ "ico" | "image/x-icon" => "ico",
+ "bmp" | "image/bmp" => "bmp",
+ "webp" | "image/webp" => "webp",
+ "dng" | "image/dng" => "dng",
+ "heic" | "image/heic" => "heic",
+ "heif" | "image/heif" => "heif",
+ "mp2" | "mpa" | "mpe" | "mpeg" | "mpg" | "mpv2" | "video/mpeg" => "mp2",
+ "mp4" | "video/mp4" => "mp4",
+ "avif" | "image/avif" => "avif",
+ "mov" | "qt" | "video/quicktime" => "mov",
+ "m4a" | "audio/mp4" => "m4a",
+ "mid" | "rmi" | "audio/mid" => "mid",
+ "mp3" | "audio/mpeg" => "mp3",
+ "wav" | "audio/vnd.wav" => "wav",
+ "aif" | "aifc" | "aiff" | "audio/aiff" => "aif",
+ "ogg" | "audio/ogg" => "ogg",
+ "pdf" | "application/pdf" => "pdf",
+ "ai" | "application/postscript" => "ai",
+ _ => return None,
+ })
+}
diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs
@@ -16,6 +16,8 @@ pub(crate) mod cbor_types;
#[allow(dead_code)]
pub(crate) mod hash_utils;
pub(crate) mod merkle;
+#[cfg(feature = "file_io")]
+pub(crate) mod mime;
#[allow(dead_code)] // for wasm build
pub(crate) mod patch;
#[cfg(feature = "add_thumbnails")]