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 20cc2122a6b31075c0809988f65c6dd1dab1bbf8
parent b276d47adc82dbb94b5930b321d48ae0b4fb6b87
Author: Dylan ross <dylan.ssor@gmail.com>
Date:   Wed,  1 Nov 2023 16:15:27 -0700

(PATCH) switches af relationship for a reference to the c2pa data to an array of references, one of which is the c2pa spec (#333)

* sets AF relationship of catalog to array instead of reference

* Add support for external manifest embedding of PDF

* adds error type

---------

Co-authored-by: Dylan Ross <dyross@adobe.com>
Co-authored-by: Maurice Fisher <mfisher@adobe.com>
Diffstat:
Msdk/src/asset_handlers/pdf.rs | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msdk/src/asset_handlers/pdf_io.rs | 13++++++++++++-
2 files changed, 101 insertions(+), 31 deletions(-)

diff --git a/sdk/src/asset_handlers/pdf.rs b/sdk/src/asset_handlers/pdf.rs @@ -51,6 +51,11 @@ pub enum Error { // the manifest bytes in the PDF's embedded files. #[error("Unable to find C2PA manifest in the PDF's embedded files.")] UnableToFindEmbeddedFileManifest, + + /// This error occurs when we an error was encountered trying to find the PDF's C2PA embedded + /// file specification in the array of Associated Files defined in the catalog. + #[error("Unable to find a C2PA embedded file specification in PDF's associated files array")] + FindingC2PAFileSpec, } const C2PA_MIME_TYPE: &str = "application/x-c2pa-manifest-store"; @@ -98,16 +103,9 @@ impl C2paPdf for Pdf { /// Determines if this PDF has a C2PA manifest embedded. /// /// This is done by checking if the Associated File key of the catalog points to a - /// [lopdf::Object::Dictionary] with an `AFRelationship` set to `C2PA_Manifest`. + /// [Object::Dictionary] with an `AFRelationship` set to `C2PA_Manifest`. fn has_c2pa_manifest(&self) -> bool { - self.document - .catalog() - .and_then(|catalog| catalog.get_deref(ASSOCIATED_FILE_KEY, &self.document)) - .and_then(Object::as_dict) - .and_then(|dict| dict.get_deref(AF_RELATIONSHIP_KEY, &self.document)) - .and_then(Object::as_name) - .map(|name| name == C2PA_RELATIONSHIP) - .unwrap_or_default() + self.c2pa_file_spec_object_id().is_some() } /// Writes the provided `bytes` to the PDF as an `EmbeddedFile`. @@ -116,7 +114,7 @@ impl C2paPdf for Pdf { let file_stream_ref = self.add_c2pa_embedded_file_stream(bytes); let file_spec_ref = self.add_embedded_file_specification(file_stream_ref); - self.set_af_relationship(file_spec_ref)?; + self.push_associated_file(file_spec_ref)?; let mut manifest_name_file_pair = vec![ Object::string_literal(CONTENT_CREDS), @@ -194,7 +192,7 @@ impl C2paPdf for Pdf { let file_stream_reference = self.add_c2pa_embedded_file_stream(bytes); let file_spec_reference = self.add_embedded_file_specification(file_stream_reference); - self.set_af_relationship(file_spec_reference)?; + self.push_associated_file(file_spec_reference)?; self.add_file_attachment_annotation(file_spec_reference)?; Ok(()) @@ -214,16 +212,15 @@ impl C2paPdf for Pdf { /// separately, due to PDF's "Incremental Update" feature. See the spec for more details: /// <https://c2pa.org/specifications/specifications/1.3/specs/C2PA_Specification.html#_embedding_manifests_into_pdfs> fn read_manifest_bytes(&self) -> Result<Option<Vec<&[u8]>>, Error> { - if !self.has_c2pa_manifest() { + let Some(id) = self.c2pa_file_spec_object_id() else { return Ok(None); }; Ok(Some(vec![ &self .document - .catalog()? - .get_deref(ASSOCIATED_FILE_KEY, &self.document)? - .as_dict()? + .get_object(id) + .and_then(Object::as_dict)? .get_deref(b"EF", &self.document)? .as_stream()? .content, @@ -237,10 +234,8 @@ impl C2paPdf for Pdf { // Find the File Spec, which contains the reference to the manifest. let file_spec_ref = self - .document - .catalog()? - .get(ASSOCIATED_FILE_KEY)? - .as_reference()?; + .c2pa_file_spec_object_id() + .ok_or_else(|| Error::NoManifest)?; // Find the manifest's file stream. let file_stream_ref = self @@ -259,8 +254,8 @@ impl C2paPdf for Pdf { self.remove_manifest_from_embedded_files() .or_else(|_| self.remove_manifest_from_annotations())?; - // Remove the AF_Relationship from the catalog. - self.document.catalog_mut()?.remove(AF_RELATIONSHIP_KEY); + // Remove C2PA associated files from the `AF` key in the catalog. + self.remove_c2pa_file_spec_reference()?; // Delete the manifest and its descriptor from the PDF self.document.delete_object(file_stream_ref); @@ -307,6 +302,58 @@ impl Pdf { Ok(Self { document }) } + /// Returns a reference to the Associated Files array from the PDF's Catalog. + fn associated_files(&self) -> Result<&Vec<Object>, Error> { + Ok(self + .document + .catalog()? + .get_deref(ASSOCIATED_FILE_KEY, &self.document)? + .as_array()?) + } + + /// Returns the [Object::ObjectId] of the C2PA File Spec Reference, if it is present in the + /// PDF's associated files array. + fn c2pa_file_spec_object_id(&self) -> Option<ObjectId> { + self.associated_files().ok()?.iter().find_map(|value| { + let Ok(reference) = value.as_reference() else { + return None; + }; + + let name = self + .document + .get_object(reference) + .and_then(Object::as_dict) + .and_then(|dict| dict.get_deref(AF_RELATIONSHIP_KEY, &self.document)) + .and_then(Object::as_name) + .ok()?; + + (name == C2PA_RELATIONSHIP).then_some(reference) + }) + } + + /// Removes the C2PA File Spec Reference if it exists in the Associated Files [Object::Array] of + /// PDF's catalog. This will return an [Err] if the PDF doesn't contain a C2PA File Spec + /// Reference. + fn remove_c2pa_file_spec_reference(&mut self) -> Result<(), Error> { + let c2pa_file_spec_reference = self + .c2pa_file_spec_object_id() + .ok_or_else(|| Error::FindingC2PAFileSpec)?; + + self.document + .catalog_mut()? + .get_mut(ASSOCIATED_FILE_KEY)? + .as_array_mut()? + .retain(|v| { + let Ok(reference) = v.as_reference() else { + return true; + }; + + reference != c2pa_file_spec_reference + }); + + Ok(()) + } + /// Adds the C2PA `Annotation` to the PDF. /// /// ### Note: @@ -361,16 +408,28 @@ impl Pdf { Ok(()) } - /// Sets the Associated File (/AF) key of the PDF to the provided embedded file spec reference. - fn set_af_relationship(&mut self, embedded_file_spec_ref: ObjectId) -> Result<(), Error> { - self.document - .catalog_mut()? - .set(ASSOCIATED_FILE_KEY, embedded_file_spec_ref); + /// Creates, or appends to, the Associated File (`AF`) array the embedded file spec reference of the + /// C2PA data. + fn push_associated_file(&mut self, embedded_file_spec_ref: ObjectId) -> Result<(), Error> { + let catalog = self.document.catalog_mut()?; + if catalog.get_mut(ASSOCIATED_FILE_KEY).is_err() { + // Add associated files array to catalog if it isn't already present. + catalog.set(ASSOCIATED_FILE_KEY, vec![]); + } + + let associated_files = catalog.get_mut(ASSOCIATED_FILE_KEY)?; + let associated_files = match associated_files.as_reference() { + Ok(object_id) => self.document.get_object_mut(object_id)?, + _ => associated_files, + } + .as_array_mut()?; + + associated_files.push(Reference(embedded_file_spec_ref)); Ok(()) } - /// Adds the `Embedded File Specification` to the PDF document. Returns the [lopdf::Object::Reference] + /// Adds the `Embedded File Specification` to the PDF document. Returns the [Reference] /// to the added `Embedded File Specification`. fn add_embedded_file_specification(&mut self, file_stream_ref: ObjectId) -> ObjectId { let embedded_file_stream = dictionary! { @@ -386,7 +445,7 @@ impl Pdf { } /// Adds the provided `bytes` as a `StreamDictionary` to the PDF document. Returns the - /// [lopdf::Object::Reference] of the added [lopdf::Object]. + /// [Reference] of the added [Object]. fn add_c2pa_embedded_file_stream(&mut self, bytes: Vec<u8>) -> ObjectId { let stream = Stream::new( dictionary! { @@ -651,7 +710,7 @@ mod tests { pdf.document .catalog_mut() .unwrap() - .set(ASSOCIATED_FILE_KEY, Object::Reference((100, 0))); + .set(ASSOCIATED_FILE_KEY, vec![Reference((100, 0))]); assert!(matches!(pdf.read_manifest_bytes(), Ok(None))); } @@ -663,7 +722,7 @@ mod tests { pdf.document .catalog_mut() .unwrap() - .set(ASSOCIATED_FILE_KEY, Object::Reference((100, 0))); + .set(ASSOCIATED_FILE_KEY, Reference((100, 0))); assert!(matches!(pdf.read_manifest_bytes(), Ok(None))); } diff --git a/sdk/src/asset_handlers/pdf_io.rs b/sdk/src/asset_handlers/pdf_io.rs @@ -15,7 +15,7 @@ use std::{fs::File, path::Path}; use crate::{ asset_handlers::pdf::{C2paPdf, Pdf}, - asset_io::{AssetIO, CAIReader, CAIWriter, HashObjectPositions}, + asset_io::{AssetIO, CAIReader, CAIWriter, ComposedManifestRef, HashObjectPositions}, CAIRead, Error, Error::{JumbfNotFound, NotImplemented, PdfReadError}, }; @@ -108,6 +108,17 @@ impl AssetIO for PdfIO { fn supported_types(&self) -> &[&str] { &SUPPORTED_TYPES } + + fn composed_data_ref(&self) -> Option<&dyn ComposedManifestRef> { + Some(self) + } +} + +impl ComposedManifestRef for PdfIO { + // Return entire CAI block as Vec<u8> + fn compose_manifest(&self, manifest_data: &[u8], _format: &str) -> Result<Vec<u8>, Error> { + Ok(manifest_data.to_vec()) + } } #[cfg(test)]