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 }