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

pdf.rs (31336B)


      1 // Copyright 2023 Adobe. All rights reserved.
      2 // This file is licensed to you under the Apache License,
      3 // Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
      4 // or the MIT license (http://opensource.org/licenses/MIT),
      5 // at your option.
      6 
      7 // Unless required by applicable law or agreed to in writing,
      8 // this software is distributed on an "AS IS" BASIS, WITHOUT
      9 // WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
     10 // implied. See the LICENSE-MIT and LICENSE-APACHE files for the
     11 // specific language governing permissions and limitations under
     12 // each license.
     13 
     14 // TODO: Remove this after we finish the PDF write feature.
     15 #![allow(dead_code)]
     16 
     17 use std::io::{Read, Write};
     18 
     19 use lopdf::{
     20     dictionary, Document, Object,
     21     Object::{Array, Integer, Name, Reference},
     22     ObjectId, Stream,
     23 };
     24 use thiserror::Error;
     25 
     26 // Associated File Relationship
     27 static AF_RELATIONSHIP_KEY: &[u8] = b"AFRelationship";
     28 static ANNOTATIONS_KEY: &[u8] = b"Annots";
     29 static ASSOCIATED_FILE_KEY: &[u8] = b"AF";
     30 static C2PA_RELATIONSHIP: &[u8] = b"C2PA_Manifest";
     31 static CONTENT_CREDS: &str = "Content Credentials";
     32 static EMBEDDED_FILES_KEY: &[u8] = b"EmbeddedFiles";
     33 static SUBTYPE_KEY: &[u8] = b"Subtype";
     34 static TYPE_KEY: &[u8] = b"Type";
     35 static NAMES_KEY: &[u8] = b"Names";
     36 
     37 /// Error representing failure scenarios while interacting with PDFs.
     38 #[derive(Debug, Error)]
     39 pub enum Error {
     40     /// Error occurred while reading the PDF. Look into the wrapped `lopdf::Error` for more
     41     /// information on the cause.
     42     #[error(transparent)]
     43     UnableToReadPdf(#[from] lopdf::Error),
     44 
     45     /// No Manifest is present in the PDF.
     46     #[error("No manifest is present in the PDF.")]
     47     NoManifest,
     48 
     49     /// Error occurred while adding a C2PA manifest as an `Annotation` to the PDF.
     50     #[error("Unable to add C2PA manifest as an annotation to the PDF.")]
     51     AddingAnnotation,
     52 
     53     // The PDF has an `AFRelationship` set to C2PA, but we were unable to find
     54     // the manifest bytes in the PDF's embedded files.
     55     #[error("Unable to find C2PA manifest in the PDF's embedded files.")]
     56     UnableToFindEmbeddedFileManifest,
     57 
     58     /// This error occurs when we an error was encountered trying to find the PDF's C2PA embedded
     59     /// file specification in the array of Associated Files defined in the catalog.
     60     #[error("Unable to find a C2PA embedded file specification in PDF's associated files array")]
     61     FindingC2PAFileSpec,
     62 }
     63 
     64 const C2PA_MIME_TYPE: &str = "application/x-c2pa-manifest-store";
     65 
     66 #[cfg_attr(test, mockall::automock)]
     67 pub(crate) trait C2paPdf: Sized {
     68     /// Save the `C2paPdf` implementation to the provided `writer`.
     69     fn save_to<W: Write + 'static>(&mut self, writer: &mut W) -> Result<(), std::io::Error>;
     70 
     71     /// Returns `true` if the `PDF` is password protected, `false` otherwise.
     72     fn is_password_protected(&self) -> bool;
     73 
     74     /// Returns `true` if this PDF has C2PA Manifests, `false` otherwise.
     75     fn has_c2pa_manifest(&self) -> bool;
     76 
     77     /// Writes provided `bytes` as a PDF `Embedded File`
     78     fn write_manifest_as_embedded_file(&mut self, bytes: Vec<u8>) -> Result<(), Error>;
     79 
     80     /// Writes provided `bytes` as a PDF `Annotation`.
     81     fn write_manifest_as_annotation(&mut self, vec: Vec<u8>) -> Result<(), Error>;
     82 
     83     /// Returns a reference to the C2PA manifest bytes.
     84     #[allow(clippy::needless_lifetimes)] // required for automock::mockall
     85     fn read_manifest_bytes<'a>(&'a self) -> Result<Option<Vec<&'a [u8]>>, Error>;
     86 
     87     fn remove_manifest_bytes(&mut self) -> Result<(), Error>;
     88 
     89     fn read_xmp(&self) -> Option<String>;
     90 }
     91 
     92 pub(crate) struct Pdf {
     93     document: Document,
     94 }
     95 
     96 impl C2paPdf for Pdf {
     97     /// Saves the in-memory PDF to the provided `writer`.
     98     fn save_to<W: Write>(&mut self, writer: &mut W) -> Result<(), std::io::Error> {
     99         self.document.save_to(writer)
    100     }
    101 
    102     fn is_password_protected(&self) -> bool {
    103         self.document.is_encrypted()
    104     }
    105 
    106     /// Determines if this PDF has a C2PA manifest embedded.
    107     ///
    108     /// This is done by checking if the Associated File key of the catalog points to a
    109     /// [Object::Dictionary] with an `AFRelationship` set to `C2PA_Manifest`.
    110     fn has_c2pa_manifest(&self) -> bool {
    111         self.c2pa_file_spec_object_id().is_some()
    112     }
    113 
    114     /// Writes the provided `bytes` to the PDF as an `EmbeddedFile`.
    115     fn write_manifest_as_embedded_file(&mut self, bytes: Vec<u8>) -> Result<(), Error> {
    116         // Add `FileStream` and `FileSpec` to the PDF.
    117         let file_stream_ref = self.add_c2pa_embedded_file_stream(bytes);
    118         let file_spec_ref = self.add_embedded_file_specification(file_stream_ref);
    119 
    120         self.push_associated_file(file_spec_ref)?;
    121 
    122         let mut manifest_name_file_pair = vec![
    123             Object::string_literal(CONTENT_CREDS),
    124             Reference(file_spec_ref),
    125         ];
    126 
    127         let Ok(catalog_names) = self.document.catalog_mut()?.get_mut(NAMES_KEY) else {
    128             // No /Names key exists in the Catalog. We can safely add the /Names key and construct
    129             // the remaining objects.
    130             // Add /EmbeddedFiles dictionary as indirect object.
    131             let embedded_files_ref = self.document.add_object(dictionary! {
    132                 NAMES_KEY => manifest_name_file_pair
    133             });
    134 
    135             // Add /Names dictionary as indirect object
    136             let names_ref = self.document.add_object(dictionary! {
    137                 EMBEDDED_FILES_KEY => Reference(embedded_files_ref)
    138             });
    139 
    140             // Set /Names key in `Catalog` to reference above indirect object names dictionary.
    141             self.document.catalog_mut()?.set(NAMES_KEY, names_ref);
    142             return Ok(());
    143         };
    144 
    145         // Follows the Reference to the /EmbeddedFiles Dictionary, if the Object is a Reference.
    146         let names_dictionary = match catalog_names.as_reference() {
    147             Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?,
    148             _ => catalog_names.as_dict_mut()?,
    149         };
    150 
    151         let Ok(embedded_files) = names_dictionary.get_mut(EMBEDDED_FILES_KEY) else {
    152             // We have a /Names dictionary, but are missing the /EmbeddedFiles dictionary
    153             // and its /Names array of embedded files.
    154             names_dictionary.set(
    155                 EMBEDDED_FILES_KEY,
    156                 dictionary! { NAMES_KEY => manifest_name_file_pair },
    157             );
    158             return Ok(());
    159         };
    160 
    161         // Follows the reference to the /EmbeddedFiles Dictionary, if the Object is a Reference.
    162         let embedded_files_dictionary = match embedded_files.as_reference() {
    163             Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?,
    164             _ => embedded_files.as_dict_mut()?,
    165         };
    166 
    167         let Ok(names) = embedded_files_dictionary.get_mut(NAMES_KEY) else {
    168             // This PDF has the /Names dictionary, and it has the /EmbeddedFiles
    169             // dictionary, but the /EmbeddedFiles Dictionary is missing the /Names Array.
    170             embedded_files_dictionary.set(
    171                 NAMES_KEY,
    172                 dictionary! { NAMES_KEY => manifest_name_file_pair },
    173             );
    174 
    175             return Ok(());
    176         };
    177 
    178         // Follows the reference to the /Names Array, if the Object is a Reference.
    179         let names_array = match names.as_reference() {
    180             Ok(object_id) => self.document.get_object_mut(object_id)?.as_array_mut()?,
    181             _ => names.as_array_mut()?,
    182         };
    183 
    184         // The PDF has the /Names dictionary, which contains the /EmbeddedFiles Dictionary, which
    185         // contains the /Names array. Append the manifest's name (Content Credentials)
    186         // and its reference.
    187         names_array.append(&mut manifest_name_file_pair);
    188 
    189         Ok(())
    190     }
    191 
    192     /// Writes the provided bytes to the PDF as a `FileAttachment` `Annotation`. This `Annotation`
    193     /// is added to the first page of the `PDF`, to the lower left corner.
    194     fn write_manifest_as_annotation(&mut self, bytes: Vec<u8>) -> Result<(), Error> {
    195         let file_stream_reference = self.add_c2pa_embedded_file_stream(bytes);
    196         let file_spec_reference = self.add_embedded_file_specification(file_stream_reference);
    197 
    198         self.push_associated_file(file_spec_reference)?;
    199         self.add_file_attachment_annotation(file_spec_reference)?;
    200 
    201         Ok(())
    202     }
    203 
    204     /// Gets a reference to the `C2PA` manifest bytes of the PDF.
    205     ///
    206     /// This method will read the bytes of the manifest, whether the manifest was added to the
    207     /// PDF via an `Annotation` or an `EmbeddedFile`.
    208     ///
    209     /// Returns an `Ok(None)` if no manifest is present. Returns a `Ok(Some(Vec<&[u8]>))` when a manifest
    210     /// is present.
    211     ///
    212     /// ### Note:
    213     ///
    214     /// A `Vec<&[u8]>` is returned because it's possible for a PDF's manifests to be stored
    215     /// separately, due to PDF's "Incremental Update" feature. See the spec for more details:
    216     /// <https://c2pa.org/specifications/specifications/1.3/specs/C2PA_Specification.html#_embedding_manifests_into_pdfs>
    217     fn read_manifest_bytes(&self) -> Result<Option<Vec<&[u8]>>, Error> {
    218         let Some(id) = self.c2pa_file_spec_object_id() else {
    219             return Ok(None);
    220         };
    221 
    222         let ef = &self
    223             .document
    224             .get_object(id)
    225             .and_then(Object::as_dict)?
    226             .get_deref(b"EF", &self.document)?
    227             .as_dict()?; // EF dictionary
    228 
    229         Ok(Some(vec![
    230             &ef.get_deref(b"F", &self.document)? // F embedded file stream
    231                 .as_stream()?
    232                 .content,
    233         ]))
    234     }
    235 
    236     fn remove_manifest_bytes(&mut self) -> Result<(), Error> {
    237         if !self.has_c2pa_manifest() {
    238             return Err(Error::NoManifest);
    239         }
    240 
    241         // Find the File Spec, which contains the reference to the manifest.
    242         let file_spec_ref = self
    243             .c2pa_file_spec_object_id()
    244             .ok_or_else(|| Error::NoManifest)?;
    245 
    246         // Find the manifest's file stream.
    247         let file_stream_ef_ref = self
    248             .document
    249             .get_object(file_spec_ref)?
    250             .as_dict()?
    251             .get(b"EF")?;
    252 
    253         let file_stream_ref = file_stream_ef_ref.as_dict()?.get(b"F")?.as_reference()?;
    254 
    255         // Attempt to remove the manifest from the PDF's `Embedded Files`s. If the manifest
    256         // isn't in the PDF's embedded files, remove the manifest from the PDF's annotations.
    257         //
    258         // We do the operation in this order because a PDF's annotations are attached to a page.
    259         // It's possible we'd have to iterate over every page of the PDF before determining the
    260         // manifest is referenced from an Embedded File instead.
    261         self.remove_manifest_from_embedded_files()
    262             .or_else(|_| self.remove_manifest_from_annotations())?;
    263 
    264         // Remove C2PA associated files from the `AF` key in the catalog.
    265         self.remove_c2pa_file_spec_reference()?;
    266 
    267         // Delete the manifest and its descriptor from the PDF
    268         self.document.delete_object(file_stream_ref);
    269         self.document.delete_object(file_spec_ref);
    270 
    271         Ok(())
    272     }
    273 
    274     /// Reads the `Metadata` field referenced in the PDF document's `Catalog` entry. Will return
    275     /// `None` if no Metadata is present.
    276     fn read_xmp(&self) -> Option<String> {
    277         self.document
    278             .catalog()
    279             .and_then(|catalog| catalog.get_deref(b"Metadata", &self.document))
    280             .and_then(Object::as_stream)
    281             .ok()
    282             .and_then(|stream_dict| {
    283                 let Ok(subtype_str) = stream_dict
    284                     .dict
    285                     .get_deref(SUBTYPE_KEY, &self.document)
    286                     .and_then(Object::as_name_str)
    287                 else {
    288                     return None;
    289                 };
    290 
    291                 if subtype_str.to_lowercase() != "xml" {
    292                     return None;
    293                 }
    294 
    295                 String::from_utf8(stream_dict.content.clone()).ok()
    296             })
    297     }
    298 }
    299 
    300 impl Pdf {
    301     #[allow(dead_code)]
    302     pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
    303         let document = Document::load_mem(bytes)?;
    304         Ok(Self { document })
    305     }
    306 
    307     pub fn from_reader<R: Read>(source: R) -> Result<Self, Error> {
    308         let document = Document::load_from(source)?;
    309         Ok(Self { document })
    310     }
    311 
    312     /// Returns a reference to the Associated Files array from the PDF's Catalog.
    313     fn associated_files(&self) -> Result<&Vec<Object>, Error> {
    314         Ok(self
    315             .document
    316             .catalog()?
    317             .get_deref(ASSOCIATED_FILE_KEY, &self.document)?
    318             .as_array()?)
    319     }
    320 
    321     /// Returns the [Object::ObjectId] of the C2PA File Spec Reference, if it is present in the
    322     /// PDF's associated files array.
    323     fn c2pa_file_spec_object_id(&self) -> Option<ObjectId> {
    324         self.associated_files().ok()?.iter().find_map(|value| {
    325             let Ok(reference) = value.as_reference() else {
    326                 return None;
    327             };
    328 
    329             let name = self
    330                 .document
    331                 .get_object(reference)
    332                 .and_then(Object::as_dict)
    333                 .and_then(|dict| dict.get_deref(AF_RELATIONSHIP_KEY, &self.document))
    334                 .and_then(Object::as_name)
    335                 .ok()?;
    336 
    337             (name == C2PA_RELATIONSHIP).then_some(reference)
    338         })
    339     }
    340 
    341     /// Removes the C2PA File Spec Reference if it exists in the Associated Files [Object::Array] of
    342     /// PDF's catalog. This will return an [Err] if the PDF doesn't contain a C2PA File Spec
    343     /// Reference.
    344     fn remove_c2pa_file_spec_reference(&mut self) -> Result<(), Error> {
    345         let c2pa_file_spec_reference = self
    346             .c2pa_file_spec_object_id()
    347             .ok_or_else(|| Error::FindingC2PAFileSpec)?;
    348 
    349         self.document
    350             .catalog_mut()?
    351             .get_mut(ASSOCIATED_FILE_KEY)?
    352             .as_array_mut()?
    353             .retain(|v| {
    354                 let Ok(reference) = v.as_reference() else {
    355                     return true;
    356                 };
    357 
    358                 reference != c2pa_file_spec_reference
    359             });
    360 
    361         Ok(())
    362     }
    363 
    364     /// Adds the C2PA `Annotation` to the PDF.
    365     ///
    366     /// ### Note:
    367     /// The `FileAttachment` annotation is added to the first page of the PDF in the lower
    368     /// left-hand corner. The `FileAttachment`'s location is not defined in the spec as of version
    369     /// `1.3`.
    370     fn add_file_attachment_annotation(
    371         &mut self,
    372         file_spec_reference: ObjectId,
    373     ) -> Result<(), Error> {
    374         let annotation = dictionary! {
    375             "Type" => Name("Annot".into()),
    376             "Contents" => Object::string_literal(CONTENT_CREDS),
    377             "Name" => Object::string_literal(CONTENT_CREDS),
    378             SUBTYPE_KEY => Name("FileAttachment".into()),
    379             "FS" => Reference(file_spec_reference),
    380             // Places annotation in the lower left-hand corner. The icon will be 10x10.
    381             "Rect" => vec![0.into(), 0.into(), 10.into(), 10.into()],
    382         };
    383 
    384         // Add C2PA annotation as an indirect object.
    385         let annotation_ref = self.document.add_object(annotation);
    386 
    387         // Find the reference to the first page of the PDF.
    388         let first_page_ref = self
    389             .document
    390             .page_iter()
    391             .next()
    392             .ok_or_else(|| Error::AddingAnnotation)?;
    393 
    394         // Get a mutable ref to the first page as a Dictionary object.
    395         let first_page = self
    396             .document
    397             .get_object_mut(first_page_ref)?
    398             .as_dict_mut()?;
    399 
    400         // Ensures the /Annots array exists on the page object.
    401         if !first_page.has(ANNOTATIONS_KEY) {
    402             first_page.set(ANNOTATIONS_KEY, Array(vec![]))
    403         }
    404 
    405         // Follows a reference to the indirect annotations array, if it exists.
    406         let annotation_object = first_page.get_mut(ANNOTATIONS_KEY)?;
    407         let annotations = if let Ok(v) = annotation_object.as_reference() {
    408             self.document.get_object_mut(v)?
    409         } else {
    410             annotation_object
    411         }
    412         .as_array_mut()?;
    413 
    414         annotations.push(Reference(annotation_ref));
    415         Ok(())
    416     }
    417 
    418     /// Creates, or appends to, the Associated File (`AF`) array the embedded file spec reference of the
    419     /// C2PA data.
    420     fn push_associated_file(&mut self, embedded_file_spec_ref: ObjectId) -> Result<(), Error> {
    421         let catalog = self.document.catalog_mut()?;
    422         if catalog.get_mut(ASSOCIATED_FILE_KEY).is_err() {
    423             // Add associated files array to catalog if it isn't already present.
    424             catalog.set(ASSOCIATED_FILE_KEY, vec![]);
    425         }
    426 
    427         let associated_files = catalog.get_mut(ASSOCIATED_FILE_KEY)?;
    428         let associated_files = match associated_files.as_reference() {
    429             Ok(object_id) => self.document.get_object_mut(object_id)?,
    430             _ => associated_files,
    431         }
    432         .as_array_mut()?;
    433 
    434         associated_files.push(Reference(embedded_file_spec_ref));
    435 
    436         Ok(())
    437     }
    438 
    439     /// Adds the `Embedded File Specification` to the PDF document. Returns the [Reference]
    440     /// to the added `Embedded File Specification`.
    441     fn add_embedded_file_specification(&mut self, file_stream_ref: ObjectId) -> ObjectId {
    442         let embedded_file_stream = dictionary! {
    443             AF_RELATIONSHIP_KEY => Name(C2PA_RELATIONSHIP.into()),
    444             "Desc" => Object::string_literal(CONTENT_CREDS),
    445             "F" => Object::string_literal(CONTENT_CREDS),
    446             "EF" => dictionary! {
    447                 "F" => Reference(file_stream_ref),
    448             },
    449             TYPE_KEY => Name("FileSpec".into()),
    450             "UF" => Object::string_literal(CONTENT_CREDS),
    451         };
    452 
    453         self.document.add_object(embedded_file_stream)
    454     }
    455 
    456     /// Adds the provided `bytes` as a `StreamDictionary` to the PDF document. Returns the
    457     /// [Reference] of the added [Object].
    458     fn add_c2pa_embedded_file_stream(&mut self, bytes: Vec<u8>) -> ObjectId {
    459         let stream = Stream::new(
    460             dictionary! {
    461                 "F" => dictionary! {
    462                 SUBTYPE_KEY => C2PA_MIME_TYPE,
    463                 "Length" => Integer(bytes.len() as i64),
    464                 },
    465             },
    466             bytes,
    467         );
    468 
    469         self.document.add_object(stream)
    470     }
    471 
    472     /// Remove the C2PA Manifest `Annotation` from the PDF.
    473     fn remove_manifest_from_annotations(&mut self) -> Result<(), Error> {
    474         for (_, page_id) in self.document.get_pages() {
    475             self.document
    476                 .get_object_mut(page_id)?
    477                 .as_dict_mut()?
    478                 .get_mut(ANNOTATIONS_KEY)?
    479                 .as_array_mut()?
    480                 .retain(|obj| {
    481                     obj.as_dict()
    482                         .and_then(|annot| annot.get(TYPE_KEY))
    483                         .and_then(Object::as_name_str)
    484                         .map(|str| str != CONTENT_CREDS)
    485                         .unwrap_or(true)
    486                 });
    487         }
    488 
    489         Ok(())
    490     }
    491 
    492     /// Removes the manifest from the PDF's embedded files collection.
    493     fn remove_manifest_from_embedded_files(&mut self) -> Result<(), Error> {
    494         let Ok(names) = self.document.catalog_mut()?.get_mut(NAMES_KEY) else {
    495             return Err(Error::NoManifest);
    496         };
    497 
    498         // Follows the reference to the /Names Dictionary.
    499         let names_dictionary = match names.as_reference() {
    500             Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?,
    501             _ => names.as_dict_mut()?,
    502         };
    503 
    504         // Follows the reference to the /EmbeddedFiles Dictionary.
    505         let embedded_files_object = names_dictionary.get_mut(EMBEDDED_FILES_KEY)?;
    506         let embedded_files_dictionary = match embedded_files_object.as_reference() {
    507             Ok(object_id) => self.document.get_object_mut(object_id)?.as_dict_mut()?,
    508             _ => embedded_files_object.as_dict_mut()?,
    509         };
    510 
    511         // Gets the /Names array from the /EmbeddedFiles Dictionary. This will contain the reference
    512         // to the C2PA manifest.
    513         let names_vector_object = embedded_files_dictionary.get_mut(NAMES_KEY)?;
    514         let names_vector = match names_vector_object.as_reference() {
    515             Ok(object_id) => self.document.get_object_mut(object_id)?.as_array_mut()?,
    516             _ => names_vector_object.as_array_mut()?,
    517         };
    518 
    519         // Find the "Content Credentials" marker name in the /Names Array.
    520         let content_creds_marker_idx = names_vector
    521             .iter()
    522             .position(|value| {
    523                 value
    524                     .as_string()
    525                     .map(|value| value == CONTENT_CREDS)
    526                     .unwrap_or_default()
    527             })
    528             .ok_or_else(|| Error::UnableToFindEmbeddedFileManifest)?;
    529 
    530         let content_creds_reference_idx = content_creds_marker_idx + 1;
    531         if content_creds_reference_idx >= names_vector.len() {
    532             return Err(Error::UnableToFindEmbeddedFileManifest);
    533         }
    534 
    535         // Delete the "Content Credentials" marker object and the reference to the C2PA
    536         // manifest in the PDF's embedded files.
    537         names_vector.drain(content_creds_marker_idx..=content_creds_reference_idx);
    538 
    539         Ok(())
    540     }
    541 }
    542 
    543 #[cfg(test)]
    544 mod tests {
    545     #![allow(clippy::unwrap_used)]
    546 
    547     use super::*;
    548 
    549     #[cfg(target_arch = "wasm32")]
    550     wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
    551 
    552     #[cfg(target_arch = "wasm32")]
    553     use wasm_bindgen_test::*;
    554 
    555     #[cfg_attr(not(target_arch = "wasm32"), test)]
    556     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    557     fn test_loads_pdf_from_bytes() {
    558         let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
    559         let pdf_result = Pdf::from_bytes(bytes);
    560         assert!(pdf_result.is_ok());
    561     }
    562 
    563     #[cfg_attr(not(target_arch = "wasm32"), test)]
    564     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    565     fn test_loads_pdf_from_bytes_with_invalid_file() {
    566         let bytes = include_bytes!("../../tests/fixtures/XCA.jpg");
    567         let pdf_result = Pdf::from_bytes(bytes);
    568         assert!(matches!(pdf_result, Err(Error::UnableToReadPdf(_))));
    569     }
    570 
    571     #[cfg_attr(not(target_arch = "wasm32"), test)]
    572     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    573     fn test_is_password_protected() {
    574         let bytes = include_bytes!("../../tests/fixtures/basic-password.pdf");
    575         let pdf_result = Pdf::from_bytes(bytes).unwrap();
    576         assert!(pdf_result.is_password_protected());
    577 
    578         let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
    579         let pdf = Pdf::from_bytes(bytes).unwrap();
    580         assert!(!pdf.is_password_protected());
    581     }
    582 
    583     #[cfg_attr(not(target_arch = "wasm32"), test)]
    584     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    585     fn test_has_c2pa_manifest_on_file_without_manifest() {
    586         let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
    587         let pdf = Pdf::from_bytes(bytes).unwrap();
    588         assert!(!pdf.has_c2pa_manifest())
    589     }
    590 
    591     #[cfg_attr(not(target_arch = "wasm32"), test)]
    592     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    593     fn test_has_c2pa_manifest_on_file_with_manifest() {
    594         let bytes = include_bytes!("../../tests/fixtures/basic.pdf");
    595         let mut pdf = Pdf::from_bytes(bytes).unwrap();
    596         assert!(!pdf.has_c2pa_manifest());
    597 
    598         pdf.write_manifest_as_annotation(vec![0u8, 1u8]).unwrap();
    599         assert!(pdf.has_c2pa_manifest());
    600     }
    601 
    602     #[cfg_attr(not(target_arch = "wasm32"), test)]
    603     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    604     fn test_adds_embedded_file_spec_to_pdf_stream() {
    605         let bytes = include_bytes!("../../tests/fixtures/express.pdf");
    606         let mut pdf = Pdf::from_bytes(bytes).unwrap();
    607         let object_count_before_add = pdf.document.objects.len();
    608 
    609         let bytes = vec![10u8];
    610         let id = pdf.add_c2pa_embedded_file_stream(bytes.clone());
    611 
    612         // Object added to the PDF's object collection.
    613         assert_eq!(object_count_before_add + 1, pdf.document.objects.len());
    614 
    615         // We are able to find the object.
    616         let stream = pdf.document.get_object(id);
    617         assert_eq!(stream.unwrap().as_stream().unwrap().content, bytes);
    618     }
    619 
    620     #[cfg_attr(not(target_arch = "wasm32"), test)]
    621     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    622     fn test_write_manifest_as_annotation() {
    623         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/express.pdf")).unwrap();
    624         assert!(!pdf.has_c2pa_manifest());
    625         pdf.write_manifest_as_annotation(vec![10u8, 20u8]).unwrap();
    626         assert!(pdf.has_c2pa_manifest());
    627     }
    628 
    629     #[cfg_attr(not(target_arch = "wasm32"), test)]
    630     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    631     fn test_write_manifest_bytes_to_pdf_with_existing_annotations() {
    632         let mut pdf =
    633             Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic-annotation.pdf")).unwrap();
    634         pdf.write_manifest_as_annotation(vec![10u8, 20u8]).unwrap();
    635         assert!(pdf.has_c2pa_manifest());
    636     }
    637 
    638     #[cfg_attr(not(target_arch = "wasm32"), test)]
    639     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    640     fn test_add_manifest_to_embedded_files() {
    641         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    642         pdf.write_manifest_as_embedded_file(vec![10u8, 20u8])
    643             .unwrap();
    644 
    645         assert!(pdf.has_c2pa_manifest());
    646     }
    647 
    648     #[cfg_attr(not(target_arch = "wasm32"), test)]
    649     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    650     fn test_add_manifest_to_embedded_files_attachments_present() {
    651         let mut pdf =
    652             Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic-attachments.pdf")).unwrap();
    653         pdf.write_manifest_as_embedded_file(vec![10u8, 20u8])
    654             .unwrap();
    655 
    656         assert!(pdf.has_c2pa_manifest());
    657     }
    658 
    659     #[cfg_attr(not(target_arch = "wasm32"), test)]
    660     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    661     fn test_save_to() {
    662         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    663         assert!(!pdf.has_c2pa_manifest());
    664 
    665         pdf.write_manifest_as_annotation(vec![10u8]).unwrap();
    666         assert!(pdf.has_c2pa_manifest());
    667 
    668         let mut saved_bytes = vec![];
    669         pdf.save_to(&mut saved_bytes).unwrap();
    670 
    671         let saved_pdf = Pdf::from_bytes(&saved_bytes).unwrap();
    672         assert!(saved_pdf.has_c2pa_manifest());
    673     }
    674 
    675     #[cfg_attr(not(target_arch = "wasm32"), test)]
    676     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    677     fn test_reads_manifest_bytes_for_embedded_files_manifest() {
    678         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/express.pdf")).unwrap();
    679         assert!(!pdf.has_c2pa_manifest());
    680 
    681         let manifest_bytes = vec![0u8, 1u8, 1u8, 2u8, 3u8];
    682         pdf.write_manifest_as_embedded_file(manifest_bytes.clone())
    683             .unwrap();
    684 
    685         assert!(pdf.has_c2pa_manifest());
    686         assert!(matches!(
    687             pdf.read_manifest_bytes(),
    688             Ok(Some(manifests)) if manifests[0] == manifest_bytes
    689         ));
    690     }
    691 
    692     #[cfg_attr(not(target_arch = "wasm32"), test)]
    693     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    694     fn test_reads_manifest_bytes_for_annotation_manifest() {
    695         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    696         assert!(!pdf.has_c2pa_manifest());
    697 
    698         let manifest_bytes = vec![0u8, 1u8, 1u8, 2u8, 3u8];
    699         pdf.write_manifest_as_annotation(manifest_bytes.clone())
    700             .unwrap();
    701 
    702         assert!(pdf.has_c2pa_manifest());
    703         assert!(matches!(
    704             pdf.read_manifest_bytes(),
    705             Ok(Some(manifests)) if manifests[0] == manifest_bytes
    706         ));
    707     }
    708 
    709     #[cfg_attr(not(target_arch = "wasm32"), test)]
    710     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    711     fn test_read_manifest_bytes_from_pdf_without_bytes_returns_none() {
    712         let pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    713         assert!(!pdf.has_c2pa_manifest());
    714         assert!(matches!(pdf.read_manifest_bytes(), Ok(None)));
    715     }
    716 
    717     #[cfg_attr(not(target_arch = "wasm32"), test)]
    718     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    719     fn test_read_manifest_bytes_from_pdf_with_other_af_relationship_returns_none() {
    720         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    721         pdf.document
    722             .catalog_mut()
    723             .unwrap()
    724             .set(ASSOCIATED_FILE_KEY, vec![Reference((100, 0))]);
    725 
    726         assert!(matches!(pdf.read_manifest_bytes(), Ok(None)));
    727     }
    728 
    729     #[cfg_attr(not(target_arch = "wasm32"), test)]
    730     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    731     fn test_read_pdf_with_associated_file_that_is_not_manifest() {
    732         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    733         pdf.document
    734             .catalog_mut()
    735             .unwrap()
    736             .set(ASSOCIATED_FILE_KEY, Reference((100, 0)));
    737 
    738         assert!(matches!(pdf.read_manifest_bytes(), Ok(None)));
    739     }
    740 
    741     #[cfg_attr(not(target_arch = "wasm32"), test)]
    742     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    743     fn test_read_xmp_on_pdf_with_none() {
    744         let pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic-no-xmp.pdf")).unwrap();
    745         assert!(pdf.read_xmp().is_none());
    746     }
    747 
    748     #[cfg_attr(not(target_arch = "wasm32"), test)]
    749     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    750     fn test_read_xmp_on_pdf_with_some_metadata() {
    751         let pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    752         assert!(pdf.read_xmp().is_some());
    753     }
    754 
    755     #[cfg_attr(not(target_arch = "wasm32"), test)]
    756     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    757     fn test_remove_manifest_bytes_from_file_without_c2pa_returns_error() {
    758         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    759 
    760         assert!(matches!(
    761             pdf.remove_manifest_bytes(),
    762             Err(Error::NoManifest)
    763         ));
    764     }
    765 
    766     #[cfg_attr(not(target_arch = "wasm32"), test)]
    767     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    768     fn test_remove_manifest_from_file_with_annotation_based_manifest() {
    769         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    770         let manifest_bytes = vec![0u8, 1u8, 1u8, 2u8, 3u8];
    771         pdf.write_manifest_as_annotation(manifest_bytes.clone())
    772             .unwrap();
    773 
    774         assert!(pdf.has_c2pa_manifest());
    775         assert!(pdf.remove_manifest_bytes().is_ok());
    776         assert!(!pdf.has_c2pa_manifest());
    777     }
    778 
    779     #[cfg_attr(not(target_arch = "wasm32"), test)]
    780     #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
    781     fn test_remove_manifest_from_file_with_embedded_file_based_manifest() {
    782         let mut pdf = Pdf::from_bytes(include_bytes!("../../tests/fixtures/basic.pdf")).unwrap();
    783         let manifest_bytes = vec![0u8, 1u8, 1u8, 2u8, 3u8];
    784 
    785         pdf.write_manifest_as_embedded_file(manifest_bytes.clone())
    786             .unwrap();
    787 
    788         assert!(pdf.has_c2pa_manifest());
    789         assert!(pdf.remove_manifest_bytes().is_ok());
    790         assert!(!pdf.has_c2pa_manifest());
    791     }
    792 }