Skip to main content

pdfrum_edit/
pages.rs

1//! Page-tree edits that are not imports: a blank page, deleting pages, and
2//! the per-page attributes a save carries — rotation and the boxes.
3//!
4//! Every function works on the page tree as the base document has it, so
5//! page indices are the base document's numbering throughout; a caller who
6//! deletes and then adds does both against that numbering, and the writer
7//! resolves the edits together.
8
9use pdfrum_common::PageIndex;
10use pdfrum_object::{Array, Dict, Name, ObjRef, Object, Resolve, names};
11
12use crate::doc::EditDoc;
13use crate::error::Error;
14use crate::import::{PageRange, init_dest, insert_into_tree};
15
16/// One of the five page boxes (ISO 32000-1 §14.11.2).
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum PageBox {
19    /// `/MediaBox`.
20    Media,
21    /// `/CropBox`.
22    Crop,
23    /// `/BleedBox`.
24    Bleed,
25    /// `/TrimBox`.
26    Trim,
27    /// `/ArtBox`.
28    Art,
29}
30
31impl PageBox {
32    fn key(self) -> &'static Name {
33        match self {
34            Self::Media => names::MEDIA_BOX,
35            Self::Crop => names::CROP_BOX,
36            Self::Bleed => names::BLEED_BOX,
37            Self::Trim => names::TRIM_BOX,
38            Self::Art => names::ART_BOX,
39        }
40    }
41}
42
43/// Add an empty page of `width` by `height` points at index `at` (past the
44/// end appends), and return its reference.
45///
46/// The page carries only `/Type`, `/Parent` and `/MediaBox`; it has no
47/// contents and no resources until a caller draws on it.
48///
49/// # Errors
50///
51/// [`Error::NoDestinationCatalog`] when the document has no catalog to hang
52/// a page tree on.
53pub fn add_blank_page(
54    dest: &mut EditDoc<'_>,
55    width: f32,
56    height: f32,
57    at: impl Into<PageIndex>,
58) -> Result<ObjRef, Error> {
59    let at = u32::from(at.into());
60    let pages_node = init_dest(dest)?;
61    let page = Dict::from_pairs([
62        (names::TYPE.clone(), Object::Name(names::PAGE.clone())),
63        (
64            names::PARENT.clone(),
65            Object::Ref(ObjRef::new(pages_node, 0)),
66        ),
67        (
68            names::MEDIA_BOX.clone(),
69            rect_object([0.0, 0.0, width, height]),
70        ),
71    ]);
72    let created = dest.add(Object::Dict(page));
73    insert_into_tree(dest, pages_node, &[created], at);
74    Ok(created)
75}
76
77/// Delete the pages `range` names, by the base document's numbering.
78///
79/// Each page is unlinked from its parent's `/Kids` and every `/Count` up the
80/// chain is decremented; the page object itself is left for the writer's
81/// garbage collection. A duplicate index deletes once.
82///
83/// # Errors
84///
85/// [`Error::PageIndexOutOfRange`] when the range names a page the document
86/// does not have; the document is untouched on that.
87pub fn delete_pages(dest: &mut EditDoc<'_>, range: &PageRange) -> Result<(), Error> {
88    let mut refs = Vec::with_capacity(range.indices().len());
89    for &index in range.indices() {
90        let page = dest
91            .base()
92            .page(index)
93            .map_err(|_| Error::PageIndexOutOfRange(index))?;
94        let Some(reference) = page.reference else {
95            // A page written inline in its parent's `/Kids` has no reference
96            // to unlink by; the oracle cannot delete one either.
97            return Err(Error::PageIndexOutOfRange(index));
98        };
99        if !refs.contains(&reference) {
100            refs.push(reference);
101        }
102    }
103    for reference in refs {
104        unlink(dest, reference);
105    }
106    Ok(())
107}
108
109/// Unlink one page from its parent and decrement the counts above it.
110fn unlink(dest: &mut EditDoc<'_>, page: ObjRef) {
111    let Some(page_dict) = dict_of(dest, page) else {
112        return;
113    };
114    let Some(Object::Ref(parent_ref)) = page_dict.raw(names::PARENT).cloned() else {
115        return;
116    };
117    let Some(mut parent) = dict_of(dest, parent_ref) else {
118        return;
119    };
120    let kids: Array = parent
121        .raw(names::KIDS)
122        .and_then(Object::as_array)
123        .map(|kids| {
124            kids.iter()
125                .filter(|kid| !matches!(kid, Object::Ref(r) if *r == page))
126                .cloned()
127                .collect()
128        })
129        .unwrap_or_default();
130    parent.insert(names::KIDS.clone(), Object::Array(kids));
131    dest.replace(parent_ref, Object::Dict(parent));
132    decrement_counts(dest, parent_ref);
133}
134
135/// Take one off `/Count` on `node` and every ancestor.
136fn decrement_counts(dest: &mut EditDoc<'_>, mut node_ref: ObjRef) {
137    // A cycle in `/Parent` would loop forever; the tree's depth bounds it.
138    for _ in 0..64 {
139        let Some(mut node) = dict_of(dest, node_ref) else {
140            return;
141        };
142        let count = node
143            .raw(names::COUNT)
144            .and_then(Object::as_int)
145            .unwrap_or(0)
146            .saturating_sub(1)
147            .max(0);
148        node.insert(names::COUNT.clone(), Object::Int(count));
149        let parent = node.raw(names::PARENT).cloned();
150        dest.replace(node_ref, Object::Dict(node));
151        match parent {
152            Some(Object::Ref(parent_ref)) => node_ref = parent_ref,
153            _ => return,
154        }
155    }
156}
157
158/// Set a page's `/Rotate`. `degrees` is normalized to a multiple of 90; a
159/// value that is not one is rounded to the nearest.
160///
161/// # Errors
162///
163/// [`Error::PageIndexOutOfRange`] when there is no such page or the page is
164/// written inline.
165pub fn set_page_rotation(
166    dest: &mut EditDoc<'_>,
167    index: impl Into<PageIndex>,
168    degrees: i32,
169) -> Result<(), Error> {
170    let index = index.into();
171    // Nearest multiple of 90, then modulo a full turn: 45 rounds up, -90
172    // is 270.
173    let quarter_turns =
174        (degrees.div_euclid(90) + i32::from(degrees.rem_euclid(90) >= 45)).rem_euclid(4);
175    let quarter_turns = i64::from(quarter_turns);
176    edit_page(dest, index, |page| {
177        page.insert(names::ROTATE.clone(), Object::Int(quarter_turns * 90));
178    })
179}
180
181/// Sets a page's `/Rotate` from the read side's own
182/// [`Rotation`](pdfrum_page::Rotation).
183///
184/// [`set_page_rotation`] takes degrees and rounds to the nearest quarter
185/// turn, which is the right shape for a `--rotate 90` flag but the wrong one
186/// for a caller that already holds the value
187/// [`Page::rotate`](pdfrum_page::Page) answered: a round trip through `i32`
188/// and back is a chance to round something that was already exact.
189///
190/// # Errors
191///
192/// [`Error::PageIndexOutOfRange`] when there is no such page or the page is
193/// written inline.
194///
195/// ```
196/// use std::sync::Arc;
197/// use pdfrum_edit::{EditDoc, set_page_rotation_to};
198/// use pdfrum_page::Rotation;
199/// use pdfrum_parser::{LoadOptions, load};
200///
201/// let bytes: Arc<[u8]> = Arc::from(&include_bytes!("../tests/files/hello.pdf")[..]);
202/// let doc = load(bytes, &LoadOptions::default())?;
203/// let mut edit = EditDoc::new(&doc);
204/// set_page_rotation_to(&mut edit, 0u32, Rotation::Quarter)?;
205/// # Ok::<(), Box<dyn std::error::Error>>(())
206/// ```
207pub fn set_page_rotation_to(
208    dest: &mut EditDoc<'_>,
209    index: impl Into<PageIndex>,
210    rotation: pdfrum_page::Rotation,
211) -> Result<(), Error> {
212    let index = index.into();
213    edit_page(dest, index, |page| {
214        page.insert(
215            names::ROTATE.clone(),
216            Object::Int(i64::from(rotation.degrees())),
217        );
218    })
219}
220
221/// Set one of a page's boxes.
222///
223/// # Errors
224///
225/// As [`set_page_rotation`].
226pub fn set_page_box(
227    dest: &mut EditDoc<'_>,
228    index: impl Into<PageIndex>,
229    which: PageBox,
230    rect: [f32; 4],
231) -> Result<(), Error> {
232    let index = index.into();
233    edit_page(dest, index, |page| {
234        page.insert(which.key().clone(), rect_object(rect));
235    })
236}
237
238/// Fetch a page's dictionary as the edits leave it, change it, write it back.
239fn edit_page(
240    dest: &mut EditDoc<'_>,
241    index: PageIndex,
242    change: impl FnOnce(&mut Dict),
243) -> Result<(), Error> {
244    let page = dest
245        .base()
246        .page(index)
247        .map_err(|_| Error::PageIndexOutOfRange(index))?;
248    let Some(reference) = page.reference else {
249        return Err(Error::PageIndexOutOfRange(index));
250    };
251    let mut dict = dict_of(dest, reference).unwrap_or(page.dict);
252    change(&mut dict);
253    dest.replace(reference, Object::Dict(dict));
254    Ok(())
255}
256
257/// A dictionary as the edits currently leave it.
258fn dict_of(dest: &EditDoc<'_>, reference: ObjRef) -> Option<Dict> {
259    dest.fetch(reference)
260        .ok()
261        .and_then(|object| object.as_dict().cloned())
262}
263
264fn rect_object(rect: [f32; 4]) -> Object {
265    Object::Array(rect.into_iter().map(Object::Real).collect())
266}
267
268/// Reorders the document's pages to the sequence `order` names.
269///
270/// `order` is the new arrangement written as old indices: `[2, 0, 1]` puts
271/// what was page 3 first. Every page must appear exactly once — a list that
272/// repeats an index or leaves one out is not a reordering, and is refused
273/// rather than guessed at.
274///
275/// The page tree is flattened to a single level in the process, because a
276/// reordering that preserved an inherited-attribute hierarchy would have to
277/// decide which node each page now belongs under, and any answer changes what
278/// the page inherits. Flattening keeps every page's own attributes intact,
279/// which is what a caller reordering pages means; inherited `/Resources`,
280/// `/MediaBox`, `/CropBox` and `/Rotate` are resolved onto each page first so
281/// nothing is lost.
282///
283/// # Errors
284///
285/// [`Error::PageIndexOutOfRange`] when an index names no page, when a page is
286/// written inline and has no reference to move, or when `order` is not a
287/// permutation of every page.
288///
289/// ```
290/// use std::sync::Arc;
291/// use pdfrum_edit::{EditDoc, reorder_pages};
292/// use pdfrum_parser::{LoadOptions, load};
293///
294/// let bytes: Arc<[u8]> = Arc::from(&include_bytes!("../tests/files/hello.pdf")[..]);
295/// let doc = load(bytes, &LoadOptions::default())?;
296/// let mut edit = EditDoc::new(&doc);
297/// // A one-page document has exactly one arrangement.
298/// reorder_pages(&mut edit, &[0])?;
299/// # Ok::<(), Box<dyn std::error::Error>>(())
300/// ```
301pub fn reorder_pages(dest: &mut EditDoc<'_>, order: &[usize]) -> Result<(), Error> {
302    let count = usize::try_from(dest.base().page_count()).unwrap_or(usize::MAX);
303    if order.len() != count {
304        // Naming fewer pages than the document has is a deletion wearing a
305        // reorder's clothes; naming more is a duplication. Both have their
306        // own call.
307        return Err(Error::PageIndexOutOfRange(PageIndex::from(
308            u32::try_from(order.len().min(count)).unwrap_or(0),
309        )));
310    }
311    let mut seen = vec![false; count];
312    for &index in order {
313        let slot = seen
314            .get_mut(index)
315            .ok_or(Error::PageIndexOutOfRange(PageIndex::from(
316                u32::try_from(index).unwrap_or(u32::MAX),
317            )))?;
318        if *slot {
319            return Err(Error::PageIndexOutOfRange(PageIndex::from(
320                u32::try_from(index).unwrap_or(u32::MAX),
321            )));
322        }
323        *slot = true;
324    }
325
326    // Resolve what each page inherits before the tree it inherits through is
327    // replaced.
328    let mut pages = Vec::with_capacity(count);
329    for &index in order {
330        let page_index = PageIndex::from(u32::try_from(index).unwrap_or(u32::MAX));
331        let page = dest
332            .base()
333            .page(page_index)
334            .map_err(|_| Error::PageIndexOutOfRange(page_index))?;
335        let reference = page
336            .reference
337            .ok_or(Error::PageIndexOutOfRange(page_index))?;
338        pages.push((reference, page.dict.clone()));
339    }
340
341    let Some(root_ref) = dest.base().trailer().reference(names::ROOT) else {
342        return Err(Error::NoDestinationCatalog);
343    };
344    let Some(catalog) = dict_of(dest, root_ref) else {
345        return Err(Error::NoDestinationCatalog);
346    };
347    let Some(Object::Ref(tree_ref)) = catalog.raw(names::PAGES).cloned() else {
348        return Err(Error::NoDestinationCatalog);
349    };
350
351    for (reference, mut dict) in pages {
352        // Every page now hangs off the root, so it carries what it used to
353        // inherit.
354        for key in [
355            names::RESOURCES.clone(),
356            names::MEDIA_BOX.clone(),
357            names::CROP_BOX.clone(),
358            names::ROTATE.clone(),
359        ] {
360            if !dict.contains_key(&key)
361                && let Some(inherited) = inherited_of(dest, &dict, &key)
362            {
363                dict.insert(key, inherited);
364            }
365        }
366        dict.insert(names::PARENT.clone(), Object::Ref(tree_ref));
367        dest.replace(reference, Object::Dict(dict));
368    }
369
370    let kids: Array = order
371        .iter()
372        .filter_map(|&index| {
373            let page_index = PageIndex::from(u32::try_from(index).unwrap_or(u32::MAX));
374            dest.base()
375                .page(page_index)
376                .ok()
377                .and_then(|page| page.reference)
378                .map(Object::Ref)
379        })
380        .collect();
381
382    let mut tree = dict_of(dest, tree_ref).unwrap_or_default();
383    tree.insert(names::TYPE.clone(), Object::Name(names::PAGES.clone()));
384    tree.insert(
385        names::COUNT.clone(),
386        Object::Int(i64::try_from(order.len()).unwrap_or(0)),
387    );
388    tree.insert(names::KIDS.clone(), Object::Array(kids));
389    tree.remove(names::PARENT);
390    dest.replace(tree_ref, Object::Dict(tree));
391    Ok(())
392}
393
394/// One inheritable attribute, walked up from `dict` through `/Parent`.
395fn inherited_of(dest: &EditDoc<'_>, dict: &Dict, key: &Name) -> Option<Object> {
396    let mut current = dict.clone();
397    // A malformed file can loop its `/Parent` chain; the page tree is never
398    // deep, so a bounded walk is both safe and sufficient.
399    for _ in 0..64 {
400        if let Some(value) = current.raw(key) {
401            return Some(value.clone());
402        }
403        let Some(Object::Ref(parent)) = current.raw(names::PARENT).cloned() else {
404            return None;
405        };
406        current = dict_of(dest, parent)?;
407    }
408    None
409}