commit 82c2aa29332e40e515afba711b6757cc5e77c74b
parent 760009cbbdd4f58c090c059e12a019acc1c95dfb
Author: Dylan ross <dylan.ssor@gmail.com>
Date: Mon, 11 Sep 2023 15:22:40 -0700
Support for writing and reading manifest data from simple PDFs (without incremental updates or signatures) (#249)
* support writing manifest data to non-incremental based pdfs
* Capitalize C2PA in error message
* implements scouten's feedback
* removes extraneous clones
* allows manifests to be read from pdf
* flattens out the writes_manifest_as_embedded_file fn
* appeases the formatter
* standardizes naming
* improves comments
* appeases clippy
* makes lopdf dependency optional
* updates interface to support returning multiple manifests
* appeases clippy
* adds note about the possibility of multiple manifests in pdf
* appeases clippy
---------
Co-authored-by: Dylan Ross <dyross@adobe.com>
Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
11 files changed, 520 insertions(+), 5 deletions(-)
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -34,6 +34,7 @@ no_interleaved_io = ["file_io"]
fetch_remote_manifests = ["file_io"]
openssl_sign = ["openssl", "openssl-sys"]
json_schema = ["dep:schemars"]
+pdf = ["dep:lopdf"]
# The diagnostics feature is unsupported and might be removed.
# It enables some low-overhead timing features used in our development cycle.
@@ -76,6 +77,7 @@ fast-xml = "0.23.1"
hex = "0.4.3"
img-parts = "0.3.0"
log = "0.4.8"
+lopdf = { version = "0.31.0", optional = true }
lazy_static = "1.4.0"
multibase = "0.9.0"
multihash = "0.11.4"
diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs
@@ -1938,12 +1938,10 @@ impl Claim {
// Do any assertions of this type exist?
pub fn has_assertion_type(&self, in_label: &str) -> bool {
let (label, _) = Claim::assertion_label_from_link(in_label);
- let found = self
- .assertion_store
- .iter()
- .find(|&x| x.assertion.label().starts_with(&label));
- found.is_some()
+ self.assertion_store
+ .iter()
+ .any(|x| x.assertion.label().starts_with(&label))
}
// Create a JUMBF URI from a claim label.
diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs
@@ -18,6 +18,8 @@ pub(crate) mod hash_utils;
pub(crate) mod merkle;
#[allow(dead_code)] // for wasm build
pub(crate) mod patch;
+#[cfg(feature = "pdf")]
+pub(crate) mod pdf_utils;
#[cfg(feature = "add_thumbnails")]
pub(crate) mod thumbnail;
pub(crate) mod time_it;
diff --git a/sdk/src/utils/pdf_utils.rs b/sdk/src/utils/pdf_utils.rs
@@ -0,0 +1,513 @@
+// Copyright 2023 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 std::io::Write;
+
+use lopdf::{
+ dictionary, Document, Object,
+ Object::{Array, Integer, Name, Reference},
+ ObjectId, Stream,
+};
+use thiserror::Error;
+
+// Associated File Relationship
+static AF_RELATIONSHIP_KEY: &[u8] = b"AFRelationship";
+static ANNOTATIONS_KEY: &[u8] = b"Annots";
+static ASSOCIATED_FILE_KEY: &[u8] = b"AF";
+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 NAMES_KEY: &[u8] = b"Names";
+
+#[derive(Debug, Error)]
+pub(crate) enum Error {
+ #[error(transparent)]
+ UnableToReadPdf(#[from] lopdf::Error),
+
+ #[error("Unable to add C2PA manifest as an annotation to the PDF.")]
+ AddingAnnotation,
+}
+
+const C2PA_MIME_TYPE: &str = "application/x-c2pa-manifest-store";
+
+pub(crate) trait C2paPdf: Sized {
+ /// Load a PDF from a slice of bytes.
+ fn from_bytes(bytes: &[u8]) -> Result<Self, Error>;
+
+ /// Save the `C2paPdf` implementation to the provided `writer`.
+ fn save_to<W: Write>(&mut self, writer: &mut W) -> Result<(), std::io::Error>;
+
+ /// 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.
+ fn has_c2pa_manifest(&self) -> bool;
+
+ /// Writes provided `bytes` as a PDF `Embedded File`
+ fn write_manifest_as_embedded_file(&mut self, bytes: Vec<u8>) -> Result<(), Error>;
+
+ /// Writes provided `bytes` as a PDF `Annotation`.
+ fn write_manifest_as_annotation(&mut self, vec: Vec<u8>) -> Result<(), Error>;
+
+ /// Returns a reference to the C2PA manifest bytes.
+ fn read_manifest_bytes(&self) -> Result<Option<Vec<&[u8]>>, Error>;
+}
+
+pub(crate) struct Pdf {
+ document: Document,
+}
+
+impl C2paPdf for Pdf {
+ fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
+ let document = Document::load_mem(bytes)?;
+ Ok(Self { document })
+ }
+
+ /// Saves the in-memory PDF to the provided `writer`.
+ fn save_to<W: Write>(&mut self, writer: &mut W) -> Result<(), std::io::Error> {
+ self.document.save_to(writer)
+ }
+
+ fn is_password_protected(&self) -> bool {
+ self.document.is_encrypted()
+ }
+
+ /// 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`.
+ 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()
+ }
+
+ /// Writes the provided `bytes` to the PDF as an `EmbeddedFile`.
+ fn write_manifest_as_embedded_file(&mut self, bytes: Vec<u8>) -> Result<(), Error> {
+ // Add `FileStream` and `FileSpec` to the 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)?;
+
+ let mut manifest_name_file_pair = vec![
+ Object::string_literal(CONTENT_CREDS),
+ Reference(file_spec_ref),
+ ];
+
+ let Ok(catalog_names) = self.document.catalog_mut()?.get_mut(NAMES_KEY) else {
+ // No /Names key exists in the Catalog. We can safely add the /Names key and construct
+ // the remaining objects.
+ // Add /EmbeddedFiles dictionary as indirect object.
+ let embedded_files_ref = self.document.add_object(dictionary! {
+ NAMES_KEY => manifest_name_file_pair
+ });
+
+ // Add /Names dictionary as indirect object
+ let names_ref = self.document.add_object(dictionary! {
+ EMBEDDED_FILES_KEY => Reference(embedded_files_ref)
+ });
+
+ // Set /Names key in `Catalog` to reference above indirect object names dictionary.
+ self.document.catalog_mut()?.set(NAMES_KEY, names_ref);
+ return Ok(());
+ };
+
+ // Follows the Reference to the /EmbeddedFiles Dictionary, if the Object is a Reference.
+ let names_dictionary = match catalog_names.as_reference() {
+ Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?,
+ _ => catalog_names.as_dict_mut()?,
+ };
+
+ let Ok(embedded_files) = names_dictionary.get_mut(EMBEDDED_FILES_KEY) else {
+ // We have a /Names dictionary, but are missing the /EmbeddedFiles dictionary
+ // and its /Names array of embedded files.
+ names_dictionary.set(
+ EMBEDDED_FILES_KEY,
+ dictionary! { NAMES_KEY => manifest_name_file_pair },
+ );
+ return Ok(());
+ };
+
+ // Follows the reference to the /EmbeddedFiles Dictionary, if the Object is a Reference.
+ let embedded_files_dictionary = match embedded_files.as_reference() {
+ Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?,
+ _ => embedded_files.as_dict_mut()?,
+ };
+
+ let Ok(names) = embedded_files_dictionary.get_mut(NAMES_KEY) else {
+ // This PDF has the /Names dictionary, and it has the /EmbeddedFiles
+ // dictionary, but the /EmbeddedFiles Dictionary is missing the /Names Array.
+ embedded_files_dictionary.set(
+ NAMES_KEY,
+ dictionary! { NAMES_KEY => manifest_name_file_pair },
+ );
+
+ return Ok(());
+ };
+
+ // Follows the reference to the /Names Array, if the Object is a Reference.
+ let names_array = match names.as_reference() {
+ Ok(object_id) => self.document.get_object_mut(object_id)?.as_array_mut()?,
+ _ => names.as_array_mut()?,
+ };
+
+ // The PDF has the /Names dictionary, which contains the /EmbeddedFiles Dictionary, which
+ // contains the /Names array. Append the manifest's name (Content Credentials)
+ // and its reference.
+ names_array.append(&mut manifest_name_file_pair);
+
+ Ok(())
+ }
+
+ /// Writes the provided bytes to the PDF as a `FileAttachment` `Annotation`. This `Annotation`
+ /// is added to the first page of the `PDF`, to the lower left corner.
+ fn write_manifest_as_annotation(&mut self, bytes: Vec<u8>) -> Result<(), Error> {
+ 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.add_file_attachment_annotation(file_spec_reference)?;
+
+ Ok(())
+ }
+
+ /// Gets a reference to the `C2PA` manifest bytes of the PDF.
+ ///
+ /// This method will read the bytes of the manifest, whether the manifest was added to the
+ /// PDF via an `Annotation` or an `EmbeddedFile`.
+ ///
+ /// Returns an `Ok(None)` if no manifest is present. Returns a `Ok(Some(Vec<&[u8]>))` when a manifest
+ /// is present.
+ ///
+ /// ### Note:
+ ///
+ /// A `Vec<&[u8]>` is returned because it's possible for a PDF's manifests to be stored
+ /// 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() {
+ return Ok(None);
+ };
+
+ Ok(Some(vec![
+ &self
+ .document
+ .catalog()?
+ .get_deref(ASSOCIATED_FILE_KEY, &self.document)?
+ .as_dict()?
+ .get_deref(b"EF", &self.document)?
+ .as_stream()?
+ .content,
+ ]))
+ }
+}
+
+impl Pdf {
+ /// Adds the C2PA `Annotation` to the PDF.
+ ///
+ /// ### Note:
+ /// The `FileAttachment` annotation is added to the first page of the PDF in the lower
+ /// left-hand corner. The `FileAttachment`'s location is not defined in the spec as of version
+ /// `1.3`.
+ fn add_file_attachment_annotation(
+ &mut self,
+ file_spec_reference: ObjectId,
+ ) -> Result<(), Error> {
+ let annotation = dictionary! {
+ "Type" => Name("Annot".into()),
+ "Contents" => Object::string_literal(CONTENT_CREDS),
+ "Name" => Object::string_literal(CONTENT_CREDS),
+ SUBTYPE_KEY => Name("FileAttachment".into()),
+ "FS" => Reference(file_spec_reference),
+ // Places annotation in the lower left-hand corner. The icon will be 10x10.
+ "Rect" => vec![0.into(), 0.into(), 10.into(), 10.into()],
+ };
+
+ // Add C2PA annotation as an indirect object.
+ let annotation_ref = self.document.add_object(annotation);
+
+ // Find the reference to the first page of the PDF.
+ let first_page_ref = self
+ .document
+ .page_iter()
+ .next()
+ .ok_or_else(|| Error::AddingAnnotation)?;
+
+ // Get a mutable ref to the first page as a Dictionary object.
+ let first_page = self
+ .document
+ .get_object_mut(first_page_ref)?
+ .as_dict_mut()?;
+
+ // Ensures the /Annots array exists on the page object.
+ if !first_page.has(ANNOTATIONS_KEY) {
+ first_page.set(ANNOTATIONS_KEY, Array(vec![]))
+ }
+
+ // Follows a reference to the indirect annotations array, if it exists.
+ let annotation_object = first_page.get_mut(ANNOTATIONS_KEY)?;
+ let annotations = if let Ok(v) = annotation_object.as_reference() {
+ self.document.get_object_mut(v)?
+ } else {
+ annotation_object
+ }
+ .as_array_mut()?;
+
+ annotations.push(Reference(annotation_ref));
+ 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);
+
+ Ok(())
+ }
+
+ /// Adds the `Embedded File Specification` to the PDF document. Returns the [lopdf::Object::Reference]
+ /// to the added `Embedded File Specification`.
+ fn add_embedded_file_specification(&mut self, file_stream_ref: ObjectId) -> ObjectId {
+ let embedded_file_stream = dictionary! {
+ AF_RELATIONSHIP_KEY => Name(C2PA_RELATIONSHIP.into()),
+ "Desc" => Object::string_literal(CONTENT_CREDS),
+ "F" => Object::string_literal(CONTENT_CREDS),
+ "EF" => Reference(file_stream_ref),
+ "Type" => Name("FileSpec".into()),
+ "UF" => Object::string_literal(CONTENT_CREDS),
+ };
+
+ self.document.add_object(embedded_file_stream)
+ }
+
+ /// Adds the provided `bytes` as a `StreamDictionary` to the PDF document. Returns the
+ /// [lopdf::Object::Reference] of the added [lopdf::Object].
+ fn add_c2pa_embedded_file_stream(&mut self, bytes: Vec<u8>) -> ObjectId {
+ let stream = Stream::new(
+ dictionary! {
+ SUBTYPE_KEY => C2PA_MIME_TYPE,
+ "Length" => Integer(bytes.len() as i64),
+ },
+ bytes,
+ );
+
+ self.document.add_object(stream)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+
+ use super::*;
+
+ #[cfg(target_arch = "wasm32")]
+ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+ #[cfg(target_arch = "wasm32")]
+ use wasm_bindgen_test::*;
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_loads_pdf_from_bytes() {
+ let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
+ let pdf_result = Pdf::from_bytes(bytes);
+ assert!(pdf_result.is_ok());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_loads_pdf_from_bytes_with_invalid_file() {
+ let bytes = include_bytes!("../../tests/fixtures/XCA.jpg");
+ let pdf_result = Pdf::from_bytes(bytes);
+ assert!(matches!(pdf_result, Err(Error::UnableToReadPdf(_))));
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_is_password_protected() {
+ let bytes = include_bytes!("../../tests/fixtures/basic-password.pdf");
+ let pdf_result = Pdf::from_bytes(bytes).unwrap();
+ assert!(pdf_result.is_password_protected());
+
+ let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
+ let pdf = Pdf::from_bytes(bytes).unwrap();
+ assert!(!pdf.is_password_protected());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_has_c2pa_manifest_on_file_without_manifest() {
+ let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
+ let pdf = Pdf::from_bytes(bytes).unwrap();
+ assert!(!pdf.has_c2pa_manifest())
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_has_c2pa_manifest_on_file_with_manifest() {
+ let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
+ let mut pdf = Pdf::from_bytes(bytes).unwrap();
+ assert!(!pdf.has_c2pa_manifest());
+
+ pdf.write_manifest_as_annotation(vec![0u8, 1u8]).unwrap();
+ assert!(pdf.has_c2pa_manifest());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_adds_embedded_file_spec_to_pdf_stream() {
+ let bytes = include_bytes!("../../tests/fixtures/express.pdf");
+ let mut pdf = Pdf::from_bytes(bytes).unwrap();
+ let object_count_before_add = pdf.document.objects.len();
+
+ let bytes = vec![10u8];
+ let id = pdf.add_c2pa_embedded_file_stream(bytes.clone());
+
+ // Object added to the PDF's object collection.
+ assert_eq!(object_count_before_add + 1, pdf.document.objects.len());
+
+ // We are able to find the object.
+ let stream = pdf.document.get_object(id);
+ assert_eq!(stream.unwrap().as_stream().unwrap().content, bytes);
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_write_manifest_as_annotation() {
+ let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/express.pdf")).unwrap();
+ assert!(!pdf.has_c2pa_manifest());
+ pdf.write_manifest_as_annotation(vec![10u8, 20u8]).unwrap();
+ assert!(pdf.has_c2pa_manifest());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_write_manifest_bytes_to_pdf_with_existing_annotations() {
+ let mut pdf =
+ Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic-annotation.pdf")).unwrap();
+ pdf.write_manifest_as_annotation(vec![10u8, 20u8]).unwrap();
+ assert!(pdf.has_c2pa_manifest());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_add_manifest_to_embedded_files() {
+ let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
+ pdf.write_manifest_as_embedded_file(vec![10u8, 20u8])
+ .unwrap();
+
+ assert!(pdf.has_c2pa_manifest());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_add_manifest_to_embedded_files_attachments_present() {
+ let mut pdf =
+ Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic-attachments.pdf")).unwrap();
+ pdf.write_manifest_as_embedded_file(vec![10u8, 20u8])
+ .unwrap();
+
+ assert!(pdf.has_c2pa_manifest());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_save_to() {
+ let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
+ assert!(!pdf.has_c2pa_manifest());
+
+ pdf.write_manifest_as_annotation(vec![10u8]).unwrap();
+ assert!(pdf.has_c2pa_manifest());
+
+ let mut saved_bytes = vec![];
+ pdf.save_to(&mut saved_bytes).unwrap();
+
+ let saved_pdf = Pdf::from_bytes(&saved_bytes).unwrap();
+ assert!(saved_pdf.has_c2pa_manifest());
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_reads_manifest_bytes_for_embedded_files_manifest() {
+ let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/express.pdf")).unwrap();
+ assert!(!pdf.has_c2pa_manifest());
+
+ 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!(matches!(
+ pdf.read_manifest_bytes(),
+ Ok(Some(manifests)) if manifests[0] == manifest_bytes
+ ));
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_reads_manifest_bytes_for_annotation_manifest() {
+ let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
+ assert!(!pdf.has_c2pa_manifest());
+
+ let manifest_bytes = vec![0u8, 1u8, 1u8, 2u8, 3u8];
+ pdf.write_manifest_as_annotation(manifest_bytes.clone())
+ .unwrap();
+
+ assert!(pdf.has_c2pa_manifest());
+ assert!(matches!(
+ pdf.read_manifest_bytes(),
+ Ok(Some(manifests)) if manifests[0] == manifest_bytes
+ ));
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_read_manifest_bytes_from_pdf_without_bytes_returns_none() {
+ let pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
+ assert!(!pdf.has_c2pa_manifest());
+ assert!(matches!(pdf.read_manifest_bytes(), Ok(None)));
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_read_manifest_bytes_from_pdf_with_other_af_relationship_returns_none() {
+ let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
+ pdf.document
+ .catalog_mut()
+ .unwrap()
+ .set(ASSOCIATED_FILE_KEY, Object::Reference((100, 0)));
+
+ assert!(matches!(pdf.read_manifest_bytes(), Ok(None)));
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ fn test_read_pdf_with_associated_file_that_is_not_manifest() {
+ let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
+ pdf.document
+ .catalog_mut()
+ .unwrap()
+ .set(ASSOCIATED_FILE_KEY, Object::Reference((100, 0)));
+
+ assert!(matches!(pdf.read_manifest_bytes(), Ok(None)));
+ }
+}
diff --git a/sdk/tests/fixtures/basic-annotation.pdf b/sdk/tests/fixtures/basic-annotation.pdf
Binary files differ.
diff --git a/sdk/tests/fixtures/basic-attachments.pdf b/sdk/tests/fixtures/basic-attachments.pdf
Binary files differ.
diff --git a/sdk/tests/fixtures/basic-password.pdf b/sdk/tests/fixtures/basic-password.pdf
Binary files differ.
diff --git a/sdk/tests/fixtures/basic-retest.pdf b/sdk/tests/fixtures/basic-retest.pdf
Binary files differ.
diff --git a/sdk/tests/fixtures/basic-signed.pdf b/sdk/tests/fixtures/basic-signed.pdf
Binary files differ.
diff --git a/sdk/tests/fixtures/basic.pdf b/sdk/tests/fixtures/basic.pdf
Binary files differ.
diff --git a/sdk/tests/fixtures/express.pdf b/sdk/tests/fixtures/express.pdf
Binary files differ.