Skip to main content

pdfrum_page/
page_edit.rs

1//! One page's object graph, opened for editing.
2//!
3//! An owned copy of what a page draws, plus a record of what changed. A
4//! writer turns that record into replacement objects; the document the graph
5//! came from is never mutated.
6
7use crate::page::PageObject;
8use crate::{IndexOutOfRange, Page};
9use kurbo::Affine;
10use pdfrum_common::PageIndex;
11
12/// One page's object graph, opened for editing.
13///
14/// Obtained from the facade's `Page::edit`. Holds the objects the page draws, in painting
15/// order, plus a record of what has changed — which is what tells the save
16/// which content streams to write again and which to leave alone.
17///
18/// # Nothing is mutated until you save
19///
20/// `Page::edit` hands back an owned graph, you change *that*, and
21/// `Document::save_pages` turns the changes
22/// into replacement objects on the way out. The document is never touched,
23/// which is why editing a page needs no `&mut Document` and why two threads
24/// can edit two pages at once.
25///
26/// # Saving an edited page rewrites it, and rewriting loses things
27///
28/// A page whose objects you changed is written again **from the object
29/// graph**, not patched. Only `rg`/`RG` colours survive, so a CMYK or
30/// ICC-based fill comes back black; patterns, shadings and Type 3 text are
31/// lost; text keeps only its matrix, font, render mode and strings, so
32/// character and word spacing go. The `pdfrum-edit` documentation lists them in full. This applies **only to pages you edited** — every other page
33/// is copied through byte-for-byte.
34///
35/// ```ignore
36/// use pdfrum::{Document, SaveOptions};
37///
38/// let doc = Document::open("in.pdf")?;
39/// let mut page = doc.page(0)?.edit();
40/// page.remove(0);
41/// doc.save_pages("out.pdf", &[page], &SaveOptions::default())?;
42/// # Ok::<(), pdfrum::Error>(())
43/// ```
44#[derive(Debug, Clone)]
45pub struct PageEdit {
46    pub(crate) index: PageIndex,
47    pub(crate) page: Page,
48}
49
50impl PageEdit {
51    /// An editable graph for the page at `index`.
52    #[must_use]
53    pub fn new(index: PageIndex, page: Page) -> Self {
54        Self { index, page }
55    }
56
57    /// How many objects the page draws, including any switched off with
58    /// [`PageEdit::hide`].
59    #[must_use]
60    pub fn len(&self) -> usize {
61        self.page.objects().len()
62    }
63
64    /// Whether the page draws nothing at all.
65    #[must_use]
66    pub fn is_empty(&self) -> bool {
67        self.page.objects().is_empty()
68    }
69
70    /// The zero-based index of the page being edited.
71    #[must_use]
72    pub fn index(&self) -> PageIndex {
73        self.index
74    }
75
76    /// The objects, in painting order.
77    #[must_use]
78    pub fn objects(&self) -> &[PageObject] {
79        self.page.objects()
80    }
81
82    /// The object at `index`, for a caller who wants to change it in place.
83    ///
84    /// Taking this reference *is* the edit — the object is marked changed on
85    /// the way out, so its content stream is rewritten whether or not you go
86    /// on to touch it. Read with [`PageEdit::objects`] when you only want to
87    /// look.
88    pub fn object_mut(&mut self, index: usize) -> Option<&mut PageObject> {
89        self.page.object_mut(index)
90    }
91
92    /// Append an object, drawn last and therefore on top.
93    pub fn push(&mut self, object: PageObject) {
94        self.page.push_object(object);
95    }
96
97    /// Insert an object at `index`, pushing the ones there and after later in
98    /// the painting order. An index equal to [`PageEdit::len`] appends.
99    ///
100    /// # Errors
101    ///
102    /// [`IndexOutOfRange`] when `index` is past the end.
103    pub fn insert(&mut self, index: usize, object: PageObject) -> Result<(), IndexOutOfRange> {
104        self.page.insert_object(index, object)
105    }
106
107    /// Remove the object at `index` and hand it back.
108    pub fn remove(&mut self, index: usize) -> Option<PageObject> {
109        self.page.remove_object(index)
110    }
111
112    /// Show the object at `index` without removing it.
113    ///
114    /// A hidden object keeps its place and its index, so it can be shown again
115    /// — but it contributes nothing to the saved page, and reloading the saved
116    /// file will not find it.
117    ///
118    /// # Errors
119    ///
120    /// [`IndexOutOfRange`] when there is no object at `index`.
121    pub fn show(&mut self, index: usize) -> Result<(), IndexOutOfRange> {
122        self.set_active(index, true)
123    }
124
125    /// Hide the object at `index` without removing it.
126    ///
127    /// See [`PageEdit::show`].
128    ///
129    /// # Errors
130    ///
131    /// [`IndexOutOfRange`] when there is no object at `index`.
132    pub fn hide(&mut self, index: usize) -> Result<(), IndexOutOfRange> {
133        self.set_active(index, false)
134    }
135
136    fn set_active(&mut self, index: usize, active: bool) -> Result<(), IndexOutOfRange> {
137        let len = self.page.objects.len();
138        let Some(object) = self.page.objects.get_mut(index) else {
139            return Err(IndexOutOfRange { index, len });
140        };
141        object.set_active(active);
142        Ok(())
143    }
144
145    /// Whether the object at `index` is drawn.
146    #[must_use]
147    pub fn is_visible(&self, index: usize) -> Option<bool> {
148        self.page.objects().get(index).map(PageObject::is_active)
149    }
150
151    /// Move the object at `index` by `transform`.
152    ///
153    /// The transform is applied *before* the object's existing one, so
154    /// `Affine::translate((10.0, 0.0))` moves it ten points right in page
155    /// space whatever it was already doing.
156    ///
157    /// # Errors
158    ///
159    /// [`IndexOutOfRange`] when there is no object at `index`.
160    pub fn transform(&mut self, index: usize, transform: Affine) -> Result<(), IndexOutOfRange> {
161        let len = self.page.objects.len();
162        let Some(object) = self.page.object_mut(index) else {
163            return Err(IndexOutOfRange { index, len });
164        };
165        transform_object(object, transform);
166        Ok(())
167    }
168
169    /// Whether anything has been changed since the page was opened.
170    ///
171    /// A `false` here means the save will not rewrite this page's content at
172    /// all, and its bytes will come through untouched.
173    #[must_use]
174    pub fn is_modified(&self) -> bool {
175        self.page.is_dirty()
176    }
177
178    /// **Escape hatch — requires `pdfrum-page`.** The object graph
179    /// underneath, for a caller reaching past this surface.
180    ///
181    /// Matches the facade's `Page::objects` and `Document::parser`.
182    #[must_use]
183    pub fn graph(&self) -> &Page {
184        &self.page
185    }
186
187    /// **Escape hatch — requires `pdfrum-page`.** The graph, mutably.
188    ///
189    /// Marks nothing: a caller reaching here is responsible for saying what it
190    /// changed.
191    pub fn graph_mut(&mut self) -> &mut Page {
192        &mut self.page
193    }
194}
195
196/// Move `object` by `transform`, composed before whatever it already had —
197/// the body of [`PageEdit::transform`], for an object not yet on a page.
198pub fn transform_object(object: &mut PageObject, transform: Affine) {
199    match object {
200        PageObject::Path(p) => p.object.matrix = transform * p.object.matrix,
201        PageObject::Text(t) => {
202            t.object.matrix = transform * t.object.matrix;
203            t.object.position = transform * t.object.position;
204        }
205        PageObject::Image(i) => i.object.matrix = transform * i.object.matrix,
206        PageObject::Shading(s) => s.object.matrix = transform * s.object.matrix,
207        PageObject::Form(f) => f.object.matrix = transform * f.object.matrix,
208    }
209}
210
211impl PageEdit {
212    /// The `/Font` resource the text object at `index` uses, for a
213    /// `TextBuilder` that wants to write in the same font.
214    ///
215    /// `None` when there is no such object, when it is not text, or when its
216    /// font was written inline in the resource dictionary and so has no object
217    /// to name.
218    #[must_use]
219    pub fn font_of(&self, index: usize) -> Option<pdfrum_object::ObjRef> {
220        match self.page.objects().get(index)? {
221            PageObject::Text(text) => text.object.font_source,
222            _ => None,
223        }
224    }
225
226    /// The image `XObject` the object at `index` draws, for an
227    /// `ImageBuilder` that wants to place the same image again.
228    ///
229    /// `None` for anything that is not an image, and for an inline image,
230    /// which has no indirect object to name.
231    #[must_use]
232    pub fn image_of(&self, index: usize) -> Option<pdfrum_object::ObjRef> {
233        match self.page.objects().get(index)? {
234            PageObject::Image(image) => image.object.source,
235            _ => None,
236        }
237    }
238}