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}