1use pdfboss_core::{Dict, Document, Name, Object};
6#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
7use pdfboss_core::{Encryptor, Permissions};
8
9use crate::error::{Error, Result};
10use crate::importer::Importer;
11use crate::pdf::Metadata;
12use crate::update::{
13 catalog_metadata_ref, core_error, merge_metadata, resolve_dict, xmp_metadata_stream,
14};
15use crate::writer::{WriteOptions, Writer};
16
17pub fn merge_documents(
27 inputs: &[(&Document, Option<&[usize]>)],
28 options: WriteOptions,
29) -> Result<Vec<u8>> {
30 let mut writer = Writer::new(options);
31 let pages_ref = writer.reserve();
32 let mut kids = Vec::new();
33 for (source, selection) in inputs {
34 let mut importer = Importer::new(&mut writer, source)?;
35 let indices: Vec<usize> = match selection {
36 Some(indices) => indices.to_vec(),
37 None => (0..source.page_count()).collect(),
38 };
39 for index in indices {
40 kids.push(importer.page(index, pages_ref)?);
41 }
42 }
43 if kids.is_empty() {
44 return Err(Error::Other(
45 "a document needs at least one page".to_string(),
46 ));
47 }
48 let mut tree = Dict::new();
49 tree.insert(name("Type"), Object::Name(name("Pages")));
50 tree.insert(
51 name("Kids"),
52 Object::Array(kids.iter().copied().map(Object::Ref).collect()),
53 );
54 tree.insert(name("Count"), Object::Int(kids.len() as i64));
55 writer.fill(pages_ref, Object::Dict(tree))?;
56 let mut catalog = Dict::new();
57 catalog.insert(name("Type"), Object::Name(name("Catalog")));
58 catalog.insert(name("Pages"), Object::Ref(pages_ref));
59 let root = writer.put(Object::Dict(catalog));
60 writer.finish(root)
61}
62
63fn name(text: &str) -> Name {
65 Name(text.to_string())
66}
67
68pub fn rotate_rewrite(
80 doc: &Document,
81 pages: &[usize],
82 by: i32,
83 options: WriteOptions,
84) -> Result<Vec<u8>> {
85 if by % 90 != 0 {
86 return Err(Error::Other(
87 "rotation must be a multiple of 90 degrees".to_string(),
88 ));
89 }
90 let mut writer = Writer::new(options);
91 let mut importer = Importer::new(&mut writer, doc)?;
92 let new_info = doc
93 .xref()
94 .trailer
95 .get_ref("Info")
96 .map(|info| importer.reference(info));
97 for &index in pages {
98 let page = doc.page(index).map_err(core_error)?;
99 let Some(page_ref) = page.object_ref() else {
100 return Err(Error::Other(format!(
101 "page {} is inlined into /Kids and cannot be edited in place; \
102 pdfboss does not yet restructure such pages to rotate them",
103 index + 1
104 )));
105 };
106 let mut dict = page.dict().clone();
107 let rotate = (page.rotate + by).rem_euclid(360);
108 dict.insert(name("Rotate"), Object::Int(i64::from(rotate)));
109 let body = importer.copy(&Object::Dict(dict))?;
110 importer.substitute(page_ref, body);
111 }
112 let new_root = importer.document()?;
113 if let Some(new_info) = new_info {
114 writer.set_info(new_info);
115 }
116 writer.finish(new_root)
117}
118
119pub fn rewrite_document(doc: &Document, options: WriteOptions) -> Result<Vec<u8>> {
125 rewrite_into(Writer::new(options), doc)
126}
127
128fn rewrite_into(mut writer: Writer, doc: &Document) -> Result<Vec<u8>> {
133 let mut importer = Importer::new(&mut writer, doc)?;
134 let new_info = doc
135 .xref()
136 .trailer
137 .get_ref("Info")
138 .map(|info| importer.reference(info));
139 let new_root = importer.document()?;
140 if let Some(new_info) = new_info {
141 writer.set_info(new_info);
142 }
143 writer.finish(new_root)
144}
145
146#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
164pub fn encrypt_document(
165 doc: &Document,
166 user_password: &str,
167 owner_password: &str,
168 permissions: Permissions,
169 options: WriteOptions,
170) -> Result<Vec<u8>> {
171 if user_password.is_empty() && owner_password.is_empty() {
172 return Err(Error::Other(
173 "user_password and owner_password cannot both be empty".to_string(),
174 ));
175 }
176 let owner_password = if owner_password.is_empty() {
177 user_password
178 } else {
179 owner_password
180 };
181 let (encryptor, encrypt_dict) = Encryptor::aes256(user_password, owner_password, permissions);
182 rewrite_into(Writer::new_encrypted(options, encryptor, encrypt_dict), doc)
183}
184
185pub fn decrypt_document(doc: &Document, options: WriteOptions) -> Result<Vec<u8>> {
196 rewrite_document(doc, options)
197}
198
199pub fn rewrite_with_metadata(
212 doc: &Document,
213 meta: Metadata,
214 options: WriteOptions,
215) -> Result<Vec<u8>> {
216 let mut writer = Writer::new(options);
217 let mut importer = Importer::new(&mut writer, doc)?;
218 let trailer = &doc.xref().trailer;
219 let root = trailer.get_ref("Root").ok_or(Error::MissingRoot)?;
220 let info_ref = trailer.get_ref("Info");
221 let existing_dict = info_ref.and_then(|r| {
222 let dict = doc.get(r).ok()?.as_dict()?.clone();
223 Some(resolve_dict(doc, &dict))
224 });
225 let xmp_ref = catalog_metadata_ref(doc, root);
226 let (dict, merged) = merge_metadata(existing_dict, &meta);
227
228 let new_info_target = match info_ref {
229 Some(r) => {
230 let target = importer.reference(r);
231 let body = importer.copy(&Object::Dict(dict.clone()))?;
232 importer.substitute(r, body);
233 Some(target)
234 }
235 None => None,
236 };
237 if let Some(r) = xmp_ref {
238 importer.substitute(r, xmp_metadata_stream(&merged));
239 }
240
241 let new_root = importer.document()?;
242 let new_info = match new_info_target {
243 Some(target) => target,
244 None => writer.put(Object::Dict(dict)),
245 };
246 writer.set_info(new_info);
247 writer.finish(new_root)
248}
249
250pub fn split_document(doc: &Document, every: usize, options: WriteOptions) -> Result<Vec<Vec<u8>>> {
254 if every == 0 {
255 return Err(Error::Other(
256 "every must be at least 1 page per part".to_string(),
257 ));
258 }
259 let total = doc.page_count();
260 let mut parts = Vec::new();
261 let mut start = 0;
262 while start < total {
263 let end = (start + every).min(total);
264 let indices: Vec<usize> = (start..end).collect();
265 parts.push(merge_documents(&[(doc, Some(&indices))], options)?);
266 start = end;
267 }
268 Ok(parts)
269}
270
271#[cfg(test)]
272mod tests {
273 use pdfboss_core::xref::{parse_section_at, startxref, XrefEntry};
274 use pdfboss_output::{extract_text, ReadingOrder};
275 use pdfboss_testkit::{encrypted_rc4_doc, multi_page_doc, PdfBuilder};
276
277 use crate::pdf::{Metadata, Page, PageSize, Pdf};
278 use crate::update::Update;
279 use crate::writer::XrefStyle;
280
281 use super::*;
282
283 #[test]
284 fn merge_keeps_sources_in_argument_order() {
285 let a = Document::load(multi_page_doc(&["a1", "a2"])).expect("doc a loads");
286 let b = Document::load(multi_page_doc(&["b1", "b2"])).expect("doc b loads");
287 let bytes = merge_documents(&[(&a, None), (&b, None)], WriteOptions::default())
288 .expect("merge succeeds");
289 let merged = Document::load(bytes).expect("merged document loads");
290 assert_eq!(merged.page_count(), 4);
291 let texts: Vec<String> = (0..4)
292 .map(|i| {
293 let page = merged.page(i).expect("page exists");
294 extract_text(&merged, &page, ReadingOrder::Content).expect("text extracts")
295 })
296 .collect();
297 assert!(texts[0].contains("a1"), "page 0: {:?}", texts[0]);
298 assert!(texts[1].contains("a2"), "page 1: {:?}", texts[1]);
299 assert!(texts[2].contains("b1"), "page 2: {:?}", texts[2]);
300 assert!(texts[3].contains("b2"), "page 3: {:?}", texts[3]);
301 }
302
303 #[test]
304 fn a_range_selects_and_reorders_pages() {
305 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("doc loads");
306 let bytes = merge_documents(&[(&doc, Some(&[2, 0]))], WriteOptions::default())
307 .expect("merge succeeds");
308 let merged = Document::load(bytes).expect("merged document loads");
309 assert_eq!(merged.page_count(), 2);
310 let first = merged.page(0).expect("first page exists");
311 let second = merged.page(1).expect("second page exists");
312 assert!(extract_text(&merged, &first, ReadingOrder::Content)
313 .unwrap()
314 .contains("three"));
315 assert!(extract_text(&merged, &second, ReadingOrder::Content)
316 .unwrap()
317 .contains("one"));
318 }
319
320 #[test]
326 fn merge_documents_accepts_a_password_opened_encrypted_source() {
327 let doc = Document::load(encrypted_rc4_doc("secret")).expect("empty-password doc loads");
328 let bytes = merge_documents(&[(&doc, None)], WriteOptions::default())
329 .expect("a password-opened encrypted source is not locked, so merge succeeds");
330 let merged = Document::load(bytes).expect("merged document loads");
331 assert!(
332 !merged.is_encrypted(),
333 "the merged output carries no /Encrypt"
334 );
335 let page = merged.page(0).expect("page 0 exists");
336 let text = extract_text(&merged, &page, ReadingOrder::Content).expect("text extracts");
337 assert!(text.contains("secret"), "{text:?}");
338 }
339
340 #[test]
341 fn split_makes_parts_of_the_requested_size_and_a_shorter_last_part() {
342 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("doc loads");
343 let parts = split_document(&doc, 2, WriteOptions::default()).expect("split succeeds");
344 assert_eq!(parts.len(), 2);
345
346 let first = Document::load(parts[0].clone()).expect("first part loads");
347 assert_eq!(first.page_count(), 2);
348 let texts: Vec<String> = (0..2)
349 .map(|i| {
350 let page = first.page(i).expect("page exists");
351 extract_text(&first, &page, ReadingOrder::Content).expect("text extracts")
352 })
353 .collect();
354 assert!(texts[0].contains("one"), "page 0: {:?}", texts[0]);
355 assert!(texts[1].contains("two"), "page 1: {:?}", texts[1]);
356
357 let second = Document::load(parts[1].clone()).expect("second part loads");
358 assert_eq!(second.page_count(), 1);
359 let page = second.page(0).expect("page exists");
360 let text = extract_text(&second, &page, ReadingOrder::Content).expect("text extracts");
361 assert!(text.contains("three"), "page 0: {:?}", text);
362 }
363
364 #[test]
365 fn split_larger_than_the_page_count_makes_one_part() {
366 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("doc loads");
367 let parts = split_document(&doc, 10, WriteOptions::default()).expect("split succeeds");
368 assert_eq!(parts.len(), 1);
369 let only = Document::load(parts[0].clone()).expect("part loads");
370 assert_eq!(only.page_count(), 3);
371 }
372
373 #[test]
374 fn split_rejects_zero_pages_per_part_with_an_honest_message() {
375 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("doc loads");
376 let result = split_document(&doc, 0, WriteOptions::default());
377 let Err(Error::Other(message)) = result else {
378 panic!("expected Error::Other, got {result:?}");
379 };
380 assert!(message.contains("every"), "message: {message}");
381 }
382
383 #[test]
388 fn rotate_rewrite_rotates_the_selected_pages() {
389 let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("doc loads");
390 let bytes =
391 rotate_rewrite(&doc, &[0, 2], 90, WriteOptions::default()).expect("rotate succeeds");
392 let rotated = Document::load(bytes).expect("rotated document loads");
393 for (index, expected) in [90, 0, 90].iter().enumerate() {
394 let page = rotated.page(index).expect("page exists");
395 assert_eq!(page.rotate, *expected, "page {index}");
396 }
397 }
398
399 #[test]
404 fn rotate_rewrite_refuses_an_inline_page() {
405 let mut b = PdfBuilder::new();
406 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
407 b.object(
408 2,
409 "<< /Type /Pages /Count 1 /Kids [ << /Type /Page /Parent 2 0 R \
410 /MediaBox [0 0 612 792] >> ] >>",
411 );
412 let doc = Document::load(b.build(1)).expect("doc loads");
413
414 let result = rotate_rewrite(&doc, &[0], 90, WriteOptions::default());
415 let Err(Error::Other(message)) = result else {
416 panic!("expected Error::Other, got {result:?}");
417 };
418 assert!(message.contains("page 1"), "message: {message}");
419 assert!(
420 message.contains("cannot be edited in place"),
421 "message: {message}"
422 );
423 assert!(
424 message.contains("does not yet restructure"),
425 "message: {message}"
426 );
427 }
428
429 #[test]
433 fn rotate_rewrite_refuses_a_non_multiple_of_90() {
434 let doc = Document::load(multi_page_doc(&["one"])).expect("doc loads");
435 let result = rotate_rewrite(&doc, &[0], 45, WriteOptions::default());
436 let Err(Error::Other(message)) = result else {
437 panic!("expected Error::Other, got {result:?}");
438 };
439 assert!(message.contains("multiple of 90"), "message: {message}");
440 }
441
442 #[test]
445 fn rotate_rewrite_accepts_a_negative_multiple_of_90() {
446 let doc = Document::load(multi_page_doc(&["one"])).expect("doc loads");
447 let bytes =
448 rotate_rewrite(&doc, &[0], -90, WriteOptions::default()).expect("rotate succeeds");
449 let rotated = Document::load(bytes).expect("rotated document loads");
450 let page = rotated.page(0).expect("page exists");
451 assert_eq!(page.rotate, 270);
452 }
453
454 #[test]
458 fn rotate_rewrite_carries_info_along() {
459 let base = Pdf {
460 pages: vec![Page::new(PageSize::A4)],
461 metadata: Some(Metadata {
462 title: Some("Rotated Title".to_string()),
463 ..Metadata::default()
464 }),
465 ..Pdf::default()
466 }
467 .to_bytes()
468 .expect("base builds");
469 let doc = Document::load(base).expect("base loads");
470 assert!(
471 doc.xref().trailer.get_ref("Info").is_some(),
472 "the base's trailer must carry /Info for this test to exercise the carry"
473 );
474
475 let bytes =
476 rotate_rewrite(&doc, &[0], 90, WriteOptions::default()).expect("rotate succeeds");
477 let rotated = Document::load(bytes).expect("rotated document loads");
478 assert!(
479 rotated.xref().trailer.get_ref("Info").is_some(),
480 "the rewritten trailer still names an /Info dictionary"
481 );
482 assert_eq!(rotated.metadata().title.as_deref(), Some("Rotated Title"));
483 }
484
485 fn live_count(doc: &Document) -> usize {
489 doc.xref()
490 .iter()
491 .filter(|(_, entry)| !matches!(entry, XrefEntry::Free))
492 .count()
493 }
494
495 #[test]
499 fn rewrite_document_carries_info_along() {
500 let base = Pdf {
501 pages: vec![Page::new(PageSize::A4)],
502 metadata: Some(Metadata {
503 title: Some("Rewritten Title".to_string()),
504 ..Metadata::default()
505 }),
506 ..Pdf::default()
507 }
508 .to_bytes()
509 .expect("base builds");
510 let doc = Document::load(base).expect("base loads");
511 assert!(
512 doc.xref().trailer.get_ref("Info").is_some(),
513 "the base's trailer must carry /Info for this test to exercise the carry"
514 );
515
516 let bytes = rewrite_document(&doc, WriteOptions::default()).expect("rewrite succeeds");
517 let rewritten = Document::load(bytes).expect("rewritten document loads");
518 assert!(
519 rewritten.xref().trailer.get_ref("Info").is_some(),
520 "the rewritten trailer still names an /Info dictionary"
521 );
522 assert_eq!(
523 rewritten.metadata().title.as_deref(),
524 Some("Rewritten Title")
525 );
526 }
527
528 #[test]
532 fn rewrite_document_drops_an_unreferenced_object_and_keeps_text() {
533 let options = WriteOptions {
534 xref: XrefStyle::Table,
535 compress: false,
536 object_streams: false,
537 version: (1, 7),
538 };
539 let mut b = PdfBuilder::new();
540 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
541 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
542 b.object(
543 3,
544 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
545 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
546 );
547 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (hello) Tj ET");
548 b.object(
549 5,
550 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
551 );
552 b.object(6, "<< /Extra (unreferenced) >>");
553 let doc = Document::load(b.build(1)).expect("doc loads");
554 assert_eq!(
555 live_count(&doc),
556 6,
557 "the fixture carries the extra object alongside the reachable five"
558 );
559
560 let bytes = rewrite_document(&doc, options).expect("rewrite succeeds");
561 let rewritten = Document::load(bytes).expect("rewritten document loads");
562 assert_eq!(
563 live_count(&rewritten),
564 5,
565 "the unreferenced object must not survive the rewrite"
566 );
567
568 let page = rewritten.page(0).expect("page exists");
569 let text = extract_text(&rewritten, &page, ReadingOrder::Content).expect("text extracts");
570 assert!(text.contains("hello"), "text: {text:?}");
571 }
572
573 #[test]
578 fn rewrite_document_collapses_an_appended_update_into_one_section() {
579 let base = Pdf {
580 pages: vec![Page::new(PageSize::A4)],
581 ..Pdf::default()
582 }
583 .to_bytes()
584 .expect("base builds");
585 let base_doc = Document::load(base).expect("base loads");
586 let mut update = Update::new(&base_doc).expect("update opens");
587 let extra = update.reserve();
588 let mut dict = Dict::new();
589 dict.insert(name("Marker"), Object::Int(1));
590 update.set(extra, Object::Dict(dict));
591 let appended = update.bytes().expect("update appends");
592
593 let control_offset = startxref(&appended).expect("startxref present in the input");
594 let control_section =
595 parse_section_at(&appended, control_offset).expect("input section parses");
596 assert!(
597 control_section.prev.is_some(),
598 "the input must really carry a /Prev chain for this test to exercise the collapse"
599 );
600
601 let appended_doc = Document::load(appended).expect("appended document loads");
602 let bytes =
603 rewrite_document(&appended_doc, WriteOptions::default()).expect("rewrite succeeds");
604
605 let offset = startxref(&bytes).expect("startxref present");
606 let section = parse_section_at(&bytes, offset).expect("section parses");
607 assert!(
608 section.prev.is_none(),
609 "the rewrite must collapse the update chain into one section"
610 );
611 }
612
613 #[test]
618 fn rewrite_with_metadata_merges_fields_into_a_fresh_file() {
619 let base = Pdf {
620 pages: vec![Page::new(PageSize::A4)],
621 metadata: Some(Metadata {
622 title: Some("Old".to_string()),
623 author: Some("Keep".to_string()),
624 ..Metadata::default()
625 }),
626 ..Pdf::default()
627 }
628 .to_bytes()
629 .expect("base builds");
630 let doc = Document::load(base.clone()).expect("base loads");
631
632 let bytes = rewrite_with_metadata(
633 &doc,
634 Metadata {
635 title: Some("New".to_string()),
636 ..Metadata::default()
637 },
638 WriteOptions::default(),
639 )
640 .expect("rewrite succeeds");
641 assert!(
642 !bytes.starts_with(&base[..]),
643 "a metadata rewrite must not merely append an update onto the base"
644 );
645
646 let rewritten = Document::load(bytes).expect("rewritten document loads");
647 let meta = rewritten.metadata();
648 assert_eq!(meta.title.as_deref(), Some("New"));
649 assert_eq!(meta.author.as_deref(), Some("Keep"));
650
651 let new_root = rewritten
652 .xref()
653 .trailer
654 .get_ref("Root")
655 .expect("rewritten trailer names /Root");
656 let catalog = rewritten.get(new_root).expect("catalog resolves");
657 let metadata_ref = catalog
658 .as_dict()
659 .expect("catalog is a dictionary")
660 .get_ref("Metadata")
661 .expect("the base's XMP packet must still be named");
662 let stream = rewritten
663 .get(metadata_ref)
664 .expect("metadata stream resolves");
665 let text = String::from_utf8(
666 stream
667 .as_stream()
668 .expect("metadata is a stream")
669 .data
670 .clone(),
671 )
672 .expect("packet is utf-8");
673 assert!(text.contains("New"), "packet: {text}");
674 assert!(text.contains("Keep"), "packet: {text}");
675 assert!(!text.contains("Old"), "packet: {text}");
676 }
677
678 #[test]
685 fn rewrite_with_metadata_creates_info_when_absent() {
686 let base = Pdf {
687 pages: vec![Page::new(PageSize::A4)],
688 ..Pdf::default()
689 }
690 .to_bytes()
691 .expect("base builds");
692 let doc = Document::load(base).expect("base loads");
693
694 let bytes = rewrite_with_metadata(
695 &doc,
696 Metadata {
697 title: Some("Fresh".to_string()),
698 ..Metadata::default()
699 },
700 WriteOptions::default(),
701 )
702 .expect("rewrite succeeds");
703
704 let rewritten = Document::load(bytes).expect("rewritten document loads");
705 assert_eq!(rewritten.metadata().title.as_deref(), Some("Fresh"));
706
707 let new_root = rewritten
708 .xref()
709 .trailer
710 .get_ref("Root")
711 .expect("rewritten trailer names /Root");
712 let catalog = rewritten.get(new_root).expect("catalog resolves");
713 assert!(
714 catalog
715 .as_dict()
716 .expect("catalog is a dictionary")
717 .get("Metadata")
718 .is_none(),
719 "a base with no XMP packet must not gain one from a metadata rewrite"
720 );
721 }
722
723 #[test]
732 fn rewrite_with_metadata_translates_a_kept_indirect_info_value() {
733 let mut w = Writer::new(WriteOptions {
734 xref: XrefStyle::Table,
735 ..WriteOptions::default()
736 });
737 let pages_root = w.reserve();
738 let page = w.reserve();
739
740 let mut page_dict = Dict::new();
741 page_dict.insert(name("Type"), Object::Name(name("Page")));
742 page_dict.insert(name("Parent"), Object::Ref(pages_root));
743 page_dict.insert(name("Resources"), Object::Dict(Dict::new()));
744 page_dict.insert(
745 name("MediaBox"),
746 Object::Array(vec![
747 Object::Int(0),
748 Object::Int(0),
749 Object::Int(612),
750 Object::Int(792),
751 ]),
752 );
753 w.fill(page, Object::Dict(page_dict))
754 .expect("page slot fills");
755
756 let mut pages_dict = Dict::new();
757 pages_dict.insert(name("Type"), Object::Name(name("Pages")));
758 pages_dict.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
759 pages_dict.insert(name("Count"), Object::Int(1));
760 w.fill(pages_root, Object::Dict(pages_dict))
761 .expect("pages slot fills");
762
763 let title_ref = w.put(Object::String(b"Indirect Title".to_vec()));
764 let mut info = Dict::new();
765 info.insert(name("Title"), Object::Ref(title_ref));
766 let info_ref = w.put(Object::Dict(info));
767 w.set_info(info_ref);
768
769 let mut catalog = Dict::new();
770 catalog.insert(name("Type"), Object::Name(name("Catalog")));
771 catalog.insert(name("Pages"), Object::Ref(pages_root));
772 let root = w.put(Object::Dict(catalog));
773 let base = w.finish(root).expect("base finishes");
774 let doc = Document::load(base).expect("base loads");
775
776 let bytes = rewrite_with_metadata(
777 &doc,
778 Metadata {
779 author: Some("New Author".to_string()),
780 ..Metadata::default()
781 },
782 WriteOptions::default(),
783 )
784 .expect("rewrite succeeds");
785
786 let rewritten = Document::load(bytes).expect("rewritten document loads");
787 let meta = rewritten.metadata();
788 assert_eq!(
789 meta.title.as_deref(),
790 Some("Indirect Title"),
791 "a kept indirect /Info value must translate rather than alias"
792 );
793 assert_eq!(meta.author.as_deref(), Some("New Author"));
794
795 let page = rewritten.page(0).expect("page still resolves");
796 assert_eq!(
797 page.dict().get_name("Type"),
798 Some(&Name("Page".to_string())),
799 "the page object must not have been aliased by an untranslated /Info reference"
800 );
801 }
802
803 #[test]
811 fn rewrite_with_metadata_translates_a_reference_nested_in_an_info_value() {
812 let mut w = Writer::new(WriteOptions {
813 xref: XrefStyle::Table,
814 ..WriteOptions::default()
815 });
816 let pages_root = w.reserve();
817 let page = w.reserve();
818
819 let mut page_dict = Dict::new();
820 page_dict.insert(name("Type"), Object::Name(name("Page")));
821 page_dict.insert(name("Parent"), Object::Ref(pages_root));
822 page_dict.insert(name("Resources"), Object::Dict(Dict::new()));
823 page_dict.insert(
824 name("MediaBox"),
825 Object::Array(vec![
826 Object::Int(0),
827 Object::Int(0),
828 Object::Int(612),
829 Object::Int(792),
830 ]),
831 );
832 w.fill(page, Object::Dict(page_dict))
833 .expect("page slot fills");
834
835 let mut pages_dict = Dict::new();
836 pages_dict.insert(name("Type"), Object::Name(name("Pages")));
837 pages_dict.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
838 pages_dict.insert(name("Count"), Object::Int(1));
839 w.fill(pages_root, Object::Dict(pages_dict))
840 .expect("pages slot fills");
841
842 let witness = w.put(Object::String(b"Witness".to_vec()));
843 let mut info = Dict::new();
844 info.insert(name("Title"), Object::String(b"Plain Title".to_vec()));
845 info.insert(
846 name("CustomRefs"),
847 Object::Array(vec![Object::Ref(witness)]),
848 );
849 let info_ref = w.put(Object::Dict(info));
850 w.set_info(info_ref);
851
852 let mut catalog = Dict::new();
853 catalog.insert(name("Type"), Object::Name(name("Catalog")));
854 catalog.insert(name("Pages"), Object::Ref(pages_root));
855 let root = w.put(Object::Dict(catalog));
856 let base = w.finish(root).expect("base finishes");
857 let doc = Document::load(base).expect("base loads");
858
859 let bytes = rewrite_with_metadata(
860 &doc,
861 Metadata {
862 author: Some("New Author".to_string()),
863 ..Metadata::default()
864 },
865 WriteOptions::default(),
866 )
867 .expect("rewrite succeeds");
868
869 let rewritten = Document::load(bytes).expect("rewritten document loads");
870 let new_info_ref = rewritten
871 .xref()
872 .trailer
873 .get_ref("Info")
874 .expect("rewritten trailer names /Info");
875 let info_dict = rewritten.get(new_info_ref).expect("info resolves");
876 let custom = info_dict
877 .as_dict()
878 .expect("info is a dictionary")
879 .get("CustomRefs")
880 .expect("CustomRefs survives the merge, untouched by the recognized fields");
881 let Object::Array(items) = custom else {
882 panic!("CustomRefs must still be an array, got {custom:?}");
883 };
884 let Object::Ref(witness_target) = items[0] else {
885 panic!(
886 "CustomRefs[0] must still be a reference, got {:?}",
887 items[0]
888 );
889 };
890 let resolved = rewritten
891 .get(witness_target)
892 .expect("the translated reference must resolve to a real object");
893 assert_eq!(
894 resolved.as_str_bytes(),
895 Some(&b"Witness"[..]),
896 "a reference nested inside an /Info value must translate into the \
897 target's own numbering, not alias whatever the rewrite happens to \
898 number the same"
899 );
900 }
901}