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 ff293565bff377e9eef03624bc9503757a59c51e
parent fab96521af9e4696b58e297cb3f615eed23bcc91
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Tue, 19 Jul 2022 15:28:49 -0700

(MINOR) `IngredientOptions` allow override of hash and thumbnail generation; image library is now a default feature (#79)

* IngredientOptions is now a trait with methods to override
* Remove blake3 from sdk Cargo.toml
* Remove manifest asset field, flatten into manifest struct
* new default add_thumbnails feature allows building sdk without image library
* export DefaultOptions, path in IngredientOptions methods.
* Adds test cases for no-default-features
Diffstat:
MCargo.toml | 1+
MMakefile | 9++++++++-
Mmake_test_images/Cargo.toml | 1+
Mmake_test_images/src/make_test_images.rs | 42++++++++++++++++++++++++++++++++++++------
Msdk/Cargo.toml | 20+++++++++++++++++---
Msdk/examples/client/client.rs | 8++++----
Asdk/examples/show.rs | 28++++++++++++++++++++++++++++
Msdk/src/error.rs | 1+
Msdk/src/ingredient.rs | 202++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msdk/src/lib.rs | 4+++-
Msdk/src/manifest.rs | 159+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msdk/src/openssl/temp_signer_async.rs | 1+
Msdk/src/utils/mod.rs | 2+-
Msdk/src/utils/thumbnail.rs | 9+++++----
Msdk/tests/integration.rs | 19+------------------
15 files changed, 344 insertions(+), 162 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -1,2 +1,3 @@ [workspace] +resolver = "2" members = ["sdk", "make_test_images"] diff --git a/Makefile b/Makefile @@ -25,12 +25,15 @@ clippy: test-local: cargo test --all-features +test-no-defaults: + cd sdk && cargo test --features="file_io" --no-default-features + test-wasm: cd sdk && wasm-pack test --node # Full local validation, build and test all features including wasm # Run this before pushing a PR to pre-validate -test: check-format check-docs clippy test-local test-wasm +test: check-format check-docs clippy test-local test-no-defaults test-wasm # Builds and views documentation doc: @@ -44,3 +47,7 @@ images: # Runs the client example using test image and output to target/tmp/client.jpg client: cargo run --example client sdk/tests/fixtures/ca.jpg target/tmp/client.jpg + +# Runs the show example +show: + cargo run --example show -- sdk/tests/fixtures/ca.jpg diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml @@ -8,6 +8,7 @@ rust-version = "1.58.0" [dependencies] anyhow = "1.0" +blake3 = "1.0.0" c2pa = { path="../sdk", features = ["file_io", "xmp_write"] } env_logger = "0.9" log = "0.4" diff --git a/make_test_images/src/make_test_images.rs b/make_test_images/src/make_test_images.rs @@ -95,6 +95,26 @@ impl Default for Config { } } +/// Generate a blake3 hash over the image in path using a fixed buffer +fn blake3_hash(path: &Path) -> Result<String> { + use std::fs::File; + use std::io::Read; + // Hash an input incrementally. + let mut hasher = blake3::Hasher::new(); + const BUFFER_LEN: usize = 1024 * 1024; + let mut buffer = [0u8; BUFFER_LEN]; + let mut file = File::open(path)?; + loop { + let read_count = file.read(&mut buffer)?; + hasher.update(&buffer[..read_count]); + if read_count != BUFFER_LEN { + break; + } + } + let hash = hasher.finalize(); + Ok(hash.to_hex().as_str().to_owned()) +} + /// Tool for building test case images for C2PA pub struct MakeTestImages { config: Config, @@ -164,10 +184,18 @@ impl MakeTestImages { // keep track of all actions here let mut actions = Actions::new(); - let options = IngredientOptions { - make_hash: true, - title: None, - }; + struct ImageOptions {} + impl ImageOptions { + fn new() -> Self { + ImageOptions {} + } + } + + impl IngredientOptions for ImageOptions { + fn hash(&self, path: &Path) -> Option<String> { + blake3_hash(path).ok() + } + } let generator = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); let mut manifest = Manifest::new(generator); @@ -185,7 +213,8 @@ impl MakeTestImages { Some(src) => { let src_path = &self.make_path(src); - let parent = Ingredient::from_file_with_options(src_path, &options)?; + let parent = Ingredient::from_file_with_options(src_path, &ImageOptions::new())?; + actions = actions.add_action( Action::new(c2pa_action::OPENED) .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?, @@ -246,7 +275,8 @@ impl MakeTestImages { image::imageops::overlay(&mut img, &img_small, x, 0); // create and add the ingredient - let ingredient = Ingredient::from_file_with_options(ing_path, &options)?; + let ingredient = + Ingredient::from_file_with_options(ing_path, &ImageOptions::new())?; actions = actions.add_action(Action::new(c2pa_action::PLACED).set_parameter( "identifier".to_owned(), diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -17,6 +17,8 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] +default = ["add_thumbnails"] +add_thumbnails = ["image"] async_signer = ["async-trait", "file_io"] bmff = [] # Work in progress support for BMFF-based containers file_io = ["openssl"] @@ -27,6 +29,19 @@ xmp_write = ["xmp_toolkit"] # It enables some low-overhead timing features used in our development cycle. diagnostics = [] +#[cfg(not(target_arch = "wasm32"))] +[[example]] +name = "client" +required-features = ["file_io"] + +[[example]] +name = "show" +required-features = ["file_io"] + +[[example]] +name = "custom_assertion" + + [lib] crate-type = ["cdylib", "rlib"] @@ -34,8 +49,7 @@ crate-type = ["cdylib", "rlib"] async-trait = { version = "0.1.48", optional = true } atree = "0.5.2" base64 = "0.13.0" -bcder = "0.6.0" -blake3 = "1.0.0" +bcder = "0.6.0" bytes = "1.1.0" byteorder = "1.3.4" chrono = { version = "0.4.19", features = ["wasmbind"] } @@ -44,7 +58,6 @@ conv = "0.3.3" coset = "0.3.1" extfmt = "0.1.1" hex = "0.4.3" -image = "0.23.10" img-parts = "0.2.3" log = "0.4.8" multibase = "0.9.0" @@ -72,6 +85,7 @@ x509-certificate = "0.12.0" ring = "0.16.20" url = "2.2.2" ureq = "2.4.0" +image = { version = "0.24.2", optional = true } instant = "0.1.0" openssl = { version = "0.10.31", features = ["vendored"], optional = true } xmp_toolkit = { version = "0.5.1", optional = true } diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs @@ -84,9 +84,9 @@ pub fn main() -> Result<()> { "target/tmp/client.jpg", ), }; + let source = PathBuf::from(src); let dest = PathBuf::from(dst); - // if a filepath was provided on the command line, read it as a parent file let parent = Ingredient::from_file(source.as_path())?; @@ -109,9 +109,9 @@ pub fn main() -> Result<()> { .add_assertion(&creative_work)?; // sign and embed into the target file - let signcert_path = "../sdk/tests/fixtures/certs.ps256.pem"; - let pkey_path = "../sdk/tests/fixtures/certs.ps256.pub"; - let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?; + let signcert_path = "sdk/tests/fixtures/certs/es256.pub"; + let pkey_path = "sdk/tests/fixtures/certs/es256.pem"; + let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None)?; manifest.embed(&source, &dest, &*signer)?; diff --git a/sdk/examples/show.rs b/sdk/examples/show.rs @@ -0,0 +1,28 @@ +// 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. + +//! Example App that generates a manifest store listing for a given file +use anyhow::Result; +use c2pa::ManifestStore; + +#[cfg(not(target_arch = "wasm32"))] +fn main() -> Result<()> { + let args: Vec<String> = std::env::args().collect(); + if args.len() > 1 { + let ms = ManifestStore::from_file(&args[1])?; + println!("{}", ms); + } else { + println!("Prints a manifest report (requires a file path argument)") + } + Ok(()) +} diff --git a/sdk/src/error.rs b/sdk/src/error.rs @@ -221,6 +221,7 @@ pub enum Error { JsonError(#[from] serde_json::Error), #[error(transparent)] + #[cfg(all(not(target_arch = "wasm32"), feature = "add_thumbnails"))] ImageError(#[from] image::ImageError), #[error(transparent)] diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs @@ -32,8 +32,8 @@ use serde::{Deserialize, Serialize}; /// Function that is used by serde to determine whether or not we should serialize /// thumbnail data based on the "serialize_thumbnails" flag (serialization is disabled by default) -pub fn skip_serializing_thumbnails(_value: &Option<(String, Vec<u8>)>) -> bool { - !cfg!(feature = "serialize_thumbnails") +fn skip_serializing_thumbnails(value: &Option<(String, Vec<u8>)>) -> bool { + !cfg!(feature = "serialize_thumbnails") || value.is_none() } #[cfg(feature = "file_io")] @@ -168,7 +168,7 @@ impl Ingredient { .map(|(format, image)| (format.as_str(), image.deref())) } - /// Returns an optional Blake3 hash made from the bits of the original image. + /// Returns an optional hash to uniquely identify this asset pub fn hash(&self) -> Option<&str> { self.hash.as_deref() } @@ -300,9 +300,24 @@ impl Ingredient { "psd" => "image/vnd.adobe.photoshop", "tiff" => "image/tiff", "svg" => "image/svg+xml", - "ico" => "image/vnd.microsoft.icon", + "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(); @@ -346,8 +361,7 @@ impl Ingredient { #[cfg(feature = "file_io")] /// Creates an `Ingredient` from a file path. pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { - let options = IngredientOptions::default(); - Self::from_file_with_options(path.as_ref(), &options) + Self::from_file_with_options(path.as_ref(), &DefaultOptions {}) } fn thumbnail_from_assertion(assertion: &Assertion) -> (String, Vec<u8>) { @@ -364,14 +378,14 @@ impl Ingredient { #[cfg(feature = "file_io")] pub fn from_file_with_options<P: AsRef<Path>>( path: P, - options: &IngredientOptions, + options: &dyn IngredientOptions, ) -> Result<Self> { Self::from_file_impl(path.as_ref(), options) } // Internal implementation to avoid code bloat. #[cfg(feature = "file_io")] - fn from_file_impl(path: &Path, options: &IngredientOptions) -> Result<Self> { + fn from_file_impl(path: &Path, options: &dyn IngredientOptions) -> Result<Self> { // these are declared inside this function in order to isolate them for wasm builds use crate::jumbf_io; use crate::status_tracker::{DetailedStatusTracker, StatusTracker}; @@ -390,17 +404,14 @@ impl Ingredient { } // if options includes a title, use it - if let Some(opt_title) = options.title { - ingredient.title = opt_title.to_string(); + if let Some(opt_title) = options.title(path) { + ingredient.title = opt_title; } // read the file into a buffer for processing let buf = std::fs::read(path).map_err(wrap_io_err)?; - // generate a hash so we know if the file has changed - // todo:: make hash algorithm an option fn taking stream - ingredient.hash = options - .make_hash - .then(|| blake3::hash(&buf).to_hex().as_str().to_owned()); + // optionally generate a hash so we know if the file has changed + ingredient.hash = options.hash(path); let mut report = DetailedStatusTracker::new(); @@ -455,10 +466,9 @@ impl Ingredient { } } - // create a thumbnail if we don't already have a claim with a thumb we can use + // create a thumbnail if we don't already have a manifest with a thumb we can use if ingredient.thumbnail.is_none() { - use crate::utils::thumbnail::make_thumbnail; - if let Ok((format, image)) = make_thumbnail(path) { + if let Some((format, image)) = options.thumbnail(path) { ingredient.set_thumbnail(format, image); } } @@ -528,6 +538,7 @@ impl Ingredient { Ok(ingredient) } + /// Converts a higher level Ingredient into the appropriate components in a claim pub(crate) fn add_to_claim( &self, claim: &mut Claim, @@ -649,17 +660,45 @@ impl std::fmt::Display for Ingredient { } } -#[derive(Default)] -/// This defines optional actions when creating [`Ingredient`]s from files. -pub struct IngredientOptions { - /// This allows setting the title for the ingredient. (If `None`, then the default behavior is to use the file's name.) - pub title: Option<&'static str>, +/// This defines optional operations when creating [`Ingredient`] structs from files. +#[cfg(feature = "file_io")] +pub trait IngredientOptions { + /// This allows setting the title for the ingredient. + /// + /// If it returns `None`, then the default behavior is to use the file's name. + fn title(&self, _path: &Path) -> Option<String> { + None + } - /// If `true`, then generate a Blake3 hash over the source asset and store it in the ingredient. + /// Returns an optional hash value for the ingredient + /// /// This can be used to test for duplicate ingredients or if a source file has changed. - pub make_hash: bool, + /// If hash is_some() Manifest.add_ingredient will dedup matching hashes + fn hash(&self, _path: &Path) -> Option<String> { + None + } + + /// Returns an optional thumbnail image representing the asset + /// + /// The first value is the content type of the thumbnail, i.e. image/jpeg + /// The second value is bytes of the thumbnail image + /// The default is to have no thumbnail, so you must provide an override to have a thumbnail image + fn thumbnail(&self, _path: &Path) -> Option<(String, Vec<u8>)> { + #[cfg(feature = "add_thumbnails")] + return crate::utils::thumbnail::make_thumbnail(_path).ok(); + #[cfg(not(feature = "add_thumbnails"))] + None + } } +#[cfg(feature = "file_io")] +/// DefaultOptions returns None for Title and Hash and generates thumbnail for supported thumbnails +/// +/// This can be use with Ingredient::from_file_with_options +pub struct DefaultOptions {} +#[cfg(feature = "file_io")] +impl IngredientOptions for DefaultOptions {} + #[cfg(test)] mod tests { #![allow(clippy::expect_used)] @@ -713,6 +752,7 @@ mod tests_file_io { use crate::utils::test::fixture_path; + const NO_MANIFEST_JPEG: &str = "earth_apollo17.jpg"; const MANIFEST_JPEG: &str = "C.jpg"; const BAD_SIGNATURE_JPEG: &str = "E-sig-CA.jpg"; const PRERELEASE_JPEG: &str = "prerelease.jpg"; @@ -731,6 +771,16 @@ mod tests_file_io { ingredient.title().len() + ingredient.instance_id().len() + thumb_size + manifest_data_size } + // check for correct thumbnail generation with or without add_thumbnails feature + fn test_thumbnail(ingredient: &Ingredient, format: &str) { + if cfg!(feature = "add_thumbnails") { + assert!(ingredient.thumbnail().is_some()); + assert_eq!(ingredient.thumbnail().unwrap().0, format); + } else { + assert!(ingredient.thumbnail().is_none()); + } + } + #[test] #[cfg(feature = "file_io")] fn test_psd() { @@ -741,48 +791,72 @@ mod tests_file_io { stats(&ingredient); println!("ingredient = {}", ingredient); - assert_eq!(&ingredient.title, "Purple Square.psd"); - assert_eq!(&ingredient.format, "image/vnd.adobe.photoshop"); - assert!(ingredient.thumbnail.is_none()); - assert!(ingredient.manifest_data.is_none()); + assert_eq!(ingredient.title(), "Purple Square.psd"); + assert_eq!(ingredient.format(), "image/vnd.adobe.photoshop"); + assert!(ingredient.thumbnail().is_none()); // should always be none + assert!(ingredient.manifest_data().is_none()); } #[test] #[cfg(feature = "file_io")] - fn test_jpg() { + fn test_manifest_jpg() { let ap = fixture_path(MANIFEST_JPEG); let ingredient = Ingredient::from_file(&ap).expect("from_file"); stats(&ingredient); println!("ingredient = {}", ingredient); assert_eq!(&ingredient.title, MANIFEST_JPEG); - assert_eq!(&ingredient.format, "image/jpeg"); - assert!(ingredient.thumbnail.is_some()); - assert!(ingredient.provenance.is_some()); - assert!(ingredient.manifest_data.is_some()); - assert!(ingredient.metadata.is_none()); + assert_eq!(ingredient.format(), "image/jpeg"); + 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()); + } + + #[test] + #[cfg(feature = "file_io")] + fn test_no_manifest_jpg() { + let ap = fixture_path(NO_MANIFEST_JPEG); + let ingredient = Ingredient::from_file(&ap).expect("from_file"); + stats(&ingredient); + + println!("ingredient = {}", ingredient); + assert_eq!(&ingredient.title, NO_MANIFEST_JPEG); + assert_eq!(ingredient.format(), "image/jpeg"); + test_thumbnail(&ingredient, "image/jpeg"); + assert!(ingredient.provenance().is_none()); + assert!(ingredient.manifest_data().is_none()); + assert!(ingredient.metadata().is_none()); } #[test] #[cfg(feature = "file_io")] fn test_jpg_options() { - let options = IngredientOptions { - make_hash: true, - title: Some("MyTitle"), - }; + struct MyOptions {} + impl IngredientOptions for MyOptions { + fn title(&self, _path: &Path) -> Option<String> { + Some("MyTitle".to_string()) + } + fn hash(&self, _path: &Path) -> Option<String> { + Some("1234568abcdef".to_string()) + } + fn thumbnail(&self, _path: &Path) -> Option<(String, Vec<u8>)> { + Some(("image/foo".to_string(), "bits".as_bytes().to_owned())) + } + } let ap = fixture_path(MANIFEST_JPEG); - let ingredient = Ingredient::from_file_with_options(&ap, &options).expect("from_file"); + 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()); - assert!(ingredient.provenance.is_some()); - assert!(ingredient.manifest_data.is_some()); - assert!(ingredient.metadata.is_none()); + 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.provenance().is_some()); + assert!(ingredient.manifest_data().is_some()); + assert!(ingredient.metadata().is_none()); } #[test] @@ -794,8 +868,8 @@ mod tests_file_io { println!("ingredient = {}", ingredient); assert_eq!(ingredient.title(), "libpng-test.png"); - assert!(ingredient.thumbnail().is_some()); - assert_eq!(ingredient.thumbnail().unwrap().0, "image/png"); + test_thumbnail(&ingredient, "image/png"); + assert!(ingredient.provenance().is_none()); assert!(ingredient.manifest_data.is_none()); } @@ -807,14 +881,14 @@ mod tests_file_io { stats(&ingredient); println!("ingredient = {}", ingredient); - assert_eq!(&ingredient.title, BAD_SIGNATURE_JPEG); - assert_eq!(&ingredient.format, "image/jpeg"); - assert!(ingredient.thumbnail.is_some()); - assert!(ingredient.provenance.is_some()); - assert!(ingredient.manifest_data.is_some()); - assert!(ingredient.validation_status.is_some()); + assert_eq!(ingredient.title(), BAD_SIGNATURE_JPEG); + assert_eq!(ingredient.format(), "image/jpeg"); + test_thumbnail(&ingredient, "image/jpeg"); + assert!(ingredient.provenance().is_some()); + assert!(ingredient.manifest_data().is_some()); + assert!(ingredient.validation_status().is_some()); assert!(ingredient - .validation_status + .validation_status() .unwrap() .iter() .any(|s| s.code() == validation_status::CLAIM_SIGNATURE_MISMATCH)); @@ -828,14 +902,14 @@ mod tests_file_io { stats(&ingredient); println!("ingredient = {}", ingredient); - assert_eq!(&ingredient.title, PRERELEASE_JPEG); - assert_eq!(&ingredient.format, "image/jpeg"); - assert!(ingredient.thumbnail.is_some()); - assert!(ingredient.provenance.is_some()); - assert!(ingredient.manifest_data.is_none()); - assert!(ingredient.validation_status.is_some()); + assert_eq!(ingredient.title(), PRERELEASE_JPEG); + assert_eq!(ingredient.format(), "image/jpeg"); + test_thumbnail(&ingredient, "image/jpeg"); + assert!(ingredient.provenance().is_some()); + assert!(ingredient.manifest_data().is_none()); + assert!(ingredient.validation_status().is_some()); assert_eq!( - ingredient.validation_status.unwrap()[0].code(), + ingredient.validation_status().unwrap()[0].code(), validation_status::STATUS_PRERELEASE ); } @@ -846,6 +920,6 @@ mod tests_file_io { let ap = fixture_path("CIE-sig-CA.jpg"); let ingredient = Ingredient::from_file(&ap).expect("from_file"); println!("ingredient = {}", ingredient); - assert_eq!(ingredient.validation_status, None); + assert_eq!(ingredient.validation_status(), None); } } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs @@ -85,7 +85,7 @@ mod error; pub use error::{Error, Result}; mod ingredient; -pub use ingredient::{Ingredient, IngredientOptions}; +pub use ingredient::Ingredient; pub mod jumbf_io; mod manifest; pub use manifest::Manifest; @@ -102,6 +102,8 @@ mod signing_alg; pub use signing_alg::{SigningAlg, UnknownAlgorithmError}; #[cfg(feature = "file_io")] +pub use ingredient::{DefaultOptions, IngredientOptions}; +#[cfg(feature = "file_io")] pub(crate) mod ocsp_utils; #[cfg(feature = "file_io")] mod openssl; diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -11,8 +11,6 @@ // specific language governing permissions and limitations under // each license. -#[cfg(feature = "file_io")] -use crate::utils::thumbnail::make_thumbnail; use crate::{ assertion::{AssertionBase, AssertionData}, assertions::{labels, Actions, CreativeWork, Thumbnail, User, UserCbor}, @@ -32,6 +30,13 @@ use serde_json::Value; use std::collections::HashMap; #[cfg(feature = "file_io")] use std::path::Path; +use uuid::Uuid; + +/// Function that is used by serde to determine whether or not we should serialize +/// thumbnail data based on the "serialize_thumbnails" flag (serialization is disabled by default) +fn skip_serializing_thumbnails(value: &Option<(String, Vec<u8>)>) -> bool { + !cfg!(feature = "serialize_thumbnails") || value.is_none() +} /// A Manifest represents all the information in a c2pa manifest #[derive(Debug, Deserialize, Serialize)] @@ -45,12 +50,20 @@ pub struct Manifest { /// Spaces are not allowed in names, versions can be specified with product/1.0 syntax pub claim_generator: String, + /// A human-readable title, generally source filename. + title: Option<String>, + + /// The format of the source file as a MIME type. + format: String, + + /// Instance ID from `xmpMM:InstanceID` in XMP metadata. + instance_id: String, + #[serde(skip_serializing_if = "Option::is_none")] claim_generator_hints: Option<HashMap<String, Value>>, - /// Information about the asset associated with this manifest - #[serde(skip_serializing_if = "Option::is_none")] - asset: Option<Ingredient>, + #[serde(skip_serializing_if = "skip_serializing_thumbnails")] + thumbnail: Option<(String, Vec<u8>)>, /// A List of ingredients ingredients: Vec<Ingredient>, @@ -77,9 +90,12 @@ impl Manifest { pub fn new<S: Into<String>>(claim_generator: S) -> Self { Self { vendor: None, + title: None, + format: "application/octet-stream".to_owned(), + instance_id: format!("xmp:iid:{}", Uuid::new_v4()), claim_generator: claim_generator.into(), claim_generator_hints: None, - asset: None, + thumbnail: None, ingredients: Vec::new(), assertions: Vec::new(), redactions: None, @@ -94,29 +110,24 @@ impl Manifest { /// Returns a MIME content_type for the asset associated with this manifest. pub fn format(&self) -> &str { - self.asset().map(|asset| asset.format()).unwrap_or_default() + &self.format } /// Returns the instance identifier. pub fn instance_id(&self) -> &str { - self.asset() - .map(|asset| asset.instance_id()) - .unwrap_or_default() + &self.instance_id } /// Returns a user-displayable title for this manifest pub fn title(&self) -> Option<&str> { - self.asset().map(|asset| asset.title()) + self.title.as_deref() } /// Returns a tuple with thumbnail format and image bytes or `None`. pub fn thumbnail(&self) -> Option<(&str, &[u8])> { - self.asset().and_then(|asset| asset.thumbnail()) - } - - /// Returns an [Ingredient] reference to the asset associated with this manifest - pub(crate) fn asset(&self) -> Option<&Ingredient> { - self.asset.as_ref() + self.thumbnail + .as_ref() + .map(|(format, image)| (format.as_str(), image.as_ref())) } /// Returns the [Ingredient]s used by this Manifest @@ -149,9 +160,27 @@ impl Manifest { self } - /// Sets an ingredient as the container asset - pub fn set_asset(&mut self, ingredient: Ingredient) -> &mut Self { - self.asset = Some(ingredient); + /// Sets a human-readable title for this ingredient. + pub fn set_format<S: Into<String>>(&mut self, format: S) -> &mut Self { + self.format = format.into(); + self + } + + /// Sets a human-readable title for this ingredient. + pub fn set_instance_id<S: Into<String>>(&mut self, instance_id: S) -> &mut Self { + self.instance_id = instance_id.into(); + self + } + + /// Sets a human-readable title for this ingredient. + pub fn set_title<S: Into<String>>(&mut self, title: S) -> &mut Self { + self.title = Some(title.into()); + self + } + + /// Sets the thumbnail format and image data. + pub fn set_thumbnail<S: Into<String>>(&mut self, format: S, thumbnail: Vec<u8>) -> &mut Self { + self.thumbnail = Some((format.into(), thumbnail)); self } @@ -351,16 +380,18 @@ impl Manifest { manifest.claim_generator_hints = claim.get_claim_generator_hint_map().cloned(); // get credentials converting from AssertionData to Value - manifest.credentials = Some( - claim - .get_verifiable_credentials() - .iter() - .filter_map(|d| match d { - AssertionData::Json(s) => serde_json::from_str(s).ok(), - _ => None, - }) - .collect(), - ); + let credentials: Vec<Value> = claim + .get_verifiable_credentials() + .iter() + .filter_map(|d| match d { + AssertionData::Json(s) => serde_json::from_str(s).ok(), + _ => None, + }) + .collect(); + + if !credentials.is_empty() { + manifest.credentials = Some(credentials); + } manifest.redactions = claim.redactions().map(|rs| { rs.iter() @@ -368,11 +399,16 @@ impl Manifest { .collect() }); - let title = claim.title().map_or("".to_owned(), |s| s.to_owned()); - let format = claim.format().to_owned(); - let instance_id = claim.instance_id().to_owned(); + // let title = claim.title().map_or("".to_owned(), |s| s.to_owned()); + // let format = claim.format().to_owned(); + // let instance_id = claim.instance_id().to_owned(); + if let Some(title) = claim.title() { + manifest.set_title(title); + } + manifest.set_format(claim.format()); + manifest.set_instance_id(claim.instance_id()); - let mut asset = Ingredient::new(&title, &format, &instance_id); + //let mut asset = Ingredient::new(&title, &format, &instance_id); for claim_assertion in claim.claim_assertion_store().iter() { let assertion = claim_assertion.assertion(); @@ -387,7 +423,7 @@ impl Manifest { } label if label.starts_with(labels::CLAIM_THUMBNAIL) => { let thumbnail = Thumbnail::from_assertion(assertion)?; - asset.set_thumbnail(thumbnail.content_type, thumbnail.data); + manifest.set_thumbnail(thumbnail.content_type, thumbnail.data); } _ => { // inject assertions for all other assertions @@ -414,7 +450,7 @@ impl Manifest { } } - manifest.set_asset(asset); + // manifest.set_asset(asset); let issuer = claim.signing_issuer(); let signing_time = claim @@ -439,19 +475,20 @@ impl Manifest { #[cfg(feature = "file_io")] pub fn set_asset_from_path<P: AsRef<Path>>(&mut self, path: P) { // Gather the information we need from the target path - let mut ingredient = Ingredient::from_file_info(path.as_ref()); + let ingredient = Ingredient::from_file_info(path.as_ref()); - if let Ok((format, image)) = make_thumbnail(path.as_ref()) { - ingredient.set_thumbnail(format, image); - } + self.set_format(ingredient.format()); + self.set_instance_id(ingredient.instance_id()); // if there is already an asset title preserve it - if let Some(title) = self.asset.as_ref().map(|i| i.title()) { - ingredient.set_title(title.to_string()); - }; + if self.title().is_none() { + self.set_title(ingredient.title()); + } - // set asset to newly created ingredient - self.asset = Some(ingredient); + #[cfg(feature = "add_thumbnails")] + if let Ok((format, image)) = crate::utils::thumbnail::make_thumbnail(path.as_ref()) { + self.set_thumbnail(format, image); + } } // Convert a Manifest into a Store @@ -465,6 +502,18 @@ impl Manifest { ); let mut claim = Claim::new(&generator, self.vendor.as_deref()); + if let Some(title) = self.title() { + claim.set_title(Some(title.to_owned())); + } + claim.format = self.format().to_owned(); + claim.instance_id = self.instance_id().to_owned(); + if let Some((format, image)) = self.thumbnail() { + claim.add_assertion(&Thumbnail::new( + &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, format), + image.to_vec(), + ))?; + } + // add any verified credentials - needs to happen early so we can reference them let mut vc_table = HashMap::new(); if let Some(verified_credentials) = self.credentials.as_ref() { @@ -475,19 +524,6 @@ impl Manifest { } } - // if the Manifest has an asset field use it to set these claim fields - if let Some(asset) = self.asset.as_ref() { - claim.set_title(Some(asset.title().to_owned())); - claim.format = asset.format().to_owned(); - claim.instance_id = asset.instance_id().to_owned(); - if let Some((format, image)) = asset.thumbnail() { - claim.add_assertion(&Thumbnail::new( - &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, format), - image.to_vec(), - ))?; - } - } - // add all ingredients to the claim for ingredient in &self.ingredients { ingredient.add_to_claim(&mut claim, self.redactions.clone())?; @@ -747,8 +783,11 @@ pub(crate) mod tests { assert_eq!(manifest.format(), "image/jpeg"); assert_eq!(manifest.title(), Some("wc_embed_test.jpg")); - assert!(manifest.thumbnail().is_some()); - + if cfg!(feature = "add_thumbnails") { + assert!(manifest.thumbnail().is_some()); + } else { + assert!(manifest.thumbnail().is_none()); + } let ingredient = Ingredient::from_file(&test_output).expect("load_from_asset"); assert!(ingredient.active_manifest().is_some()); } diff --git a/sdk/src/openssl/temp_signer_async.rs b/sdk/src/openssl/temp_signer_async.rs @@ -19,6 +19,7 @@ //! the asynchronous signing of claims. //! This module should be used only for testing purposes. +#[cfg(feature = "async_signer")] use crate::SigningAlg; #[cfg(feature = "async_signer")] diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs @@ -16,7 +16,7 @@ pub(crate) mod cbor_types; pub(crate) mod hash_utils; #[allow(dead_code)] // for wasm build pub(crate) mod patch; -#[cfg(feature = "file_io")] +#[cfg(all(feature = "file_io", feature = "add_thumbnails"))] pub(crate) mod thumbnail; pub(crate) mod time_it; #[allow(dead_code)] // for wasm builds diff --git a/sdk/src/utils/thumbnail.rs b/sdk/src/utils/thumbnail.rs @@ -12,7 +12,7 @@ // each license. use crate::Result; -use image::{GenericImageView, ImageFormat}; +use image::ImageFormat; /// utility to generate a thumbnail from a file at path /// returns Result (format, image_bits) if successful, otherwise Error @@ -40,9 +40,10 @@ pub fn make_thumbnail(path: &std::path::Path) -> Result<(String, Vec<u8>)> { "image/jpeg", ), }; - let mut thumbnail_bits = Vec::new(); - img.write_to(&mut thumbnail_bits, output_format)?; + let thumbnail_bits = Vec::new(); + let mut cursor = std::io::Cursor::new(thumbnail_bits); + img.write_to(&mut cursor, output_format)?; let format = content_type.to_owned(); - Ok((format, thumbnail_bits)) + Ok((format, cursor.into_inner())) } diff --git a/sdk/tests/integration.rs b/sdk/tests/integration.rs @@ -64,10 +64,6 @@ mod integration_1 { // set the parent ingredient manifest.set_parent(parent)?; - // edit our image - let mut img = image::open(&parent_path)?; - img = img.brighten(50); // brighten the image - actions = actions.add_action( Action::new("c2pa.edit").set_parameter("name".to_owned(), "brightnesscontrast")?, ); @@ -75,11 +71,6 @@ mod integration_1 { // add an ingredient let ingredient = Ingredient::from_file(&ingredient_path)?; - // now place an image in the image - let img_ingredient = image::open(&ingredient_path)?; - let img_small = img_ingredient.thumbnail(500, 500); - image::imageops::overlay(&mut img, &img_small, 0, 0); - // add an action assertion stating that we imported this file actions = actions.add_action( Action::new(c2pa_action::EDITED) @@ -92,17 +83,9 @@ mod integration_1 { manifest.add_assertion(&actions)?; - // now place an image in the image - let img_ingredient = image::open(&ingredient_path)?; - let img_small = img_ingredient.thumbnail(500, 500); - image::imageops::overlay(&mut img, &img_small, 0, 0); - - // save the edited image to our output path - img.save(&output_path)?; - // sign and embed into the target file let signer = get_temp_signer(); - manifest.embed(&output_path, &output_path, &*signer)?; + manifest.embed(&parent_path, &output_path, &*signer)?; // read our new file with embedded manifest let manifest_store = ManifestStore::from_file(&output_path)?;