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 67c206e317131b0270ffdea9a510ef3883b72dc5
parent 36493ab66c398d3a8eb70a945d04919a8f4fe49f
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Thu,  2 Jun 2022 08:52:17 -0700

Convert make_tests into a scriptable engine; rename to make_test_images (#29)

* Move make_tests to bin and drive it from a config file

* doc updates, change tsa to ta

* add dependabot support for make_tests

* remove unused dependencies

* Add a make_tests unit test

* Change a short name to longer awkward one
Diffstat:
M.github/dependabot.yml | 5+++++
MCargo.toml | 2+-
MMakefile | 4++--
Amake_test_images/Cargo.toml | 22++++++++++++++++++++++
Amake_test_images/src/main.rs | 38++++++++++++++++++++++++++++++++++++++
Amake_test_images/src/make_test_images.rs | 400+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amake_test_images/tests.json | 32++++++++++++++++++++++++++++++++
Msdk/Cargo.toml | 1-
Dsdk/examples/make_tests/main.rs | 43-------------------------------------------
Dsdk/examples/make_tests/make_tests.rs | 361-------------------------------------------------------------------------------
Msdk/src/lib.rs | 3+--
11 files changed, 501 insertions(+), 410 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml @@ -12,3 +12,8 @@ updates: directory: "c2patool" schedule: interval: "daily" + + - package-ecosystem: "cargo" + directory: "make_test_images" + schedule: + interval: "daily" diff --git a/Cargo.toml b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["sdk", "c2patool"] +members = ["sdk", "c2patool", "make_test_images"] diff --git a/Makefile b/Makefile @@ -80,10 +80,10 @@ endif doc: cargo doc --no-deps --open -# Builds a set of test images using the make_tests example +# Builds a set of test images using the make_test_images example # Outputs to release/test-images images: - cargo run --release --example make_tests + cargo run --release --example make_test_images # Runs the client example using test image and output to target/tmp/client.jpg client: diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "make_test_images" +version = "0.1.0" +authors = ["Gavin Peacock <gpeacock@adobe.com>"] +license = "MIT OR Apache-2.0" +edition = "2018" +rust-version = "1.58.0" + +[dependencies] +anyhow = "1.0" +c2pa = { path="../sdk", features = ["file_io"] } +env_logger = "0.9" +log = "0.4" +tempfile = "3.3" +image = "0.23.10" +nom = "7.1.1" +regex = "1.5.6" +serde = "1.0.137" +serde_json = "1.0.81" +twoway = "0.2.2" + + diff --git a/make_test_images/src/main.rs b/make_test_images/src/main.rs @@ -0,0 +1,38 @@ +// 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. + +//! This generates a set of test images with a wide variety of configurations +//! To run this, use the following command in a terminal +//! cargo run --release --bin make_test_images +//! +mod make_test_images; +use anyhow::{Context, Result}; + +fn main() -> Result<()> { + let args: Vec<String> = std::env::args().collect(); + let path = if args.len() > 1 { + args[1].as_ref() + } else { + "make_test_images/tests.json" + }; + let buf = std::fs::read_to_string(&path).context(format!("Reading {}", path))?; + let config: make_test_images::Config = + serde_json::from_str(&buf).context("Config file format")?; + + // set RUST_LOG=debug to get detailed debug logging + env_logger::init(); + + make_test_images::MakeTestImages::new(config).run()?; + + Ok(()) +} diff --git a/make_test_images/src/make_test_images.rs b/make_test_images/src/make_test_images.rs @@ -0,0 +1,400 @@ +// 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. + +//! Constructs a set of test images using a configuration script +//! +use c2pa::{ + assertions::{c2pa_action, Action, Actions, CreativeWork, SchemaDotOrgPerson}, + jumbf_io, + openssl::temp_signer::get_signer_by_alg, + Error, Ingredient, IngredientOptions, Manifest, ManifestStore, +}; + +use anyhow::{Context, Result}; +use image::GenericImageView; +use nom::AsBytes; +use serde::Deserialize; +use std::{ + fs, + path::{Path, PathBuf}, +}; +use tempfile::tempdir; +use twoway::find_bytes; + +const IMAGE_WIDTH: u32 = 2048; +const IMAGE_HEIGHT: u32 = 1365; + +/// Defines an operation for creating a test image +#[derive(Debug, Deserialize)] +pub struct Recipe { + /// The operation to perform: + /// + /// One of: "copy", "make", "ogp", "dat", "sig", "uri", "clm", "prv" + pub op: String, + /// Path or filename of parent + /// + /// Assumes output folder if no path + /// Will add default extension if non specified + pub parent: Option<String>, + /// A list of Ingredient paths + /// + /// Assumes output folder if no path + /// Will add default extension if non specified + pub ingredients: Option<Vec<String>>, + /// The folder to write files to, will create if it does not exist + pub output: String, +} + +/// Configuration +#[derive(Debug, Deserialize)] +#[serde(default)] +pub struct Config { + /// The signing algorithm to use + pub alg: String, + /// A url to a time authority if desired + pub ta: Option<String>, + /// The output folder for the generated files + pub output_path: String, + /// Extension to add to filenames if none was given + pub default_ext: String, + /// A name for a Creative Work Author assertion + pub author: Option<String>, + /// A list of recipes for test files + pub recipes: Vec<Recipe>, +} + +// Defaults for Config +impl Default for Config { + fn default() -> Self { + Self { + alg: "ps256".to_owned(), + ta: None, + output_path: "target/images".to_owned(), + default_ext: "jpg".to_owned(), + author: None, + recipes: Vec::new(), + } + } +} + +/// Tool for building test case images for C2PA +pub struct MakeTestImages { + config: Config, + output_dir: PathBuf, +} + +impl MakeTestImages { + pub fn new(config: Config) -> Self { + let output = config.output_path.to_owned(); + Self { + config, + output_dir: PathBuf::from(output), + } + } + + /// Makes a full path from a filename or path + /// + /// If there is no parent, prepend the output path + /// If there is no extension, use the default + fn make_path(&self, s: &str) -> PathBuf { + let mut path_buf = PathBuf::from(s); + // parent() tends to return an empty string instead of None + let has_path = match path_buf.parent() { + Some(p) => p.to_string_lossy().len() > 0, + None => false, + }; + // if we just have a filename, then assume it is in the output folder + if !has_path { + path_buf = PathBuf::from(&self.output_dir); + path_buf.push(s); + } + // add the default extension is none is supplied + if path_buf.extension().is_none() { + path_buf.set_extension(&self.config.default_ext); + } + path_buf + } + + /// Patches new content into a file + /// + /// # Parameters + /// path - path to file to be patched + /// search_bytes - bytes to be replaced + /// replace_bytes - replacement bytes + fn patch_file(path: &std::path::Path, search_bytes: &[u8], replace_bytes: &[u8]) -> Result<()> { + let mut buf = fs::read(path)?; + + if let Some(splice_start) = find_bytes(&buf, search_bytes) { + buf.splice( + splice_start..splice_start + search_bytes.len(), + replace_bytes.iter().cloned(), + ); + } else { + return Err(Error::NotFound.into()); + } + + fs::write(path, &buf)?; + + Ok(()) + } + + /// Creates a test image with optional source and ingredients, out to dest + fn make_image(&self, recipe: &Recipe) -> Result<PathBuf> { + let src = recipe.parent.as_deref(); + let dst_path = self.make_path(&recipe.output); + println!("Creating {:?}", dst_path); + // keep track of all actions here + let mut actions = Actions::new(); + + let options = IngredientOptions { + make_hash: true, + title: None, + }; + + let generator = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + let mut manifest = Manifest::new(generator); + manifest.set_vendor("contentauth".to_owned()); // needed for generating error cases below + + if let Some(user) = self.config.author.as_ref() { + let creative_work = CreativeWork::new() + .add_author(SchemaDotOrgPerson::new().set_name(user.to_owned())?)?; + + manifest.add_assertion(&creative_work)?; + } + + // process parent first + let mut img = match src { + Some(src) => { + let src_path = &self.make_path(src); + + let parent = Ingredient::from_file_with_options(src_path, &options)?; + actions.add_action( + Action::new(c2pa_action::OPENED) + .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?, + ); + manifest.set_parent(parent)?; + + // load the image for editing + let mut img = + image::open(&src_path).context(format!("opening parent {:?}", src_path))?; + + // adjust brightness to show we made an edit + img = img.brighten(30); + actions.add_action( + Action::new(c2pa_action::COLOR_ADJUSTMENTS) + .set_parameter("name".to_owned(), "brightnesscontrast")?, + ); + img + } + None => { + // create a default image with a gradient + let mut img = image::DynamicImage::new_rgb8(IMAGE_WIDTH, IMAGE_HEIGHT); + if let Some(img_ref) = img.as_mut_rgb8() { + // fill image with a gradient + for (x, y, pixel) in img_ref.enumerate_pixels_mut() { + let r = (0.3 * x as f32) as u8; + let b = (0.3 * y as f32) as u8; + *pixel = image::Rgb([r, 100, b]); + } + } + actions + .add_action(Action::new(c2pa_action::CREATED)) + .add_action( + Action::new(c2pa_action::DRAWING) + .set_parameter("name".to_owned(), "gradient")?, + ); + + img + } + }; + + // then add all ingredients + if let Some(ing_vec) = &recipe.ingredients { + // scale ingredients to paste in top row of the image + let width = match ing_vec.len() as u32 { + 0 | 1 => img.width() / 2, + _ => img.width() / ing_vec.len() as u32, + }; + let height = img.height() as u32 / 2; + + let mut x = 0; + for ing in ing_vec { + let ing_path = &self.make_path(ing); + + // get the bits of the ingredient, resize it and overlay it on the base image + let img_ingredient = + image::open(&ing_path).context(format!("opening ingredient {:?}", ing_path))?; + let img_small = img_ingredient.thumbnail(width, height); + image::imageops::overlay(&mut img, &img_small, x, 0); + + // create and add the ingredient + let ingredient = Ingredient::from_file_with_options(ing_path, &options)?; + actions.add_action( + Action::new(c2pa_action::PLACED).set_parameter( + "identifier".to_owned(), + ingredient.instance_id().to_owned(), + )?, + ); + manifest.add_ingredient(ingredient); + + x += width; + } + // record what we did as an action (only need to record this once) + actions.add_action(Action::new(c2pa_action::RESIZED)); + } + + // save the changes to the image as our target file + img.save(&dst_path)?; + + // add all our actions as an assertion now. + manifest.add_assertion(&actions)?; // extra get required here, since actions is an array + + // now create store; sign claim and embed in target + let temp_dir = tempdir()?; + let (signer, _) = + get_signer_by_alg(&temp_dir.path(), &self.config.alg, self.config.ta.clone()); + + manifest.embed(&dst_path, &dst_path, signer.as_ref())?; + + Ok(dst_path) + } + + /// makes an off the golden path image from an existing image with a claim + fn make_ogp(&self, recipe: &Recipe) -> Result<PathBuf> { + let src = recipe.parent.as_deref().unwrap_or_default(); + let src_path = &self.make_path(src); + let dst_path = self.make_path(recipe.output.as_str()); + println!("Creating OGP {:?}", dst_path); + + let jumbf = jumbf_io::load_jumbf_from_file(&PathBuf::from(src_path)) + .context(format!("loading OGP {:?}", src_path))?; + // save the edited image to our destination file + let mut img = image::open(&Path::new(src_path)) + .context(format!("loading OGP image{:?}", src_path))?; + img = img.grayscale(); + img.save(&dst_path) + .context(format!("saving OGP image{:?}", &dst_path))?; + // write the original claim data to the edited image + jumbf_io::save_jumbf_to_file(&jumbf, &PathBuf::from(&dst_path), Some(&dst_path)) + .context(format!("OGP save_jumbf_to_file {:?}", &dst_path))?; + // The image library does not preserve any metadata so we have to write it ourselves. + // todo: should preserve all metadata and update instanceId. + Ok(dst_path) + } + + /// Generates various error conditions + fn make_err(&self, recipe: &Recipe) -> Result<PathBuf> { + let op = recipe.op.as_str(); + let src = recipe.parent.as_deref().unwrap_or_default(); + let dst_path = self.make_path(recipe.output.as_str()); + println!("Creating Error op={} {:?}", op, dst_path); + + let (search_bytes, replace_bytes) = match op { + // modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP) + "dat" => ( + b"W5M0MpCehiHzreSzNTczkc9d".as_bytes(), + b"W5M0MpCehiHzreSzdeadbeef".as_bytes(), + ), + // modify the claim_generator value inside the claim, the claim hash will no longer match the signature + "sig" => ( + b"make_test_images".as_bytes(), + b"make_test_xxxxxx".as_bytes(), + ), + // modify a value inside an actions assertion, the assertion hash will fail + "uri" => ( + b"brightnesscontrast".as_bytes(), + b"brightnessdeadbeef".as_bytes(), + ), + // modify a uri to a manifest so the manifest cannot be found (missing manifest) + "clm" => ( + b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(), + b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentbeef".as_bytes(), + ), + // modify the provenance uri so that is references a non-existing manifest + "prv" => ( + b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(), + b"dcterms:provenance=\"self#jumbf=/c2pa/contentbeef".as_bytes(), + ), + _ => panic!("bad parameter"), + }; + + std::fs::copy(&self.make_path(src), &dst_path).context("copying for make_err")?; + + Self::patch_file(&dst_path, search_bytes, replace_bytes) + .context(format!("patching {}", op))?; + + Ok(dst_path) + } + + /// copies a file from the parent to the output + fn make_copy(&self, recipe: &Recipe) -> Result<PathBuf> { + let dst_path = self.make_path(recipe.output.as_str()); + println!("Copying {:?}", dst_path); + let src = recipe.parent.as_deref().unwrap_or_default(); + let dst = recipe.output.as_str(); + std::fs::copy(src, &dst_path).context(format!("copying {} to {}", src, dst))?; + Ok(dst_path) + } + + /// Runs a list of recipes + pub fn run(&self) -> Result<()> { + if !self.output_dir.exists() { + std::fs::create_dir_all(&self.output_dir).context("Can't create output folder")?; + }; + + let recipes = &self.config.recipes; + for recipe in recipes { + let dst_path = match recipe.op.as_str() { + "make" => self.make_image(recipe)?, + "ogp" => self.make_ogp(recipe)?, + "dat" | "sig" | "uri" | "clm" | "prv" => self.make_err(recipe)?, + "copy" => self.make_copy(recipe)?, + _ => return Err(Error::BadParam(recipe.op.to_string()).into()), + }; + let manifest_store = ManifestStore::from_file(&dst_path); + + if recipe.op.as_str() != "copy" { + println!("{}", manifest_store?); + } + } + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + #![allow(clippy::expect_used)] + + use super::*; + const TESTS: &str = r#"{ + "alg": "ps256", + "tsa": "http://timestamp.digicert.com", + "output_path": "../target/tmp", + "default_ext": "jpg", + "author": "Gavin Peacock", + "recipes": [ + { "op": "copy", "parent": "../sdk/tests/fixtures/IMG_0003.jpg", "output": "A.jpg" }, + { "op": "make", "output": "C" }, + { "op": "ogp", "parent": "C", "output": "XC" }, + { "op": "sig", "parent": "C", "output": "E-sig-C" } + ] + }"#; + + #[test] + fn test_make_images() { + let config: Config = serde_json::from_str(TESTS) + .context("Config file format") + .expect("serde_json"); + MakeTestImages::new(config).run().expect("running"); + } +} diff --git a/make_test_images/tests.json b/make_test_images/tests.json @@ -0,0 +1,31 @@ +{ + "alg": "ps256", + "tsa": "http://timestamp.digicert.com", + "output_path": "target/images", + "default_ext": "jpg", + "author": "Gavin Peacock", + "recipes": [ + { "op": "copy", "parent": "sdk/tests/fixtures/IMG_0003.jpg", "output": "A.jpg" }, + { "op": "copy", "parent": "sdk/tests/fixtures/P1000827.jpg", "output": "I.jpg" }, + { "op": "make", "output": "C" }, + { "op": "make", "parent": "A.jpg", "output": "CA" }, + { "op": "make", "parent": "CA", "output": "CACA" }, + { "op": "make", "ingredients": ["I.jpg"], "output": "CI" }, + { "op": "make", "ingredients": ["I.jpg","I.jpg"], "output": "CII" }, + { "op": "make", "parent": "A.jpg", "ingredients": ["I.jpg"], "output": "CAI" }, + { "op": "make", "parent": "A.jpg", "ingredients": ["CA"], "output": "CAICA" }, + { "op": "make", "ingredients": ["CA"], "output": "CICA" }, + { "op": "make", "parent": "A.jpg", "ingredients": ["CAI"], "output": "CAICAI" }, + { "op": "make", "parent": "CAICA", "ingredients": ["CICA"], "output": "CACAICAICICA" }, + { "op": "make", "ingredients": ["CA","CA","CA"], "output": "CICACACA" }, + { "op": "ogp", "parent": "CA", "output": "XCA" }, + { "op": "ogp", "parent": "CI", "output": "XCI" }, + { "op": "dat", "parent": "CA", "output": "E-dat-CA" }, + { "op": "sig", "parent": "CA", "output": "E-sig-CA" }, + { "op": "uri", "parent": "CA", "output": "E-uri-CA" }, + { "op": "clm", "parent": "CAICAI", "output": "E-clm-CAICAI" }, + { "op": "make", "ingredient": ["E-sig-CA"], "output": "CIE-sig-CA" }, + { "op": "uri", "parent": "CIE-sig-CA", "output": "E-uri-CIE-sig-CA" }, + { "op": "make", "parent": "A.jpg", "ingredients": ["C", "A.jpg", "I.jpg", "CA", "CI", "CAI", "CICA"], "output": "CAIAIIICAICIICAIICICA" } + ] +} +\ No newline at end of file diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -81,7 +81,6 @@ web-sys = { version = "0.3.54", features = ["Crypto", "SubtleCrypto", "CryptoKey [dev-dependencies] anyhow = "1.0.40" -env_logger = "0.7.1" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.0" diff --git a/sdk/examples/make_tests/main.rs b/sdk/examples/make_tests/main.rs @@ -1,43 +0,0 @@ -// 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. - -//! This generates a set of test images with a wide variety of configurations -//! To run this, use the following command in a terminal -//! cargo run --release --example make_tests -//! -use anyhow::Result; -use std::path::PathBuf; -// The make_tests sample is not designed to work with a wasm build -// so we provide a wasm stub here and only include the module for non wasm -#[cfg(not(target_arch = "wasm32"))] -mod make_tests; -#[cfg(not(target_arch = "wasm32"))] -use crate::make_tests::make_tests; - -#[cfg(target_arch = "wasm32")] -fn make_tests(_output_folder: &std::path::Path, _alg: &str, _tsa: Option<String>) -> Result<()> { - panic!("Not implemented for wasm"); -} - -const TARGET_FOLDER: &str = "target/test_images"; -fn main() -> Result<()> { - // set RUST_LOG=debug to get detailed debug logging - env_logger::init(); - - // choose a timestamp service authority - let tsa = Some("http://timestamp.digicert.com".to_string()); - - make_tests(&PathBuf::from(TARGET_FOLDER), "ps256", tsa)?; - - Ok(()) -} diff --git a/sdk/examples/make_tests/make_tests.rs b/sdk/examples/make_tests/make_tests.rs @@ -1,361 +0,0 @@ -// 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. - -use anyhow::{Context, Result}; - -use c2pa::{ - assertions::{c2pa_action, Action, Actions, CreativeWork, SchemaDotOrgPerson}, - jumbf_io, - openssl::temp_signer::get_signer_by_alg, - Error, Ingredient, IngredientOptions, Manifest, ManifestStore, -}; - -use image::GenericImageView; -use nom::AsBytes; -use tempfile::tempdir; -use twoway::find_bytes; - -use std::{ - fs, - path::{Path, PathBuf}, -}; - -const GENERATOR: &str = "make_tests"; -const USER: &str = "Joe Bloggs"; - -const IMAGE_WIDTH: u32 = 2048; -const IMAGE_HEIGHT: u32 = 1365; - -/** -Patch new content into a file -path - path to file to be patched -search_bytes - bytes to be replaced -replace_bytes - replacement bytes -*/ -pub fn patch_file(path: &std::path::Path, search_bytes: &[u8], replace_bytes: &[u8]) -> Result<()> { - let mut buf = fs::read(path)?; - - if let Some(splice_start) = find_bytes(&buf, search_bytes) { - buf.splice( - splice_start..splice_start + search_bytes.len(), - replace_bytes.iter().cloned(), - ); - } else { - return Err(Error::NotFound.into()); - } - - fs::write(path, &buf)?; - - Ok(()) -} - -pub struct MakeTests { - output_dir: PathBuf, -} - -impl MakeTests { - pub fn new(path: &Path) -> Self { - Self { - output_dir: PathBuf::from(path), - } - } - - pub fn output_dir(&self) -> PathBuf { - self.output_dir.to_owned() - } - - fn make_path(&self, s: &str) -> PathBuf { - //let output_dir = unsafe { OUTPUT_FOLDER.as_ref().unwrap().lock().unwrap().to_string() }; - let mut path_buf = PathBuf::from(&self.output_dir); - path_buf.push(s); - if path_buf.extension().is_none() { - path_buf.set_extension("jpg"); - } - path_buf - } - - // create a test image with optional source and ingredients, out to dest - pub fn make_image( - &self, - src: Option<&str>, - ing: Option<&Vec<&str>>, - dst: &str, - alg: &str, - tsa: Option<String>, - ) -> Result<()> { - let dst_path = &self.make_path(dst); - println!("creating {:?}", dst_path); - // keep track of all actions here - let mut actions = Actions::new(); - - let options = IngredientOptions { - make_hash: true, - title: None, - }; - - let mut manifest = Manifest::new(GENERATOR.to_string()); - manifest.set_vendor("contentauth".to_owned()); // needed for generating error cases below - - let creative_work = - CreativeWork::new().add_author(SchemaDotOrgPerson::new().set_name(USER.to_owned())?)?; - - manifest.add_assertion(&creative_work)?; - - // process parent first - let mut img = match src { - Some(src) => { - let src_path = &self.make_path(src); - - let parent = Ingredient::from_file_with_options(src_path, &options)?; - actions.add_action( - Action::new(c2pa_action::OPENED) - .set_parameter("identifier".to_owned(), parent.instance_id().to_owned())?, - ); - manifest.set_parent(parent)?; - - // load the image for editing - let mut img = - image::open(&src_path).context(format!("opening image A {:?}", src_path))?; - - // adjust brightness to show we made an edit - img = img.brighten(30); - actions.add_action( - Action::new(c2pa_action::COLOR_ADJUSTMENTS) - .set_parameter("name".to_owned(), "brightnesscontrast")?, - ); - img - } - None => { - // create a default image with a gradient - let mut img = image::DynamicImage::new_rgb8(IMAGE_WIDTH, IMAGE_HEIGHT); - if let Some(img_ref) = img.as_mut_rgb8() { - // fill image with a gradient - for (x, y, pixel) in img_ref.enumerate_pixels_mut() { - let r = (0.3 * x as f32) as u8; - let b = (0.3 * y as f32) as u8; - *pixel = image::Rgb([r, 100, b]); - } - } - actions - .add_action(Action::new(c2pa_action::CREATED)) - .add_action( - Action::new(c2pa_action::DRAWING) - .set_parameter("name".to_owned(), "gradient")?, - ); - - img - } - }; - - // then add all ingredients - if let Some(ing_vec) = ing { - // scale ingredients to paste in top row of the image - let width = match ing_vec.len() as u32 { - 0 | 1 => img.width() / 2, - _ => img.width() / ing_vec.len() as u32, - }; - let height = img.height() as u32 / 2; - - let mut x = 0; - for ing in ing_vec { - let ing_path = &self.make_path(ing); - - // get the bits of the ingredient, resize it and overlay it on the base image - let img_ingredient = - image::open(&ing_path).context(format!("opening image I {:?}", ing_path))?; - let img_small = img_ingredient.thumbnail(width, height); - image::imageops::overlay(&mut img, &img_small, x, 0); - - // create and add the ingredient - let ingredient = Ingredient::from_file_with_options(ing_path, &options)?; - actions.add_action( - Action::new(c2pa_action::PLACED).set_parameter( - "identifier".to_owned(), - ingredient.instance_id().to_owned(), - )?, - ); - manifest.add_ingredient(ingredient); - - x += width; - } - // record what we did as an action (only need to record this once) - actions.add_action(Action::new(c2pa_action::RESIZED)); - } - - // save the changes to the image as our target file - img.save(dst_path)?; - - // add all our actions as an assertion now. - manifest.add_assertion(&actions)?; // extra get required here, since actions is an array - - // now create store; sign claim and embed in target - let temp_dir = tempdir()?; - let (signer, _) = get_signer_by_alg(&temp_dir.path(), alg, tsa); - - manifest.embed(dst_path, dst_path, signer.as_ref())?; - - println!("{}", ManifestStore::from_file(dst_path)?); - - Ok(()) - } - - // make an off the golden path image from an existing image with a claim - fn make_ogp(&self, src: &str, dst: &str) -> Result<()> { - println!("creating OGP {}", dst); - let src_path = &self.make_path(src); - let dst_path = &self.make_path(dst); - let jumbf = jumbf_io::load_jumbf_from_file(&PathBuf::from(src_path)) - .context(format!("loading OGP {:?}", src_path))?; - // save the edited image to our destination file - let mut img = image::open(&Path::new(src_path)) - .context(format!("loading OGP image{:?}", src_path))?; - img = img.grayscale(); - img.save(dst_path) - .context(format!("saving OGP image{:?}", dst_path))?; - // write the original claim data to the edited image - jumbf_io::save_jumbf_to_file( - &jumbf, - &PathBuf::from(dst_path), - Some(&PathBuf::from(dst_path)), - ) - .context(format!("OGP save_jumbf_to_file {:?}", dst_path))?; - // The image library does not preserve any metadata so we have to write it ourselves. - // todo: should preserve all metadata and update instanceId. - Ok(()) - } - - fn make_err(&self, src: &str, err: &str) -> Result<()> { - let (search_bytes, replace_bytes) = match err { - // modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP) - "dat" => ( - b"W5M0MpCehiHzreSzNTczkc9d".as_bytes(), - b"W5M0MpCehiHzreSzdeadbeef".as_bytes(), - ), - // modify the claim_generator value inside the claim, the claim hash will no longer match the signature - "sig" => (b"make_tests".as_bytes(), b"make_xxxxx".as_bytes()), - // modify a value inside an actions assertion, the assertion hash will fail - "uri" => ( - b"brightnesscontrast".as_bytes(), - b"brightnessdeadbeef".as_bytes(), - ), - // modify a uri to a manifest so the manifest cannot be found (missing manifest) - "clm" => ( - b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(), - b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth".as_bytes(), - ), - // modify the provenance uri so that is references a non-existing manifest - "prv" => ( - b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(), - b"dcterms:provenance=\"self#jumbf=/c2pa/contentauth".as_bytes(), - ), - _ => panic!("bad parameter"), - }; - - let dst = format!("E-{}-{}", err, src); - std::fs::copy(&self.make_path(src), &self.make_path(&dst)) - .context("copying for make_err")?; - patch_file(&self.make_path(&dst), search_bytes, replace_bytes) - .context(format!("patching {}", err))?; - - Ok(()) - } -} - -pub fn make_tests(output_folder: &Path, alg: &str, tsa: Option<String>) -> Result<()> { - // destination folder for this content - let mt = MakeTests::new(output_folder); - let data_dir = mt.output_dir(); - - if !data_dir.exists() { - std::fs::create_dir_all(&data_dir).expect("Can't create C2PA data directory"); - }; - - // copy A and I source images into destination folder - // these images should have no claims - std::fs::copy("sdk/tests/fixtures/IMG_0003.jpg", &mt.make_path("A.jpg")) - .context("error copying A")?; - std::fs::copy("sdk/tests/fixtures/P1000827.jpg", &mt.make_path("I.jpg")) - .context("error copying I")?; - - // -------------------------------------------------------------------- - - //make_cai(None, Some(&vec!["PS.svg"]), "CIPS")?; - mt.make_image(None, None, "C", alg, tsa.clone())?; - mt.make_image(Some("A"), None, "CA", alg, tsa.clone())?; - mt.make_image(Some("CA"), None, "CACA", alg, tsa.clone())?; - mt.make_image(None, Some(&vec!["I"]), "CI", alg, tsa.clone())?; - mt.make_image(None, Some(&vec!["I", "I"]), "CII", alg, tsa.clone())?; - mt.make_image( - None, - Some(&vec!["I", "I", "I", "I", "I"]), - "CIIIII", - alg, - tsa.clone(), - )?; - mt.make_image(Some("A"), Some(&vec!["I"]), "CAI", alg, tsa.clone())?; - mt.make_image(Some("A"), Some(&vec!["CA"]), "CAICA", alg, tsa.clone())?; - mt.make_image(None, Some(&vec!["CA"]), "CICA", alg, tsa.clone())?; - mt.make_image(Some("CA"), Some(&vec!["CAI"]), "CAICAI", alg, tsa.clone())?; - mt.make_image(None, Some(&vec!["CA"]), "CICA", alg, tsa.clone())?; - mt.make_image( - Some("CAICA"), - Some(&vec!["CICA"]), - "CACAICAICICA", - alg, - tsa.clone(), - )?; - mt.make_image( - None, - Some(&vec!["CA", "CA", "CA"]), - "CICACACA", - alg, - tsa.clone(), - )?; - - mt.make_ogp("CA", "XCA")?; - mt.make_ogp("CI", "XCI")?; - mt.make_image(Some("CA"), Some(&vec!["XCI"]), "CAIXCI", alg, tsa.clone())?; - mt.make_image( - Some("XCA"), - Some(&vec!["XCI"]), - "CAXCAIXCI", - alg, - tsa.clone(), - )?; - - mt.make_err("CA", "dat")?; - mt.make_err("CA", "sig")?; - mt.make_err("CA", "uri")?; - mt.make_err("CAICAI", "clm")?; - mt.make_err("CA", "prv")?; - mt.make_image( - None, - Some(&vec!["E-sig-CA"]), - "CIE-sig-CA", - alg, - tsa.clone(), - )?; - // inject an assertion error into a claim that has an accepted error - mt.make_err("CIE-sig-CA", "uri")?; - - mt.make_image( - Some("A"), - Some(&vec!["C", "A", "I", "CA", "CI", "CAI", "CICA"]), - "CAIAIIICAICIICAIICICA", - alg, - tsa, - )?; - // // save the changes to the image and add the claim - println!("done"); - Ok(()) -} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs @@ -76,8 +76,7 @@ pub use error::{Error, Result}; mod ingredient; pub use ingredient::{Ingredient, IngredientOptions}; -pub mod jumbf_io; // used by make_tests - +pub mod jumbf_io; mod manifest; pub use manifest::{Manifest, ManifestAssertion};