Skip to main content

pdfrum_page/
mutate.rs

1//! Editing a page's object graph: what changed, and which content streams
2//! have to be written again because of it (ISO 32000-1 §7.8.2).
3//!
4//! # A page is a value, so "dirty" is a field and not a callback
5//!
6//! [`Page`] is a plain record. The two dirty facts live as two fields —
7//! [`Content::dirty`](crate::Content::dirty) on each object and
8//! [`Page::dirty_streams`] on the page — and the functions in this module are
9//! the only things that set them. A mutation is a function from a page to a
10//! page with two more bits set.
11//!
12//! # Why removal needs a set and modification does not
13//!
14//! A modified object still exists, so the regenerator finds it, sees its
15//! `dirty`, and knows to rewrite the stream it names. A *removed* object is
16//! gone — nothing is left to point at the stream that has to lose it — so its
17//! stream index is recorded on the page before the object goes. That is the
18//! whole reason `dirty_streams` exists, and it is why
19//! [`Page::remove_object`] takes an index rather than being a `Vec::retain`
20//! at the call site.
21//!
22//! An object switched to inactive is the third case and behaves like the
23//! first: it stays in the list, carries `dirty`, and the regenerator skips it
24//! when emitting while still counting its stream as needing a rewrite. That
25//! is exactly how it disappears.
26//!
27//! # The streamless object
28//!
29//! A brand-new object has never been in a content stream, so its
30//! [`Content::content_stream`](crate::Content::content_stream) is `None`. It
31//! sorts before `Some(0)` in the regenerator's ordered walk — `Option`'s own
32//! `Ord` — which is what gives a new object the lowest free `/Contents` index
33//! rather than one past the end.
34
35use std::collections::BTreeSet;
36
37use crate::page::{Page, PageObject};
38
39/// An object index that is not on the page.
40///
41/// Returned by [`Page::insert_object`] when `index` is past the end — an index
42/// equal to the length appends and is valid.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
44#[error("page object index {index} is out of range (len {len})")]
45pub struct IndexOutOfRange {
46    /// The index that was asked for.
47    pub index: usize,
48    /// How many objects the page has.
49    pub len: usize,
50}
51
52impl PageObject {
53    /// Whether the object has been changed since it was parsed.
54    ///
55    /// A dirty object's content stream is rewritten on the next save; a clean
56    /// one's is left byte-for-byte alone.
57    #[must_use]
58    pub fn is_dirty(&self) -> bool {
59        self.common().dirty
60    }
61
62    /// Whether the object is painted at all.
63    ///
64    /// An inactive object stays in the list — so its index and its stream are
65    /// still known — but contributes nothing to a regenerated stream, which is
66    /// how it disappears from the page without being deleted.
67    #[must_use]
68    pub fn is_active(&self) -> bool {
69        self.common().active
70    }
71
72    /// Which `/Contents` element the object came from, or `None` for one
73    /// that was created rather than parsed.
74    #[must_use]
75    pub fn content_stream(&self) -> Option<usize> {
76        self.common().content_stream
77    }
78
79    /// Mark the object changed, so its stream is rewritten on save.
80    pub fn mark_dirty(&mut self) {
81        *self.common_mut().dirty = true;
82    }
83
84    /// Mark the object unchanged. The regenerator does this once a stream has
85    /// been written.
86    pub fn mark_clean(&mut self) {
87        *self.common_mut().dirty = false;
88    }
89
90    /// Show or hide the object.
91    ///
92    /// Changing activity **always** dirties the object, because both
93    /// directions change what the stream must contain. Setting it to the value
94    /// it already has changes nothing at all — an idempotent call does not
95    /// force a regeneration.
96    pub fn set_active(&mut self, active: bool) {
97        let common = self.common_mut();
98        if *common.active == active {
99            return;
100        }
101        *common.active = active;
102        *common.dirty = true;
103    }
104
105    /// Set the `/Contents` index the object belongs to.
106    ///
107    /// Used by the regenerator's removal bookkeeping, which renumbers every
108    /// object after elements are dropped from the array.
109    pub fn set_content_stream(&mut self, stream: Option<usize>) {
110        *self.common_mut().content_stream = stream;
111    }
112}
113
114impl Page {
115    /// The objects, for a caller who only reads them.
116    #[must_use]
117    pub fn objects(&self) -> &[PageObject] {
118        &self.objects
119    }
120
121    /// The objects that are painted, in painting order.
122    pub fn active_objects(&self) -> impl Iterator<Item = &PageObject> {
123        self.objects.iter().filter(|o| o.is_active())
124    }
125
126    /// The object at `index`, marked dirty for the caller's edit.
127    ///
128    /// Handing out a `&mut` *is* the edit, so the flag is set on the way out
129    /// rather than being the caller's to remember. A caller that only wants to
130    /// read uses [`Page::objects`].
131    pub fn object_mut(&mut self, index: usize) -> Option<&mut PageObject> {
132        let object = self.objects.get_mut(index)?;
133        object.mark_dirty();
134        Some(object)
135    }
136
137    /// Append an object to the end of the page's painting order.
138    ///
139    /// The object arrives dirty and streamless, so the next save gives it a
140    /// content stream of its own — or the lowest free index, when the page had
141    /// none.
142    pub fn push_object(&mut self, mut object: PageObject) {
143        object.mark_dirty();
144        object.set_content_stream(None);
145        self.objects.push(object);
146    }
147
148    /// Insert an object at `index`, shifting the objects there and after.
149    ///
150    /// A streamless object **adopts its new neighbour's stream** so that the
151    /// requested position survives the save: without that it would be appended
152    /// to a fresh `/Contents` element, which is drawn last whatever its index
153    /// in the list said. An index equal to the length appends.
154    ///
155    /// # Errors
156    ///
157    /// [`IndexOutOfRange`] when `index` is past the end.
158    pub fn insert_object(
159        &mut self,
160        index: usize,
161        mut object: PageObject,
162    ) -> Result<(), IndexOutOfRange> {
163        let len = self.objects.len();
164        if index > len {
165            return Err(IndexOutOfRange { index, len });
166        }
167        object.mark_dirty();
168        if object.content_stream().is_none()
169            && let Some(neighbour) = self.objects.get(index)
170            && let Some(stream) = neighbour.content_stream()
171        {
172            object.set_content_stream(Some(stream));
173            self.dirty_streams.insert(Some(stream));
174        }
175        self.objects.insert(index, object);
176        Ok(())
177    }
178
179    /// Remove the object at `index` and hand it back.
180    ///
181    /// Its stream is recorded as dirty first — once the object is gone nothing
182    /// is left to say the stream must lose it.
183    pub fn remove_object(&mut self, index: usize) -> Option<PageObject> {
184        if index >= self.objects.len() {
185            return None;
186        }
187        let object = self.objects.remove(index);
188        if let Some(stream) = object.content_stream() {
189            self.dirty_streams.insert(Some(stream));
190        }
191        Some(object)
192    }
193
194    /// Whether anything on the page needs its content stream rewritten.
195    ///
196    /// The regenerator's early-out: a page answering `false` keeps its
197    /// `/Contents` and `/Resources` bytes exactly as they were.
198    #[must_use]
199    pub fn is_dirty(&self) -> bool {
200        !self.dirty_streams.is_empty() || self.objects.iter().any(PageObject::is_dirty)
201    }
202
203    /// Every content stream that has to be written again.
204    ///
205    /// The union of the streams named by dirty objects — **including inactive
206    /// ones**, whose streams must be regenerated precisely so that they lose
207    /// them — and the streams recorded when objects were removed.
208    #[must_use]
209    pub fn dirty_stream_set(&self) -> BTreeSet<Option<usize>> {
210        let mut set = self.dirty_streams.clone();
211        for object in &self.objects {
212            if object.is_dirty() {
213                set.insert(object.content_stream());
214            }
215        }
216        set
217    }
218
219    /// Forget every pending change: the page is now as its bytes describe it.
220    ///
221    /// Called once a save has written the regenerated streams.
222    pub fn mark_clean(&mut self) {
223        self.dirty_streams.clear();
224        for object in &mut self.objects {
225            object.mark_clean();
226        }
227    }
228
229    /// The transform in force where `stream` begins.
230    ///
231    /// A content stream inherits whatever transform the streams before it left
232    /// behind, so a stream rewritten on its own has to undo that inheritance
233    /// before it states its own state — otherwise the regenerated bytes are
234    /// interpreted under a matrix the original never saw. Stream 0, and a page
235    /// that recorded no transforms at all, begin at the identity; the
236    /// streamless object is appended after everything and so begins wherever
237    /// the last stream ended.
238    #[must_use]
239    pub fn ctm_at_start_of_stream(&self, stream: Option<usize>) -> kurbo::Affine {
240        let Some(stream) = stream else {
241            // Streamless: appended last, so it inherits the final transform.
242            return self
243                .stream_ctms
244                .values()
245                .next_back()
246                .copied()
247                .unwrap_or(kurbo::Affine::IDENTITY);
248        };
249        if stream == 0 || self.stream_ctms.is_empty() {
250            return kurbo::Affine::IDENTITY;
251        }
252        self.ctm_at_end_of_stream(stream.saturating_sub(1))
253    }
254
255    /// The transform in force where `stream` ends.
256    ///
257    /// A stream that recorded nothing answers with the first later stream that
258    /// did — the transform was never changed in between — and falls back to the
259    /// last recorded one when there is no later stream at all.
260    #[must_use]
261    pub fn ctm_at_end_of_stream(&self, stream: usize) -> kurbo::Affine {
262        self.stream_ctms
263            .range(stream..)
264            .next()
265            .or_else(|| self.stream_ctms.iter().next_back())
266            .map_or(kurbo::Affine::IDENTITY, |(_, m)| *m)
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    // The fixtures below build pages of a length they fix themselves, so
273    // indexing one is the clearest way to name the object under test.
274    #![allow(
275        clippy::indexing_slicing,
276        reason = "test fixtures index arrays whose length the fixture fixes"
277    )]
278
279    use super::IndexOutOfRange;
280    use crate::page::{Content, Page, PageObject, PathObject};
281    use crate::state::{ContentMarks, GraphicsState};
282    use kurbo::{Affine, BezPath};
283
284    fn object(stream: Option<usize>) -> PageObject {
285        PageObject::Path(Box::new(Content {
286            object: PathObject {
287                path: BezPath::new(),
288                matrix: Affine::IDENTITY,
289                fill_rule: crate::ops::FillRule::Winding,
290                stroke: false,
291            },
292            state: GraphicsState::default(),
293            marks: ContentMarks::new(),
294            content_stream: stream,
295            dirty: false,
296            active: true,
297        }))
298    }
299
300    fn page(streams: &[Option<usize>]) -> Page {
301        Page {
302            objects: streams.iter().copied().map(object).collect(),
303            ..Page::empty()
304        }
305    }
306
307    /// The common case: every object came out of a real stream.
308    fn page_of(streams: &[usize]) -> Page {
309        page(&streams.iter().copied().map(Some).collect::<Vec<_>>())
310    }
311
312    // A freshly parsed page regenerates nothing.
313    #[test]
314    fn an_untouched_page_is_clean() {
315        let page = page_of(&[0, 0, 1]);
316        assert!(!page.is_dirty());
317        assert!(page.dirty_stream_set().is_empty());
318    }
319
320    #[test]
321    fn taking_a_mutable_object_dirties_it() {
322        let mut page = page_of(&[0, 1]);
323        assert!(page.object_mut(1).is_some());
324        assert!(page.is_dirty());
325        assert_eq!(page.dirty_stream_set(), [Some(1)].into_iter().collect());
326        // Reading does not.
327        let mut clean = page.clone();
328        clean.mark_clean();
329        let _ = clean.objects();
330        assert!(!clean.is_dirty());
331    }
332
333    #[test]
334    fn an_appended_object_is_dirty_and_streamless() {
335        let mut page = page_of(&[0]);
336        page.push_object(object(Some(7)));
337        let added = page.objects.last().expect("pushed");
338        assert!(added.is_dirty());
339        assert_eq!(added.content_stream(), None);
340        assert_eq!(page.dirty_stream_set(), [None].into_iter().collect());
341    }
342
343    // A streamless insert adopts the neighbour's stream so its position
344    // survives the save.
345    #[test]
346    fn an_inserted_object_adopts_its_neighbours_stream() {
347        let mut page = page_of(&[0, 2, 2]);
348        let mut fresh = object(None);
349        fresh.set_content_stream(None);
350        assert!(page.insert_object(1, fresh).is_ok());
351        assert_eq!(page.objects.len(), 4);
352        assert_eq!(page.objects[1].content_stream(), Some(2));
353        assert!(page.dirty_streams.contains(&Some(2)));
354    }
355
356    #[test]
357    fn an_insert_past_the_end_is_refused() {
358        let mut page = page_of(&[0]);
359        let err = page
360            .insert_object(2, object(None))
361            .expect_err("past the end");
362        assert_eq!(err, IndexOutOfRange { index: 2, len: 1 });
363        assert_eq!(page.objects.len(), 1);
364        // One past the last index appends.
365        assert!(page.insert_object(1, object(None)).is_ok());
366        assert_eq!(page.objects.len(), 2);
367    }
368
369    // Removal is the case that needs the page-level set: the object that knew
370    // the stream is gone.
371    #[test]
372    fn removing_an_object_records_its_stream() {
373        let mut page = page_of(&[0, 3]);
374        let removed = page.remove_object(1).expect("removed");
375        assert_eq!(removed.content_stream(), Some(3));
376        assert_eq!(page.objects.len(), 1);
377        assert!(page.is_dirty());
378        assert_eq!(page.dirty_stream_set(), [Some(3)].into_iter().collect());
379    }
380
381    #[test]
382    fn removing_a_streamless_object_records_nothing() {
383        let mut page = page(&[None]);
384        assert!(page.remove_object(0).is_some());
385        assert!(page.dirty_streams.is_empty());
386        assert!(!page.is_dirty());
387        assert!(page.remove_object(0).is_none());
388    }
389
390    // An inactive object's stream is still regenerated — that is how the
391    // object disappears from it.
392    #[test]
393    fn deactivating_an_object_dirties_its_stream() {
394        let mut page = page_of(&[0, 1]);
395        page.objects[1].set_active(false);
396        assert!(!page.objects[1].is_active());
397        assert!(page.objects[1].is_dirty());
398        assert_eq!(page.dirty_stream_set(), [Some(1)].into_iter().collect());
399        assert_eq!(page.active_objects().count(), 1);
400    }
401
402    #[test]
403    fn setting_activity_to_what_it_already_is_changes_nothing() {
404        let mut page = page_of(&[0]);
405        page.objects[0].set_active(true);
406        assert!(!page.is_dirty());
407    }
408
409    #[test]
410    fn a_save_leaves_the_page_clean() {
411        let mut page = page_of(&[0, 1]);
412        page.object_mut(0);
413        let _ = page.remove_object(1);
414        assert!(page.is_dirty());
415        page.mark_clean();
416        assert!(!page.is_dirty());
417        assert!(page.objects.iter().all(|o| !o.is_dirty()));
418    }
419
420    // The CTM lookups, against the C++'s own three cases.
421    #[test]
422    fn stream_zero_and_an_empty_map_begin_at_the_identity() {
423        let page = page_of(&[0]);
424        assert_eq!(page.ctm_at_start_of_stream(Some(0)), Affine::IDENTITY);
425        assert_eq!(page.ctm_at_start_of_stream(Some(3)), Affine::IDENTITY);
426        assert_eq!(page.ctm_at_end_of_stream(0), Affine::IDENTITY);
427    }
428
429    #[test]
430    fn a_later_stream_begins_where_the_previous_one_ended() {
431        let mut page = page_of(&[0, 1]);
432        let scaled = Affine::scale(2.0);
433        let moved = Affine::translate((5.0, 0.0));
434        page.stream_ctms.insert(0, scaled);
435        page.stream_ctms.insert(1, moved);
436        assert_eq!(page.ctm_at_start_of_stream(Some(0)), Affine::IDENTITY);
437        assert_eq!(page.ctm_at_start_of_stream(Some(1)), scaled);
438        assert_eq!(page.ctm_at_end_of_stream(1), moved);
439        // A streamless object is appended last, so it starts at the end
440        // of everything.
441        assert_eq!(page.ctm_at_start_of_stream(None), moved);
442    }
443
444    // A stream that changed nothing has no entry; the lookup rolls forward to
445    // the first that did.
446    #[test]
447    fn a_stream_with_no_entry_reads_the_next_one_that_has_it() {
448        let mut page = page_of(&[0]);
449        let scaled = Affine::scale(3.0);
450        page.stream_ctms.insert(0, Affine::IDENTITY);
451        page.stream_ctms.insert(4, scaled);
452        assert_eq!(page.ctm_at_end_of_stream(2), scaled);
453        // Past the last entry, the last entry answers.
454        assert_eq!(page.ctm_at_end_of_stream(9), scaled);
455    }
456}