commit ed6468ac42529e68db96be6096e631ed13c9a800
parent 62e88d6adaef1c48f8034ceb5cb39eda574265f5
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Thu, 29 Sep 2022 14:30:08 -0700
Add Exif Assertion support (#140)
* Add Exif Assertion support
* Ensure Exif is written in JSON
Diffstat:
4 files changed, 229 insertions(+), 3 deletions(-)
diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs
@@ -17,7 +17,7 @@ use std::path::PathBuf;
use anyhow::Result;
use c2pa::{
- assertions::{c2pa_action, labels, Action, Actions, CreativeWork, SchemaDotOrgPerson},
+ assertions::{c2pa_action, labels, Action, Actions, CreativeWork, Exif, SchemaDotOrgPerson},
create_signer, Ingredient, Manifest, ManifestStore, SigningAlg,
};
@@ -100,13 +100,28 @@ pub fn main() -> Result<()> {
let creative_work =
CreativeWork::new().add_author(SchemaDotOrgPerson::new().set_name("me")?)?;
+ let exif = Exif::from_json_str(
+ r#"{
+ "@context" : {
+ "exif": "http://ns.adobe.com/exif/1.0/"
+ },
+ "exif:GPSVersionID": "2.2.0.0",
+ "exif:GPSLatitude": "39,21.102N",
+ "exif:GPSLongitude": "74,26.5737W",
+ "exif:GPSAltitudeRef": 0,
+ "exif:GPSAltitude": "100963/29890",
+ "exif:GPSTimeStamp": "2019-09-22T18:22:57Z"
+ }"#,
+ )?;
+
// create a new Manifest
let mut manifest = Manifest::new(GENERATOR.to_owned());
// add parent and assertions
manifest
.set_parent(parent)?
.add_assertion(&actions)?
- .add_assertion(&creative_work)?;
+ .add_assertion(&creative_work)?
+ .add_assertion(&exif)?;
// sign and embed into the target file
let signcert_path = "sdk/tests/fixtures/certs/es256.pub";
diff --git a/sdk/src/assertions/exif.rs b/sdk/src/assertions/exif.rs
@@ -0,0 +1,203 @@
+// 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.
+
+//! Exif Assertion
+//!
+use std::collections::HashMap;
+
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde_json::{json, Value};
+
+use crate::{assertions::labels, Assertion, AssertionBase, AssertionJson, Error, Result};
+
+/// The EXIF assertion as defined in the C2PA spec section 17.13
+/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_exif_information>
+///
+/// This does not yet define or validate individual fields, but will ensure the correct assertion structure
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Exif {
+ #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
+ object_context: Option<Value>,
+ #[serde(flatten)]
+ value: HashMap<String, Value>,
+}
+
+impl Exif {
+ pub fn new() -> Self {
+ Self {
+ object_context: Some(json!({
+ "dc": "http://purl.org/dc/elements/1.1/",
+ "exifEX": "http://cipa.jp/exif/2.32/",
+ "exif": "http://ns.adobe.com/exif/1.0/",
+ "tiff": "http://ns.adobe.com/tiff/1.0/",
+ "xmp": "http://ns.adobe.com/xap/1.0/",
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ })),
+ value: HashMap::new(),
+ }
+ }
+
+ /// sets the @context field for Schema dot org.
+ pub fn set_context(mut self, context: Value) -> Self {
+ self.object_context = Some(context);
+ self
+ }
+
+ /// get values by key as an instance of type `T`.
+ /// This return T is owned, not a reference
+ /// # Errors
+ ///
+ /// This conversion can fail if the structure of the field at key does not match the
+ /// structure expected by `T`
+ pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
+ self.value
+ .get(key)
+ .and_then(|v| serde_json::from_value(v.clone()).ok())
+ }
+
+ /// insert key / value pair of instance of type `T`
+ /// # Errors
+ ///
+ /// This conversion can fail if `T`'s implementation of `Serialize` decides to
+ /// fail, or if `T` contains a map with non-string keys.
+ pub fn insert<S: Into<String>, T: Serialize>(mut self, key: S, value: T) -> Result<Self> {
+ self.value.insert(key.into(), serde_json::to_value(value)?);
+ Ok(self)
+ }
+
+ // add a value to a Vec stored at key
+ pub fn insert_push<S: Into<String>, T: Serialize + DeserializeOwned>(
+ self,
+ key: S,
+ value: T,
+ ) -> Result<Self> {
+ let key = key.into();
+ Ok(match self.get(&key) as Option<Vec<T>> {
+ Some(mut v) => {
+ v.push(value);
+ self
+ }
+ None => self.insert(&key, &Vec::from([value]))?,
+ })
+ }
+
+ /// creates the struct from a correctly formatted JSON string
+ pub fn from_json_str(json: &str) -> Result<Self> {
+ serde_json::from_slice(json.as_bytes()).map_err(Error::JsonError)
+ }
+}
+
+// Implementing default is a good idea
+impl Default for Exif {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+// Implement as AssertionJson
+impl AssertionJson for Exif {}
+
+impl AssertionBase for Exif {
+ // A label for our assertion, use reverse domain name syntax
+ const LABEL: &'static str = labels::EXIF;
+
+ fn to_assertion(&self) -> Result<Assertion> {
+ Self::to_json_assertion(self)
+ }
+
+ fn from_assertion(assertion: &Assertion) -> Result<Self> {
+ Self::from_json_assertion(assertion)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+ use crate::Manifest;
+
+ const SPEC_EXAMPLE: &str = r#"{
+ "@context" : {
+ "dc": "http://purl.org/dc/elements/1.1/",
+ "exifEX": "http://cipa.jp/exif/2.32/",
+ "exif": "http://ns.adobe.com/exif/1.0/",
+ "tiff": "http://ns.adobe.com/tiff/1.0/",
+ "xmp": "http://ns.adobe.com/xap/1.0/",
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ },
+ "exif:GPSVersionID": "2.2.0.0",
+ "exif:GPSLatitude": "39,21.102N",
+ "exif:GPSLongitude": "74,26.5737W",
+ "exif:GPSAltitudeRef": 0,
+ "exif:GPSAltitude": "100963/29890",
+ "exif:GPSTimeStamp": "2019-09-22T18:22:57Z",
+ "exif:GPSSpeedRef": "K",
+ "exif:GPSSpeed": "4009/161323",
+ "exif:GPSImgDirectionRef": "T",
+ "exif:GPSImgDirection": "296140/911",
+ "exif:GPSDestBearingRef": "T",
+ "exif:GPSDestBearing": "296140/911",
+ "exif:GPSHPositioningError": "13244/2207",
+ "exif:ExposureTime": "1/100",
+ "exif:FNumber": 4.0,
+ "exif:ColorSpace": 1,
+ "exif:DigitalZoomRatio": 2.0,
+ "tiff:Make": "CameraCompany",
+ "tiff:Model": "Shooter S1",
+ "exifEX:LensMake": "CameraCompany",
+ "exifEX:LensModel": "17.0-35.0 mm",
+ "exifEX:LensSpecification": { "@list": [ 1.55, 4.2, 1.6, 2.4 ] }
+ }"#;
+
+ #[test]
+ fn exif_new() {
+ let mut manifest = Manifest::new("my_app".to_owned());
+ let original = Exif::new()
+ .insert("exif:GPSLatitude", "39,21.102N")
+ .unwrap();
+ manifest.add_assertion(&original).expect("adding assertion");
+ println!("{}", manifest);
+ let exif: Exif = manifest
+ .find_assertion(Exif::LABEL)
+ .expect("find_assertion");
+ let latitude: String = exif.get("exif:GPSLatitude").unwrap();
+ assert_eq!(&latitude, "39,21.102N")
+ }
+
+ #[test]
+ fn exif_from_json() {
+ let mut manifest = Manifest::new("my_app".to_owned());
+ let original = Exif::from_json_str(SPEC_EXAMPLE).expect("from_json");
+ manifest.add_assertion(&original).expect("adding assertion");
+ println!("{}", manifest);
+ let exif: Exif = manifest
+ .find_assertion(Exif::LABEL)
+ .expect("find_assertion");
+ let latitude: String = exif.get("exif:GPSLatitude").unwrap();
+ assert_eq!(&latitude, "39,21.102N")
+ }
+
+ #[test]
+ fn exif_to_assertoin() {
+ let original = Exif::from_json_str(SPEC_EXAMPLE).expect("from_json");
+ let assertion = original.to_assertion().expect("to_assertion");
+ assert_eq!(assertion.content_type(), "application/json");
+ println!("{:?}", assertion);
+ let result = Exif::from_assertion(&assertion).expect("from_assertion");
+ println!("{:?}", result);
+ let latitude: String = result.get("exif:GPSLatitude").unwrap();
+ assert_eq!(&latitude, "39,21.102N")
+ }
+}
diff --git a/sdk/src/assertions/mod.rs b/sdk/src/assertions/mod.rs
@@ -25,6 +25,10 @@ pub(crate) use data_hash::DataHash;
mod creative_work;
pub use creative_work::CreativeWork;
+
+mod exif;
+pub use exif::Exif;
+
#[allow(dead_code)] // will become public later
mod ingredient;
pub(crate) use ingredient::{Ingredient, Relationship};
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -24,7 +24,7 @@ use uuid::Uuid;
use crate::Signer;
use crate::{
assertion::{AssertionBase, AssertionData},
- assertions::{labels, Actions, CreativeWork, Thumbnail, User, UserCbor},
+ assertions::{labels, Actions, CreativeWork, Exif, Thumbnail, User, UserCbor},
claim::{Claim, RemoteManifest},
error::{Error, Result},
jumbf,
@@ -625,6 +625,10 @@ impl Manifest {
}
claim.add_assertion_with_salt(&cw, &salt)
}
+ Exif::LABEL => {
+ let exif: Exif = manifest_assertion.to_assertion()?;
+ claim.add_assertion_with_salt(&exif, &salt)
+ }
_ => match manifest_assertion.kind() {
ManifestAssertionKind::Cbor => claim.add_assertion_with_salt(
&UserCbor::new(