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:
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)?;