1mod copy;
71mod inherit;
72mod nup;
73mod range;
74mod viewer;
75
76use pdfrum_common::PageIndex;
77use pdfrum_object::{Array, Dict, Name, ObjRef, Object, Resolve, names};
78use pdfrum_parser::Document;
79
80use crate::doc::EditDoc;
81use crate::error::Error;
82use crate::names as edit_names;
83
84use copy::ObjectMap;
85use inherit::inheritable;
86use nup::{NupGrid, sub_page_fragment};
87
88pub use range::PageRange;
89
90const LETTER: [i64; 4] = [0, 0, 612, 792];
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
98#[non_exhaustive]
99pub struct ImportOptions {
100 pub at: PageIndex,
103 pub viewer_preferences: bool,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq)]
114#[non_exhaustive]
115pub struct NUpOptions {
116 pub sheet: (f32, f32),
118 pub grid: (u32, u32),
120}
121
122impl Default for NUpOptions {
123 fn default() -> Self {
124 Self {
125 sheet: (612.0, 792.0),
126 grid: (2, 1),
127 }
128 }
129}
130
131impl ImportOptions {
132 #[must_use]
142 pub fn builder() -> ImportOptionsBuilder {
143 ImportOptionsBuilder(ImportOptions::default())
144 }
145}
146
147#[derive(Debug, Clone, Copy)]
149pub struct ImportOptionsBuilder(ImportOptions);
150
151impl ImportOptionsBuilder {
152 #[must_use]
154 pub fn at(mut self, at: impl Into<PageIndex>) -> Self {
155 self.0.at = at.into();
156 self
157 }
158
159 #[must_use]
161 pub fn viewer_preferences(mut self, copy: bool) -> Self {
162 self.0.viewer_preferences = copy;
163 self
164 }
165
166 #[must_use]
168 pub fn build(self) -> ImportOptions {
169 self.0
170 }
171}
172
173impl NUpOptions {
174 #[must_use]
189 pub fn builder() -> NUpOptionsBuilder {
190 NUpOptionsBuilder(NUpOptions::default())
191 }
192}
193
194#[derive(Debug, Clone, Copy)]
200pub struct NUpOptionsBuilder(NUpOptions);
201
202impl NUpOptionsBuilder {
203 #[must_use]
205 #[expect(
206 clippy::cast_possible_truncation,
207 reason = "PDF reals are f32; sheet sizes fit"
208 )]
209 pub fn sheet(mut self, sheet: kurbo::Size) -> Self {
210 self.0.sheet = (sheet.width as f32, sheet.height as f32);
211 self
212 }
213
214 #[must_use]
216 pub fn grid(mut self, columns: u32, rows: u32) -> Self {
217 self.0.grid = (columns, rows);
218 self
219 }
220
221 #[must_use]
223 pub fn build(self) -> NUpOptions {
224 self.0
225 }
226}
227
228pub(crate) fn init_dest(dest: &mut EditDoc<'_>) -> Result<u32, Error> {
244 let catalog_ref = dest
245 .base()
246 .trailer()
247 .reference(names::ROOT)
248 .ok_or(Error::NoDestinationCatalog)?;
249 let catalog = dest
250 .fetch(catalog_ref)
251 .ok()
252 .and_then(|o| o.as_dict().cloned())
253 .ok_or(Error::NoDestinationCatalog)?;
254
255 let mut catalog = catalog;
256
257 if catalog
260 .name(names::TYPE)
261 .is_none_or(|n| n.as_bytes().is_empty())
262 {
263 set(
264 &mut catalog,
265 names::TYPE,
266 Object::Name(edit_names::CATALOG.clone()),
267 );
268 }
269
270 let pages_ref = match catalog.raw(names::PAGES) {
273 Some(Object::Ref(r)) if dest.fetch(*r).is_ok_and(|o| o.as_dict().is_some()) => *r,
274 _ => {
275 let fresh = fresh_pages_node(dest);
276 set(&mut catalog, names::PAGES, Object::Ref(fresh));
277 fresh
278 }
279 };
280
281 let mut pages = dest
282 .fetch(pages_ref)
283 .ok()
284 .and_then(|o| o.as_dict().cloned())
285 .unwrap_or_default();
286 if pages
287 .name(names::TYPE)
288 .is_none_or(|n| n.as_bytes().is_empty())
289 {
290 set(&mut pages, names::TYPE, Object::Name(names::PAGES.clone()));
291 }
292 if !matches!(pages.raw(names::KIDS), Some(Object::Array(_))) {
295 set(&mut pages, names::KIDS, Object::Array(Array::new()));
296 set(&mut pages, names::COUNT, Object::Int(0));
297 }
298
299 dest.replace(pages_ref, Object::Dict(pages));
300 dest.replace(catalog_ref, Object::Dict(catalog));
301 Ok(pages_ref.num)
302}
303
304fn fresh_pages_node(dest: &mut EditDoc<'_>) -> ObjRef {
306 dest.add(Object::Dict(Dict::from_pairs([
307 (names::TYPE.clone(), Object::Name(names::PAGES.clone())),
308 (names::COUNT.clone(), Object::Int(0)),
309 (names::KIDS.clone(), Object::Array(Array::new())),
310 ])))
311}
312
313pub fn import_pages(
328 dest: &mut EditDoc<'_>,
329 src: &Document,
330 pages: &PageRange,
331 opts: &ImportOptions,
332) -> Result<(), Error> {
333 for index in pages.indices() {
336 if index.get() >= src.page_count() {
337 return Err(Error::PageIndexOutOfRange(*index));
338 }
339 }
340
341 let pages_node = init_dest(dest)?;
342 let mut map = ObjectMap::new();
343 let mut created = Vec::new();
344
345 for index in pages.indices() {
346 let page = src
347 .page(*index)
348 .map_err(|_| Error::PageIndexOutOfRange(*index))?;
349 let dest_page = dest.add(Object::Null);
350
351 if let Some(source_ref) = page.reference {
355 map.record(source_ref.num, dest_page.num);
356 }
357
358 let built = build_page(dest, src, &page.dict, pages_node, &mut map, dest_page);
359 dest.replace(dest_page, Object::Dict(built));
360 created.push(dest_page);
361 }
362
363 insert_into_tree(dest, pages_node, &created, opts.at.get());
364
365 if opts.viewer_preferences
366 && let Ok(catalog) = src.catalog()
367 && let Some(prefs) = viewer::filtered(&catalog, src)
368 {
369 copy_viewer_preferences(dest, prefs);
370 }
371 Ok(())
372}
373
374fn build_page(
376 dest: &mut EditDoc<'_>,
377 src: &Document,
378 source: &Dict,
379 pages_node: u32,
380 map: &mut ObjectMap,
381 self_ref: ObjRef,
382) -> Dict {
383 let mut out = Dict::from_pairs([
384 (names::TYPE.clone(), Object::Name(names::PAGE.clone())),
385 (
386 names::PARENT.clone(),
387 Object::Ref(ObjRef::new(pages_node, 0)),
388 ),
389 ]);
390
391 for (key, value) in source.iter() {
394 if key == names::TYPE || key == names::PARENT {
395 continue;
396 }
397 let mut value = value.clone();
398 if rewrite_value(dest, src, &mut value, map, pages_node) {
399 out.push(key.clone(), value);
400 }
401 }
402
403 flatten_inherited(dest, src, source, &mut out, map, pages_node);
404 let _ = self_ref;
405 out
406}
407
408fn flatten_inherited(
410 dest: &mut EditDoc<'_>,
411 src: &Document,
412 source: &Dict,
413 out: &mut Dict,
414 map: &mut ObjectMap,
415 pages_node: u32,
416) {
417 if !copy_inherited(dest, src, source, out, names::MEDIA_BOX, map, pages_node)
420 && !copy_inherited(dest, src, source, out, names::CROP_BOX, map, pages_node)
421 {
422 set(
424 out,
425 names::MEDIA_BOX,
426 Object::Array(Array::of(LETTER.map(Object::Int))),
427 );
428 } else if !out.contains_key(names::MEDIA_BOX) {
429 if let Some(crop) = out.raw(names::CROP_BOX).cloned() {
431 set(out, names::MEDIA_BOX, crop);
432 }
433 }
434
435 if !copy_inherited(dest, src, source, out, names::RESOURCES, map, pages_node) {
437 set(out, names::RESOURCES, Object::Dict(Dict::new()));
438 }
439
440 copy_inherited(dest, src, source, out, names::CROP_BOX, map, pages_node);
444 copy_inherited(dest, src, source, out, names::ROTATE, map, pages_node);
445}
446
447fn copy_inherited(
450 dest: &mut EditDoc<'_>,
451 src: &Document,
452 source: &Dict,
453 out: &mut Dict,
454 key: &Name,
455 map: &mut ObjectMap,
456 pages_node: u32,
457) -> bool {
458 if out.contains_key(key) {
459 return true;
460 }
461 let Some(mut value) = inheritable(source, key, src) else {
462 return false;
463 };
464 if !rewrite_value(dest, src, &mut value, map, pages_node) {
465 return false;
466 }
467 out.push(key.clone(), value);
468 true
469}
470
471fn rewrite_value(
473 dest: &mut EditDoc<'_>,
474 src: &Document,
475 value: &mut Object,
476 map: &mut ObjectMap,
477 pages_node: u32,
478) -> bool {
479 copy::rewrite_in_place(dest, src, value, map, pages_node)
480}
481
482pub(crate) fn insert_into_tree(
484 dest: &mut EditDoc<'_>,
485 pages_node: u32,
486 created: &[ObjRef],
487 at: u32,
488) {
489 let node_ref = ObjRef::new(pages_node, 0);
490 let mut node = dest
491 .fetch(node_ref)
492 .ok()
493 .and_then(|o| o.as_dict().cloned())
494 .unwrap_or_default();
495
496 let existing: Vec<Object> = node
497 .raw(names::KIDS)
498 .and_then(Object::as_array)
499 .map(|a| a.iter().cloned().collect())
500 .unwrap_or_default();
501
502 let split = (at as usize).min(existing.len());
505 let mut kids = Array::new();
506 for value in existing.get(..split).unwrap_or_default() {
507 kids.push(value.clone());
508 }
509 for page in created {
510 kids.push(Object::Ref(*page));
511 }
512 for value in existing.get(split..).unwrap_or_default() {
513 kids.push(value.clone());
514 }
515
516 let count = i64::try_from(kids.len()).unwrap_or(i64::MAX);
517 set(&mut node, names::KIDS, Object::Array(kids));
518 set(&mut node, names::COUNT, Object::Int(count));
519 dest.replace(node_ref, Object::Dict(node));
520}
521
522fn copy_viewer_preferences(dest: &mut EditDoc<'_>, prefs: Dict) {
525 let Some(catalog_ref) = dest.base().trailer().reference(names::ROOT) else {
526 return;
527 };
528 let Some(mut catalog) = dest
529 .fetch(catalog_ref)
530 .ok()
531 .and_then(|o| o.as_dict().cloned())
532 else {
533 return;
534 };
535 set(&mut catalog, names::VIEWER_PREFERENCES, Object::Dict(prefs));
537 dest.replace(catalog_ref, Object::Dict(catalog));
538}
539
540pub fn n_page_to_one(
557 dest: &mut EditDoc<'_>,
558 src: &Document,
559 pages: &PageRange,
560 opts: &NUpOptions,
561) -> Result<(), Error> {
562 let (x, y) = opts.grid;
563 let (width, height) = opts.sheet;
564 if x == 0 || y == 0 || width <= 0.0 || height <= 0.0 {
565 return Err(Error::BadNupParams);
566 }
567 for index in pages.indices() {
568 if index.get() >= src.page_count() {
569 return Err(Error::PageIndexOutOfRange(*index));
570 }
571 }
572
573 let pages_node = init_dest(dest)?;
574 let grid = NupGrid {
575 sheet_width: width,
576 sheet_height: height,
577 x,
578 y,
579 };
580
581 let mut map = ObjectMap::new();
582 let mut forms: Vec<(u32, ObjRef)> = Vec::new();
585 let mut created = Vec::new();
586
587 for chunk in pages.indices().chunks(grid.per_sheet().max(1) as usize) {
588 let mut content = String::new();
589 let mut xobjects = Dict::new();
590
591 for (slot, index) in chunk.iter().enumerate() {
592 let page = src
593 .page(*index)
594 .map_err(|_| Error::PageIndexOutOfRange(*index))?;
595 let key = page.reference.map_or(u32::MAX - index.get(), |r| r.num);
596
597 let form = if let Some((_, existing)) = forms.iter().find(|(k, _)| *k == key) {
598 *existing
599 } else {
600 let made = make_form(dest, src, &page.dict, pages_node, &mut map);
601 forms.push((key, made));
602 made
603 };
604
605 let (page_w, page_h) = page_size(&page.dict, src);
606 #[expect(
607 clippy::cast_possible_truncation,
608 reason = "a slot index is bounded by the grid, which is a u32"
609 )]
610 let edit = grid.edit(slot as u32, page_w, page_h);
611 let name = format!("X{}", xobjects.len() + 1);
612 content.push_str(&sub_page_fragment(&name, edit));
613 xobjects.push(Name::from(name.as_str()), Object::Ref(form));
616 }
617
618 created.push(make_sheet(
619 dest, pages_node, &content, xobjects, width, height,
620 ));
621 }
622
623 insert_into_tree(dest, pages_node, &created, 0);
624 Ok(())
625}
626
627fn make_form(
629 dest: &mut EditDoc<'_>,
630 src: &Document,
631 page: &Dict,
632 pages_node: u32,
633 map: &mut ObjectMap,
634) -> ObjRef {
635 let content = assemble_content(page, src);
639
640 let mut resources = Dict::new();
641 let mut carrier = Dict::new();
642 if copy_inherited(
643 dest,
644 src,
645 page,
646 &mut carrier,
647 names::RESOURCES,
648 map,
649 pages_node,
650 ) && let Some(found) = carrier.raw(names::RESOURCES).and_then(Object::as_dict)
651 {
652 resources = found.clone();
653 }
654
655 let (media, crop) = boxes(page, src);
656 let bbox = intersect(media, crop);
657
658 let dict = Dict::from_pairs([
662 (names::RESOURCES.clone(), Object::Dict(resources)),
663 (
664 edit_names::BBOX.clone(),
665 Object::Array(Array::of(bbox.map(Object::Real))),
666 ),
667 (
668 edit_names::MATRIX.clone(),
669 Object::Array(Array::of(
670 [1.0, 0.0, 0.0, 1.0, -bbox[0], -bbox[1]].map(Object::Real),
671 )),
672 ),
673 (
674 names::TYPE.clone(),
675 Object::Name(edit_names::XOBJECT.clone()),
676 ),
677 (
678 names::SUBTYPE.clone(),
679 Object::Name(edit_names::FORM.clone()),
680 ),
681 (edit_names::FORM_TYPE.clone(), Object::Int(1)),
682 (
683 names::LENGTH.clone(),
684 Object::Int(i64::try_from(content.len()).unwrap_or(0)),
685 ),
686 ]);
687
688 dest.add(Object::Stream(Box::new(pdfrum_object::Stream::new(
689 dict,
690 pdfrum_object::ByteSpan::from(content),
691 ))))
692}
693
694fn make_sheet(
696 dest: &mut EditDoc<'_>,
697 pages_node: u32,
698 content: &str,
699 xobjects: Dict,
700 width: f32,
701 height: f32,
702) -> ObjRef {
703 let stream = dest.add(Object::Stream(Box::new(pdfrum_object::Stream::new(
704 Dict::from_pairs([(
705 names::LENGTH.clone(),
706 Object::Int(i64::try_from(content.len()).unwrap_or(0)),
707 )]),
708 pdfrum_object::ByteSpan::from(content.as_bytes().to_vec()),
709 ))));
710
711 dest.add(Object::Dict(Dict::from_pairs([
712 (names::TYPE.clone(), Object::Name(names::PAGE.clone())),
713 (
714 names::PARENT.clone(),
715 Object::Ref(ObjRef::new(pages_node, 0)),
716 ),
717 (
718 names::MEDIA_BOX.clone(),
719 Object::Array(Array::of([
720 Object::Int(0),
721 Object::Int(0),
722 Object::Real(width),
723 Object::Real(height),
724 ])),
725 ),
726 (
727 names::RESOURCES.clone(),
728 Object::Dict(Dict::from_pairs([(
729 edit_names::XOBJECT.clone(),
730 Object::Dict(xobjects),
731 )])),
732 ),
733 (names::CONTENTS.clone(), Object::Ref(stream)),
734 ])))
735}
736
737fn assemble_content(page: &Dict, src: &Document) -> Vec<u8> {
741 let limits = pdfrum_common::Limits::default();
742 let mut diags = pdfrum_common::Diagnostics::default();
743 let mut out = Vec::new();
744
745 match page.get(names::CONTENTS, src).as_deref() {
746 Some(Object::Stream(s)) => {
747 out = pdfrum_parser::decoded_stream(s, src, &limits, &mut diags);
748 }
749 Some(Object::Array(a)) => {
750 for i in 0..a.len() {
751 let Some(s) = a.stream_at(i, src) else {
752 continue;
753 };
754 out.extend_from_slice(&pdfrum_parser::decoded_stream(&s, src, &limits, &mut diags));
755 out.push(b'\n');
756 }
757 }
758 _ => {}
760 }
761 out
762}
763
764fn boxes(page: &Dict, src: &Document) -> ([f32; 4], Option<[f32; 4]>) {
766 let read = |key: &Name| -> Option<[f32; 4]> {
767 let a = match inheritable(page, key, src) {
768 Some(Object::Array(a)) => a,
769 _ => page.array(key, src)?,
770 };
771 if a.len() < 4 {
772 return None;
773 }
774 Some(normalize([
775 a.number_at_or_zero(0),
776 a.number_at_or_zero(1),
777 a.number_at_or_zero(2),
778 a.number_at_or_zero(3),
779 ]))
780 };
781 let media = read(names::MEDIA_BOX).unwrap_or([0.0, 0.0, 612.0, 792.0]);
782 (media, read(names::CROP_BOX))
783}
784
785fn normalize(b: [f32; 4]) -> [f32; 4] {
787 [
788 b[0].min(b[2]),
789 b[1].min(b[3]),
790 b[0].max(b[2]),
791 b[1].max(b[3]),
792 ]
793}
794
795fn intersect(media: [f32; 4], crop: Option<[f32; 4]>) -> [f32; 4] {
797 let Some(crop) = crop else {
798 return media;
799 };
800 [
801 media[0].max(crop[0]),
802 media[1].max(crop[1]),
803 media[2].min(crop[2]),
804 media[3].min(crop[3]),
805 ]
806}
807
808fn page_size(page: &Dict, src: &Document) -> (f32, f32) {
810 let (media, crop) = boxes(page, src);
811 let b = intersect(media, crop);
812 let (w, h) = (b[2] - b[0], b[3] - b[1]);
813 let quarter = matches!(
815 inheritable(page, names::ROTATE, src)
816 .or_else(|| page.raw(names::ROTATE).cloned())
817 .and_then(|o| o.as_int())
818 .map(|v| ((v / 90) % 4 + 4) % 4),
819 Some(1 | 3)
820 );
821 if quarter { (h, w) } else { (w, h) }
822}
823
824fn set(dict: &mut Dict, key: &Name, value: Object) {
826 if dict.contains_key(key) {
827 *dict = Dict::from_pairs(dict.iter().map(|(k, v)| {
828 if k == key {
829 (k.clone(), value.clone())
830 } else {
831 (k.clone(), v.clone())
832 }
833 }));
834 } else {
835 dict.push(key.clone(), value);
836 }
837}