Skip to main content

rightkit_pdf/
merge.rs

1//! Page-level assembly: merge several PDFs into one and select/reorder pages.
2//!
3//! Donor: Legion's `merge_page_pdfs` (lopdf renumber-and-splice recipe). Hardened: attributes a page
4//! inherits from its `/Pages` ancestors (`Resources`, `MediaBox`, `CropBox`, `Rotate`) are
5//! materialized onto the page before the old page tree is discarded, and unreachable objects
6//! (old catalogs and page trees) are pruned.
7
8use std::collections::BTreeMap;
9
10use lopdf::{Dictionary, Document, Object, ObjectId};
11
12#[derive(Debug, thiserror::Error)]
13pub enum MergeError {
14    #[error("no input documents")]
15    Empty,
16    #[error("input {index} could not be parsed: {message}")]
17    Parse { index: usize, message: String },
18    #[error("input {0} is encrypted")]
19    Encrypted(usize),
20    #[error("page {0} is out of range")]
21    PageOutOfRange(u32),
22    #[error("serialize failed: {0}")]
23    Write(String),
24}
25
26const INHERITED: [&[u8]; 4] = [b"Resources", b"MediaBox", b"CropBox", b"Rotate"];
27
28fn load(index: usize, bytes: &[u8]) -> Result<Document, MergeError> {
29    let doc = Document::load_mem(bytes).map_err(|e| MergeError::Parse {
30        index,
31        message: e.to_string(),
32    })?;
33    if doc.is_encrypted() {
34        return Err(MergeError::Encrypted(index));
35    }
36    Ok(doc)
37}
38
39fn materialize_inherited(doc: &mut Document, page_id: ObjectId) {
40    let mut found: Vec<(&[u8], Object)> = Vec::new();
41    let own = match doc.get_object(page_id).and_then(|o| o.as_dict()) {
42        Ok(d) => d.clone(),
43        Err(_) => return,
44    };
45    for key in INHERITED {
46        if own.has(key) {
47            continue;
48        }
49        let mut cursor = own.get(b"Parent").ok().and_then(|p| p.as_reference().ok());
50        let mut hops = 0;
51        while let Some(id) = cursor {
52            hops += 1;
53            if hops > 64 {
54                break;
55            }
56            let Ok(node) = doc.get_object(id).and_then(|o| o.as_dict()) else {
57                break;
58            };
59            if let Ok(value) = node.get(key) {
60                found.push((key, value.clone()));
61                break;
62            }
63            cursor = node.get(b"Parent").ok().and_then(|p| p.as_reference().ok());
64        }
65    }
66    if let Ok(Object::Dictionary(dict)) = doc.get_object_mut(page_id) {
67        for (key, value) in found {
68            dict.set(key, value);
69        }
70    }
71}
72
73/// Merges `inputs` in order into one document (one page tree, pages in input order).
74pub fn merge_pdfs(inputs: &[&[u8]]) -> Result<Vec<u8>, MergeError> {
75    if inputs.is_empty() {
76        return Err(MergeError::Empty);
77    }
78    let mut max_id: u32 = 1;
79    let mut page_ids: Vec<ObjectId> = Vec::new();
80    let mut objects: BTreeMap<ObjectId, Object> = BTreeMap::new();
81    for (index, bytes) in inputs.iter().enumerate() {
82        let mut doc = load(index, bytes)?;
83        let pages: Vec<ObjectId> = doc.get_pages().into_values().collect();
84        for id in &pages {
85            materialize_inherited(&mut doc, *id);
86        }
87        // Renumber moves page ids too; recompute them from the renumbered document.
88        doc.renumber_objects_with(max_id);
89        max_id = doc.max_id + 1;
90        page_ids.extend(doc.get_pages().into_values());
91        objects.extend(doc.objects);
92    }
93    assemble(objects, page_ids)
94}
95
96/// Builds a new document containing `pages` (1-based, any order, repeats allowed) of `source`.
97pub fn select_pages(source: &[u8], pages: &[u32]) -> Result<Vec<u8>, MergeError> {
98    if pages.is_empty() {
99        return Err(MergeError::Empty);
100    }
101    let mut doc = load(0, source)?;
102    let map = doc.get_pages();
103    for id in map.values().copied().collect::<Vec<_>>() {
104        materialize_inherited(&mut doc, id);
105    }
106    let mut picked = Vec::with_capacity(pages.len());
107    let mut objects = doc.objects.clone();
108    let mut next = doc.max_id;
109    for &p in pages {
110        let id = *map.get(&p).ok_or(MergeError::PageOutOfRange(p))?;
111        if picked.contains(&id) {
112            // Repeated page: clone the page dictionary under a fresh id (shares content streams).
113            next += 1;
114            let new_id = (next, 0);
115            let copy = objects
116                .get(&id)
117                .cloned()
118                .ok_or(MergeError::PageOutOfRange(p))?;
119            objects.insert(new_id, copy);
120            picked.push(new_id);
121        } else {
122            picked.push(id);
123        }
124    }
125    assemble(objects.into_iter().collect(), picked)
126}
127
128fn assemble(
129    objects: BTreeMap<ObjectId, Object>,
130    page_ids: Vec<ObjectId>,
131) -> Result<Vec<u8>, MergeError> {
132    let mut merged = Document::with_version("1.7");
133    merged.objects = objects;
134    merged.max_id = merged.objects.keys().map(|id| id.0).max().unwrap_or(1);
135    let pages_id = merged.new_object_id();
136    let kids: Vec<Object> = page_ids.iter().map(|id| Object::Reference(*id)).collect();
137    let mut pages_dict = Dictionary::new();
138    pages_dict.set("Type", Object::Name(b"Pages".to_vec()));
139    pages_dict.set("Count", Object::Integer(kids.len() as i64));
140    pages_dict.set("Kids", Object::Array(kids));
141    merged
142        .objects
143        .insert(pages_id, Object::Dictionary(pages_dict));
144    for id in &page_ids {
145        if let Ok(Object::Dictionary(dict)) = merged.get_object_mut(*id) {
146            dict.set("Parent", Object::Reference(pages_id));
147        }
148    }
149    let mut catalog = Dictionary::new();
150    catalog.set("Type", Object::Name(b"Catalog".to_vec()));
151    catalog.set("Pages", Object::Reference(pages_id));
152    let catalog_id = merged.add_object(Object::Dictionary(catalog));
153    merged.trailer.set("Root", catalog_id);
154    merged.prune_objects();
155    merged.renumber_objects();
156    merged.compress();
157    let mut out = Vec::new();
158    merged
159        .save_to(&mut out)
160        .map_err(|e| MergeError::Write(e.to_string()))?;
161    Ok(out)
162}