Skip to main content

pdfrum_edit/
doc.rs

1//! The editable view of a document: a read-only base plus an overlay of
2//! changes.
3//!
4//! # Why the base stays immutable
5//!
6//! [`Document`] is a `Sync`, lazily-caching reader over `Arc<[u8]>`, and every
7//! crate above it borrows from that shape. Making it mutable so the writer
8//! could edit in place would cost the `OnceLock` store, `Sync`, and every
9//! downstream borrow — to serve exactly one caller.
10//!
11//! So edits live here instead. [`EditDoc`] holds `&Document` plus a map of
12//! added and replaced objects and a set of removed ones, and implements
13//! [`Resolve`] by asking the overlay first and the base second, so an editor
14//! reads a flattened view of the document without adding a new seam.
15//!
16//! The C++'s writer has a memory dance this shape removes entirely: it fetches
17//! an old object, writes it, and then *deletes it from the document again* so
18//! that saving does not permanently grow the in-memory object map. Our overlay
19//! never materializes an object it did not need, so there is nothing to undo —
20//! and "save twice, get the same bytes" falls out rather than being arranged.
21
22use std::collections::{BTreeMap, BTreeSet};
23use std::sync::Arc;
24
25use pdfrum_common::PageIndex;
26use pdfrum_object::{Dict, ObjRef, Object, Resolve, names};
27use pdfrum_page::PageEdit;
28use pdfrum_parser::Document;
29
30/// A document plus the edits made to it.
31///
32/// Cheap to create and to drop: it borrows the base and owns only what
33/// changed. Cloning copies the overlay's map and shares its objects, so a
34/// save that must not disturb the session works on a clone.
35#[derive(Debug, Clone)]
36pub struct EditDoc<'a> {
37    base: &'a Document,
38    /// Objects added or replaced, by number. Sorted, because the writer walks
39    /// new objects in ascending order and the subsetter binary-searches them.
40    overlay: BTreeMap<u32, Arc<Object>>,
41    /// Objects removed. A removed object resolves as null and is not written.
42    removed: BTreeSet<u32>,
43    /// The next number [`EditDoc::add`] will hand out.
44    next_num: u32,
45    /// An `/Info` for the saved trailer to name, when the edits gave the
46    /// document one it did not have. The trailer is the writer's, built from
47    /// the base's, so this is the one key an edit overrides there.
48    info: Option<ObjRef>,
49}
50
51impl<'a> EditDoc<'a> {
52    /// An unedited view of `base`.
53    #[must_use]
54    pub fn new(base: &'a Document) -> Self {
55        Self {
56            base,
57            overlay: BTreeMap::new(),
58            removed: BTreeSet::new(),
59            // One past the highest number the file used, so a fresh object
60            // can never collide with one the xref already names.
61            next_num: base.xref().last_object_number().saturating_add(1),
62            info: None,
63        }
64    }
65
66    /// The document underneath the edits.
67    #[must_use]
68    pub fn base(&self) -> &'a Document {
69        self.base
70    }
71
72    /// The trailer the save will build from: the base's, with `/Info`
73    /// pointing at the object [`EditDoc::set_info`] named.
74    pub(crate) fn trailer(&self) -> Dict {
75        let mut trailer = self.base.trailer().clone();
76        if let Some(info) = self.info {
77            trailer.insert(names::INFO.clone(), Object::Ref(info));
78        }
79        trailer
80    }
81
82    /// Name `r` as the document's `/Info` in the saved trailer.
83    pub(crate) fn set_info(&mut self, r: ObjRef) {
84        self.info = Some(r);
85    }
86
87    /// Add `obj` as a new indirect object, returning the reference that names
88    /// it. Generation is always 0: the writer emits nothing else.
89    pub fn add(&mut self, obj: Object) -> ObjRef {
90        let num = self.next_num;
91        self.next_num = self.next_num.saturating_add(1);
92        self.overlay.insert(num, Arc::new(obj));
93        self.removed.remove(&num);
94        ObjRef::new(num, 0)
95    }
96
97    /// Replace what `r` names.
98    ///
99    /// The base is untouched: the overlay simply answers first from now on.
100    pub fn replace(&mut self, r: ObjRef, obj: Object) {
101        self.overlay.insert(r.num, Arc::new(obj));
102        self.removed.remove(&r.num);
103        self.next_num = self.next_num.max(r.num.saturating_add(1));
104    }
105
106    /// Remove what `r` names. It then resolves as null and is not written.
107    pub fn remove(&mut self, r: ObjRef) {
108        self.overlay.remove(&r.num);
109        self.removed.insert(r.num);
110    }
111
112    /// Whether `num` was removed.
113    #[must_use]
114    pub fn is_removed(&self, num: u32) -> bool {
115        self.removed.contains(&num)
116    }
117
118    /// The overlay's objects in ascending number order — everything this
119    /// editing session added or replaced.
120    pub fn edited(&self) -> impl Iterator<Item = (u32, &Arc<Object>)> {
121        self.overlay.iter().map(|(n, o)| (*n, o))
122    }
123
124    /// Whether `num` has an overlay entry.
125    #[must_use]
126    pub fn is_edited(&self, num: u32) -> bool {
127        self.overlay.contains_key(&num)
128    }
129
130    /// The highest object number in play, across the base and the overlay.
131    #[must_use]
132    pub fn last_object_number(&self) -> u32 {
133        let base = self.base.xref().last_object_number();
134        self.overlay
135            .keys()
136            .next_back()
137            .copied()
138            .unwrap_or(0)
139            .max(base)
140    }
141
142    /// A page's dictionary as the session's edits leave it, with the
143    /// reference the writer replaces and the resources the page reaches.
144    ///
145    /// Read through the overlay, not the base, so an earlier edit of the same
146    /// page — a rotation, a stamp — is what a later content rewrite builds on.
147    /// `None` for a page written inline in its parent's `/Kids`, which has no
148    /// object to replace.
149    ///
150    /// # Errors
151    ///
152    /// When `index` is outside the document.
153    pub fn page_state(
154        &self,
155        index: PageIndex,
156    ) -> Result<Option<(ObjRef, Dict, Dict)>, pdfrum_parser::Error> {
157        let page = self.base().page(index)?;
158        let Some(reference) = page.reference else {
159            return Ok(None);
160        };
161        let dict = self
162            .fetch(reference)
163            .ok()
164            .as_deref()
165            .and_then(Object::as_dict)
166            .cloned()
167            .unwrap_or_else(|| page.dict.clone());
168        let resources = dict
169            .dict(crate::names::RESOURCES, &self)
170            .or_else(|| {
171                page.inherited(crate::names::RESOURCES, &self)?
172                    .resolve(&self)
173                    .ok()?
174                    .as_dict()
175                    .cloned()
176            })
177            .unwrap_or_default();
178        Ok(Some((reference, dict, resources)))
179    }
180
181    /// Turn one page edit into replacement objects on the session.
182    ///
183    /// `shared` is [`shared_objects`](crate::shared_objects) over the session, computed
184    /// once by the caller for however many pages it applies.
185    ///
186    /// # Errors
187    ///
188    /// When the page's index is outside the document.
189    pub fn apply_page(
190        &mut self,
191        page: &PageEdit,
192        shared: &crate::ShareCounts,
193    ) -> Result<(), pdfrum_parser::Error> {
194        let Some((reference, dict, resources)) = self.page_state(page.index())? else {
195            // Rather than half-apply the change, leave the page as it was.
196            return Ok(());
197        };
198        let Some(rewrite) = crate::regenerate(page.graph(), &resources, self) else {
199            return Ok(());
200        };
201        crate::apply_rewrite(self, reference, &dict, &rewrite, shared);
202        Ok(())
203    }
204}
205
206impl Resolve for EditDoc<'_> {
207    fn fetch(&self, r: ObjRef) -> Result<Arc<Object>, pdfrum_object::Error> {
208        if self.removed.contains(&r.num) {
209            return Ok(Arc::new(Object::Null));
210        }
211        if let Some(obj) = self.overlay.get(&r.num) {
212            return Ok(Arc::clone(obj));
213        }
214        self.base.fetch(r)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::EditDoc;
221    use pdfrum_object::{ObjRef, Object, Resolve};
222    use pdfrum_parser::{Document, LoadOptions, load};
223    use std::sync::Arc;
224
225    fn doc() -> Document {
226        let file = b"%PDF-1.7\n\
2271 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n\
2282 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n\
2293 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n\
230trailer\n<< /Root 1 0 R /Size 4 >>\n";
231        load(Arc::from(&file[..]), &LoadOptions::default()).expect("opens")
232    }
233
234    #[test]
235    fn an_unedited_view_reads_straight_through() {
236        let base = doc();
237        let edit = EditDoc::new(&base);
238        let catalog = edit.fetch(ObjRef::new(1, 0)).expect("catalog");
239        assert!(catalog.as_dict().is_some());
240        assert!(edit.edited().next().is_none());
241    }
242
243    // New numbers start past the file's highest, so nothing collides.
244    #[test]
245    fn added_objects_take_fresh_numbers() {
246        let base = doc();
247        let mut edit = EditDoc::new(&base);
248        let first = edit.add(Object::Int(1));
249        let second = edit.add(Object::Int(2));
250        assert!(first.num > base.xref().last_object_number());
251        assert_eq!(second.num, first.num + 1);
252        assert_eq!(first.generation, 0);
253        assert_eq!(*edit.fetch(first).expect("added"), Object::Int(1));
254    }
255
256    #[test]
257    fn the_overlay_answers_before_the_base() {
258        let base = doc();
259        let mut edit = EditDoc::new(&base);
260        let page = ObjRef::new(3, 0);
261        assert!(edit.fetch(page).expect("page").as_dict().is_some());
262        edit.replace(page, Object::Int(99));
263        assert_eq!(*edit.fetch(page).expect("replaced"), Object::Int(99));
264        // The base itself never changed.
265        assert!(base.fetch(page).expect("page").as_dict().is_some());
266    }
267
268    // A removed object resolves as null rather than as an error, which is how
269    // a dangling reference already reads to everything downstream.
270    #[test]
271    fn a_removed_object_reads_as_null() {
272        let base = doc();
273        let mut edit = EditDoc::new(&base);
274        let page = ObjRef::new(3, 0);
275        edit.remove(page);
276        assert!(edit.fetch(page).expect("null").is_null());
277        assert!(edit.is_removed(3));
278        assert!(!edit.is_edited(3));
279    }
280
281    #[test]
282    fn replacing_a_removed_object_brings_it_back() {
283        let base = doc();
284        let mut edit = EditDoc::new(&base);
285        let page = ObjRef::new(3, 0);
286        edit.remove(page);
287        edit.replace(page, Object::Int(7));
288        assert!(!edit.is_removed(3));
289        assert_eq!(*edit.fetch(page).expect("back"), Object::Int(7));
290    }
291
292    #[test]
293    fn edits_come_back_in_ascending_number_order() {
294        let base = doc();
295        let mut edit = EditDoc::new(&base);
296        edit.replace(ObjRef::new(9, 0), Object::Int(9));
297        edit.replace(ObjRef::new(2, 0), Object::Int(2));
298        edit.replace(ObjRef::new(5, 0), Object::Int(5));
299        let nums: Vec<u32> = edit.edited().map(|(n, _)| n).collect();
300        assert_eq!(nums, vec![2, 5, 9]);
301    }
302
303    // Replacing past the end moves the allocator, so a later add cannot
304    // land on a number an edit already claimed.
305    #[test]
306    fn replacing_past_the_end_moves_the_allocator() {
307        let base = doc();
308        let mut edit = EditDoc::new(&base);
309        edit.replace(ObjRef::new(100, 0), Object::Int(1));
310        assert_eq!(edit.add(Object::Int(2)).num, 101);
311        assert_eq!(edit.last_object_number(), 101);
312    }
313}