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 f9cbad5a8ac4140a697f4dea5833c33c1eecf636
parent 439c9fddd6ea5a49bdb67f98714b593e6564957a
Author: Dylan ross <dylan.ssor@gmail.com>
Date:   Fri, 29 Sep 2023 14:43:56 -0700

supports removal of manifests from pdf (#312)

* supports removal of manifests from pdf

* implements feedback - c2pa -> C2PA

---------

Co-authored-by: Dylan Ross <dyross@adobe.com>
Diffstat:
Msdk/src/asset_handlers/pdf.rs | 163++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 161 insertions(+), 2 deletions(-)

diff --git a/sdk/src/asset_handlers/pdf.rs b/sdk/src/asset_handlers/pdf.rs @@ -28,6 +28,7 @@ static C2PA_RELATIONSHIP: &[u8] = b"C2PA_Manifest"; static CONTENT_CREDS: &str = "Content Credentials"; static EMBEDDED_FILES_KEY: &[u8] = b"EmbeddedFiles"; static SUBTYPE_KEY: &[u8] = b"Subtype"; +static TYPE_KEY: &[u8] = b"Type"; static NAMES_KEY: &[u8] = b"Names"; /// Error representing failure scenarios while interacting with PDFs. @@ -38,9 +39,18 @@ pub enum Error { #[error(transparent)] UnableToReadPdf(#[from] lopdf::Error), + /// No Manifest is present in the PDF. + #[error("No manifest is present in the PDF.")] + NoManifest, + /// Error occurred while adding a C2PA manifest as an `Annotation` to the PDF. #[error("Unable to add C2PA manifest as an annotation to the PDF.")] AddingAnnotation, + + // The PDF has an `AFRelationship` set to C2PA, but we were unable to find + // the manifest bytes in the PDF's embedded files. + #[error("Unable to find C2PA manifest in the PDF's embedded files.")] + UnableToFindEmbeddedFileManifest, } const C2PA_MIME_TYPE: &str = "application/x-c2pa-manifest-store"; @@ -53,7 +63,7 @@ pub(crate) trait C2paPdf: Sized { /// Returns `true` if the `PDF` is password protected, `false` otherwise. fn is_password_protected(&self) -> bool; - /// Returns `true` if this PDF has `c2pa` Manifests, `false` otherwise. + /// Returns `true` if this PDF has C2PA Manifests, `false` otherwise. fn has_c2pa_manifest(&self) -> bool; /// Writes provided `bytes` as a PDF `Embedded File` @@ -66,6 +76,8 @@ pub(crate) trait C2paPdf: Sized { #[allow(clippy::needless_lifetimes)] // required for automock::mockall fn read_manifest_bytes<'a>(&'a self) -> Result<Option<Vec<&'a [u8]>>, Error>; + fn remove_manifest_bytes(&mut self) -> Result<(), Error>; + fn read_xmp(&self) -> Option<String>; } @@ -218,6 +230,45 @@ impl C2paPdf for Pdf { ])) } + fn remove_manifest_bytes(&mut self) -> Result<(), Error> { + if !self.has_c2pa_manifest() { + return Err(Error::NoManifest); + } + + // Find the File Spec, which contains the reference to the manifest. + let file_spec_ref = self + .document + .catalog()? + .get(ASSOCIATED_FILE_KEY)? + .as_reference()?; + + // Find the manifest's file stream. + let file_stream_ref = self + .document + .get_object(file_spec_ref)? + .as_dict()? + .get(b"EF")? + .as_reference()?; + + // Attempt to remove the manifest from the PDF's `Embedded Files`s. If the manifest + // isn't in the PDF's embedded files, remove the manifest from the PDF's annotations. + // + // We do the operation in this order because a PDF's annotations are attached to a page. + // It's possible we'd have to iterate over every page of the PDF before determining the + // manifest is referenced from an Embedded File instead. + 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); + + // Delete the manifest and its descriptor from the PDF + self.document.delete_object(file_stream_ref); + self.document.delete_object(file_spec_ref); + + Ok(()) + } + /// Reads the `Metadata` field referenced in the PDF document's `Catalog` entry. Will return /// `None` if no Metadata is present. fn read_xmp(&self) -> Option<String> { @@ -327,7 +378,7 @@ impl Pdf { "Desc" => Object::string_literal(CONTENT_CREDS), "F" => Object::string_literal(CONTENT_CREDS), "EF" => Reference(file_stream_ref), - "Type" => Name("FileSpec".into()), + TYPE_KEY => Name("FileSpec".into()), "UF" => Object::string_literal(CONTENT_CREDS), }; @@ -347,6 +398,76 @@ impl Pdf { self.document.add_object(stream) } + + /// Remove the C2PA Manifest `Annotation` from the PDF. + fn remove_manifest_from_annotations(&mut self) -> Result<(), Error> { + for (_, page_id) in self.document.get_pages() { + self.document + .get_object_mut(page_id)? + .as_dict_mut()? + .get_mut(ANNOTATIONS_KEY)? + .as_array_mut()? + .retain(|obj| { + obj.as_dict() + .and_then(|annot| annot.get(TYPE_KEY)) + .and_then(Object::as_name_str) + .map(|str| str != CONTENT_CREDS) + .unwrap_or(true) + }); + } + + Ok(()) + } + + /// Removes the manifest from the PDF's embedded files collection. + fn remove_manifest_from_embedded_files(&mut self) -> Result<(), Error> { + let Ok(names) = self.document.catalog_mut()?.get_mut(NAMES_KEY) else { + return Err(Error::NoManifest); + }; + + // Follows the reference to the /Names Dictionary. + let names_dictionary = match names.as_reference() { + Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?, + _ => names.as_dict_mut()?, + }; + + // Follows the reference to the /EmbeddedFiles Dictionary. + let embedded_files_object = names_dictionary.get_mut(EMBEDDED_FILES_KEY)?; + let embedded_files_dictionary = match embedded_files_object.as_reference() { + Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?, + _ => embedded_files_object.as_dict_mut()?, + }; + + // Gets the /Names array from the /EmbeddedFiles Dictionary. This will contain the reference + // to the C2PA manifest. + let names_vector_object = embedded_files_dictionary.get_mut(NAMES_KEY)?; + let names_vector = match names_vector_object.as_reference() { + Ok(object_id) => self.document.get_object_mut(object_id)?.as_array_mut()?, + _ => names_vector_object.as_array_mut()?, + }; + + // Find the "Content Credentials" marker name in the /Names Array. + let content_creds_marker_idx = names_vector + .iter() + .position(|value| { + value + .as_string() + .map(|value| value == CONTENT_CREDS) + .unwrap_or_default() + }) + .ok_or_else(|| Error::UnableToFindEmbeddedFileManifest)?; + + let content_creds_reference_idx = content_creds_marker_idx + 1; + if content_creds_reference_idx >= names_vector.len() { + return Err(Error::UnableToFindEmbeddedFileManifest); + } + + // Delete the "Content Credentials" marker object and the reference to the C2PA + // manifest in the PDF's embedded files. + names_vector.drain(content_creds_marker_idx..=content_creds_reference_idx); + + Ok(()) + } } #[cfg(test)] @@ -560,4 +681,42 @@ mod tests { let pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap(); assert!(pdf.read_xmp().is_some()); } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn test_remove_manifest_bytes_from_file_without_c2pa_returns_error() { + let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap(); + + assert!(matches!( + pdf.remove_manifest_bytes(), + Err(Error::NoManifest) + )); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn test_remove_manifest_from_file_with_annotation_based_manifest() { + let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap(); + let manifest_bytes = vec![0u8, 1u8, 1u8, 2u8, 3u8]; + pdf.write_manifest_as_annotation(manifest_bytes.clone()) + .unwrap(); + + assert!(pdf.has_c2pa_manifest()); + assert!(pdf.remove_manifest_bytes().is_ok()); + assert!(!pdf.has_c2pa_manifest()); + } + + #[cfg_attr(not(target_arch = "wasm32"), test)] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn test_remove_manifest_from_file_with_embedded_file_based_manifest() { + let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap(); + let manifest_bytes = vec![0u8, 1u8, 1u8, 2u8, 3u8]; + + pdf.write_manifest_as_embedded_file(manifest_bytes.clone()) + .unwrap(); + + assert!(pdf.has_c2pa_manifest()); + assert!(pdf.remove_manifest_bytes().is_ok()); + assert!(!pdf.has_c2pa_manifest()); + } }