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