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 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:
Msdk/src/claim.rs | 2+-
Msdk/src/ingredient.rs | 113+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msdk/src/manifest.rs | 29+++++++----------------------
Msdk/src/resource_store.rs | 12+++++++++---
Msdk/src/store.rs | 7++++++-
Asdk/src/utils/mime.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdk/src/utils/mod.rs | 2++
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")]