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: u32,
58) -> Result<ObjRef, Error> {
59    let pages_node = init_dest(dest)?;
60    let page = Dict::from_pairs([
61        (names::TYPE.clone(), Object::Name(names::PAGE.clone())),
62        (
63            names::PARENT.clone(),
64            Object::Ref(ObjRef::new(pages_node, 0)),
65        ),
66        (
67            names::MEDIA_BOX.clone(),
68            rect_object([0.0, 0.0, width, height]),
69        ),
70    ]);
71    let created = dest.add(Object::Dict(page));
72    insert_into_tree(dest, pages_node, &[created], at);
73    Ok(created)
74}
75
76/// Delete the pages `range` names, by the base document's numbering.
77///
78/// Each page is unlinked from its parent's `/Kids` and every `/Count` up the
79/// chain is decremented; the page object itself is left for the writer's
80/// garbage collection. A duplicate index deletes once.
81///
82/// # Errors
83///
84/// [`Error::PageIndexOutOfRange`] when the range names a page the document
85/// does not have; the document is untouched on that.
86pub fn delete_pages(dest: &mut EditDoc<'_>, range: &PageRange) -> Result<(), Error> {
87    let mut refs = Vec::with_capacity(range.indices().len());
88    for &index in range.indices() {
89        let page = dest
90            .base()
91            .page(index)
92            .map_err(|_| Error::PageIndexOutOfRange(index))?;
93        let Some(reference) = page.reference else {
94            // A page written inline in its parent's `/Kids` has no reference
95            // to unlink by; the oracle cannot delete one either.
96            return Err(Error::PageIndexOutOfRange(index));
97        };
98        if !refs.contains(&reference) {
99            refs.push(reference);
100        }
101    }
102    for reference in refs {
103        unlink(dest, reference);
104    }
105    Ok(())
106}
107
108/// Unlink one page from its parent and decrement the counts above it.
109fn unlink(dest: &mut EditDoc<'_>, page: ObjRef) {
110    let Some(page_dict) = dict_of(dest, page) else {
111        return;
112    };
113    let Some(Object::Ref(parent_ref)) = page_dict.raw(names::PARENT).cloned() else {
114        return;
115    };
116    let Some(mut parent) = dict_of(dest, parent_ref) else {
117        return;
118    };
119    let kids: Array = parent
120        .raw(names::KIDS)
121        .and_then(Object::as_array)
122        .map(|kids| {
123            kids.iter()
124                .filter(|kid| !matches!(kid, Object::Ref(r) if *r == page))
125                .cloned()
126                .collect()
127        })
128        .unwrap_or_default();
129    parent.insert(names::KIDS.clone(), Object::Array(kids));
130    dest.replace(parent_ref, Object::Dict(parent));
131    decrement_counts(dest, parent_ref);
132}
133
134/// Take one off `/Count` on `node` and every ancestor.
135fn decrement_counts(dest: &mut EditDoc<'_>, mut node_ref: ObjRef) {
136    // A cycle in `/Parent` would loop forever; the tree's depth bounds it.
137    for _ in 0..64 {
138        let Some(mut node) = dict_of(dest, node_ref) else {
139            return;
140        };
141        let count = node
142            .raw(names::COUNT)
143            .and_then(Object::as_int)
144            .unwrap_or(0)
145            .saturating_sub(1)
146            .max(0);
147        node.insert(names::COUNT.clone(), Object::Int(count));
148        let parent = node.raw(names::PARENT).cloned();
149        dest.replace(node_ref, Object::Dict(node));
150        match parent {
151            Some(Object::Ref(parent_ref)) => node_ref = parent_ref,
152            _ => return,
153        }
154    }
155}
156
157/// Set a page's `/Rotate`. `degrees` is normalized to a multiple of 90; a
158/// value that is not one is rounded to the nearest.
159///
160/// # Errors
161///
162/// [`Error::PageIndexOutOfRange`] when there is no such page or the page is
163/// written inline.
164pub fn set_page_rotation(
165    dest: &mut EditDoc<'_>,
166    index: PageIndex,
167    degrees: i32,
168) -> Result<(), Error> {
169    // Nearest multiple of 90, then modulo a full turn: 45 rounds up, -90
170    // is 270.
171    let quarter_turns =
172        (degrees.div_euclid(90) + i32::from(degrees.rem_euclid(90) >= 45)).rem_euclid(4);
173    let quarter_turns = i64::from(quarter_turns);
174    edit_page(dest, index, |page| {
175        page.insert(names::ROTATE.clone(), Object::Int(quarter_turns * 90));
176    })
177}
178
179/// Set one of a page's boxes.
180///
181/// # Errors
182///
183/// As [`set_page_rotation`].
184pub fn set_page_box(
185    dest: &mut EditDoc<'_>,
186    index: PageIndex,
187    which: PageBox,
188    rect: [f32; 4],
189) -> Result<(), Error> {
190    edit_page(dest, index, |page| {
191        page.insert(which.key().clone(), rect_object(rect));
192    })
193}
194
195/// Fetch a page's dictionary as the edits leave it, change it, write it back.
196fn edit_page(
197    dest: &mut EditDoc<'_>,
198    index: PageIndex,
199    change: impl FnOnce(&mut Dict),
200) -> Result<(), Error> {
201    let page = dest
202        .base()
203        .page(index)
204        .map_err(|_| Error::PageIndexOutOfRange(index))?;
205    let Some(reference) = page.reference else {
206        return Err(Error::PageIndexOutOfRange(index));
207    };
208    let mut dict = dict_of(dest, reference).unwrap_or(page.dict);
209    change(&mut dict);
210    dest.replace(reference, Object::Dict(dict));
211    Ok(())
212}
213
214/// A dictionary as the edits currently leave it.
215fn dict_of(dest: &EditDoc<'_>, reference: ObjRef) -> Option<Dict> {
216    dest.fetch(reference)
217        .ok()
218        .and_then(|object| object.as_dict().cloned())
219}
220
221fn rect_object(rect: [f32; 4]) -> Object {
222    Object::Array(rect.into_iter().map(Object::Real).collect())
223}