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