Skip to main content

pdfboss_write/
importer.rs

1//! Transplanting a source document's object graph into a [`Writer`] (ISO
2//! 32000 ยง7.3.10, indirect references): [`Importer`] renumbers every
3//! reference it meets once and drains the resulting queue iteratively, so a
4//! long chain of references costs no stack, and a caller may substitute a
5//! translated body for any object before the drain reaches it.
6
7use pdfboss_core::{Dict, Document, FastMap, Name, ObjRef, Object, Rect, Stream};
8
9use crate::error::{Error, Result};
10use crate::update::{core_error, deflate};
11use crate::writer::Writer;
12
13/// Copies one source document's reachable object graph into a [`Writer`].
14/// Every reference met through [`Importer::reference`] or [`Importer::copy`]
15/// is reserved a target number once and queued, so repeated imports from the
16/// same source dedup by source object number: one `Importer` per source
17/// document, and one `Writer` accepts several `Importer`s in sequence.
18pub struct Importer<'w, 's> {
19    writer: &'w mut Writer,
20    source: &'s Document,
21    map: FastMap<ObjRef, ObjRef>,
22    pending: Vec<ObjRef>,
23    substitutions: FastMap<ObjRef, Object>,
24    compress: bool,
25}
26
27impl<'w, 's> Importer<'w, 's> {
28    /// Opens `source` for import into `writer`. Refuses a locked source:
29    /// `Document::get` only decrypts transparently once a working
30    /// password configured a decryptor, so copying from a locked source
31    /// would emit raw ciphertext into a plain target with no `/Encrypt`
32    /// of its own. A password-opened encrypted document copies fine: its
33    /// content already reads as plaintext through `Document::get`, exactly
34    /// what [`crate::encrypt_document`] relies on to re-encrypt it under
35    /// new passwords.
36    pub fn new(writer: &'w mut Writer, source: &'s Document) -> Result<Importer<'w, 's>> {
37        // The public load path already refuses a wrong or missing password
38        // before any `Document` exists; this check is a second safeguard,
39        // for a `Document` constructed some other way.
40        if source.is_locked() {
41            return Err(Error::EncryptedBase);
42        }
43        let compress = writer.compress();
44        Ok(Importer {
45            writer,
46            source,
47            map: FastMap::default(),
48            pending: Vec::new(),
49            substitutions: FastMap::default(),
50            compress,
51        })
52    }
53
54    /// The target number for source reference `r`, reserved and queued on
55    /// first sight.
56    pub fn reference(&mut self, r: ObjRef) -> ObjRef {
57        if let Some(copied) = self.map.get(&r) {
58            return *copied;
59        }
60        let copied = self.writer.reserve();
61        self.map.insert(r, copied);
62        self.pending.push(r);
63        copied
64    }
65
66    /// A copy of `obj` (source-space) with every reference renumbered into
67    /// the target: the existing private translation, made public.
68    pub fn copy(&mut self, obj: &Object) -> Result<Object> {
69        Ok(match obj {
70            Object::Ref(r) => Object::Ref(self.reference(*r)),
71            Object::Dict(d) => Object::Dict(self.copy_dict(d)?),
72            Object::Array(items) => Object::Array(
73                items
74                    .iter()
75                    .map(|item| self.copy(item))
76                    .collect::<Result<Vec<Object>>>()?,
77            ),
78            Object::Stream(_) => return Err(Error::NestedStream),
79            other => other.clone(),
80        })
81    }
82
83    /// A copy of `dict`'s direct structure with every reference mapped.
84    pub(crate) fn copy_dict(&mut self, dict: &Dict) -> Result<Dict> {
85        let mut out = Dict::new();
86        for (key, value) in dict.iter() {
87            out.insert(key.clone(), self.copy(value)?);
88        }
89        Ok(out)
90    }
91
92    /// A stream body: its dictionary copied without `/Length` (the writer
93    /// sets it), its data compressed when asked and not already filtered.
94    fn copy_stream(&mut self, stream: &Stream) -> Result<Object> {
95        let mut dict = stream.dict.clone();
96        dict.remove("Length");
97        let mut dict = self.copy_dict(&dict)?;
98        let data = if self.compress && dict.get("Filter").is_none() {
99            dict.insert(name("Filter"), Object::Name(name("FlateDecode")));
100            deflate(&stream.data)
101        } else {
102            stream.data.clone()
103        };
104        Ok(Object::Stream(Stream { dict, data }))
105    }
106
107    /// Replaces the source object's body during the transplant. The
108    /// body is TARGET-space and drain fills it verbatim, no
109    /// renumbering: the caller translates any source refs into it via
110    /// `reference`/`copy` first, and may use refs of objects already
111    /// in the writer directly.
112    ///
113    /// Must be called before `finish` drains `r`: once `r` is popped off
114    /// the pending queue and filled, a later `substitute` for it has no
115    /// effect. `page` cannot trigger this hazard: it only substitutes a
116    /// reference it has itself just queued for the first time.
117    pub fn substitute(&mut self, r: ObjRef, body: Object) {
118        self.substitutions.insert(r, body);
119    }
120
121    /// Drains the pending queue; called by `page`/`document` before
122    /// returning, public for callers that mixed `reference` in.
123    pub fn finish(&mut self) -> Result<()> {
124        while let Some(r) = self.pending.pop() {
125            let target = self.map[&r];
126            let body = match self.substitutions.remove(&r) {
127                Some(body) => body,
128                None => match self.source.get(r).map_err(core_error)? {
129                    Object::Stream(s) => self.copy_stream(&s)?,
130                    other => self.copy(&other)?,
131                },
132            };
133            self.writer.fill(target, body)?;
134        }
135        Ok(())
136    }
137
138    /// The whole reachable graph from the source catalog; returns the
139    /// new root ref. Substitutions apply.
140    pub fn document(&mut self) -> Result<ObjRef> {
141        let root = self
142            .source
143            .xref()
144            .trailer
145            .get_ref("Root")
146            .ok_or(Error::MissingRoot)?;
147        let new_root = self.reference(root);
148        self.finish()?;
149        Ok(new_root)
150    }
151
152    /// Page `index` as a self-contained object under `parent`
153    /// (target-space): old `/Parent` replaced with `parent`, effective
154    /// `/Resources` and `/MediaBox` materialized, `/Rotate` when non-zero,
155    /// `/CropBox` when it differs from the media box, `/Type /Page` always
156    /// present. Returns the page's new ref. The source `/Parent` is dropped
157    /// before translation, so none of the source's page tree (siblings,
158    /// ancestors) rides along as unreachable objects in the target.
159    ///
160    /// ISO 32000 gives a page exactly one parent, so every call gets its
161    /// own page object: importing the same source index again (for a
162    /// second parent, or a repeat in an assembled document) never reuses
163    /// an earlier call's object, even though the resources and content
164    /// beneath keep deduping through this `Importer`'s map. The one
165    /// exception is the very first time a given indirect source page is
166    /// seen: that call keeps the page's source-graph identity (so an
167    /// `/Annots` `/P` back-reference elsewhere in the graph resolves to
168    /// this same object rather than a duplicate). Pages inlined into
169    /// `/Kids` (no object of their own) always get a fresh object.
170    pub fn page(&mut self, index: usize, parent: ObjRef) -> Result<ObjRef> {
171        let page = self.source.page(index).map_err(core_error)?;
172        let mut dict = page.dict().clone();
173        dict.remove("Parent");
174        dict.insert(name("Type"), Object::Name(name("Page")));
175        dict.insert(name("Resources"), Object::Dict(page.resources.clone()));
176        dict.insert(name("MediaBox"), rect_array(page.media_box));
177        if page.rotate != 0 {
178            dict.insert(name("Rotate"), Object::Int(i64::from(page.rotate)));
179        }
180        if page.crop_box != page.media_box {
181            dict.insert(name("CropBox"), rect_array(page.crop_box));
182        }
183        let mut translated = self.copy_dict(&dict)?;
184        translated.insert(name("Parent"), Object::Ref(parent));
185        let target = match page.object_ref() {
186            Some(r) if !self.map.contains_key(&r) => {
187                let target = self.reference(r);
188                self.substitute(r, Object::Dict(translated));
189                target
190            }
191            _ => {
192                let target = self.writer.reserve();
193                self.writer.fill(target, Object::Dict(translated))?;
194                target
195            }
196        };
197        self.finish()?;
198        Ok(target)
199    }
200}
201
202/// A rectangle as a PDF `[x0 y0 x1 y1]` array of reals.
203pub(crate) fn rect_array(rect: Rect) -> Object {
204    Object::Array(
205        [rect.x0, rect.y0, rect.x1, rect.y1]
206            .iter()
207            .map(|v| Object::Real(widen(*v)))
208            .collect(),
209    )
210}
211
212/// `value` widened to `f64` through its own shortest round-trip decimal,
213/// rather than a raw bit widening: a plain `f64::from(f32)` keeps every
214/// bit of the `f32`'s binary value, which usually needs far more decimal
215/// digits to print than the `f32` itself means (`0.1_f32` widens to
216/// `0.10000000149011612`, not `0.1`). Formatting the `f32` first and
217/// reparsing produces the `f64` an `Object::Real` should hold: the one
218/// whose decimal digits match what the source number meant.
219fn widen(value: f32) -> f64 {
220    value
221        .to_string()
222        .parse()
223        .expect("a float's own Display output parses back as a float")
224}
225
226fn name(text: &str) -> Name {
227    Name(text.to_string())
228}
229
230#[cfg(test)]
231mod tests {
232    use pdfboss_core::Rect;
233
234    use super::*;
235    use crate::ser::serialize_object;
236
237    fn real_at(array: &Object, index: usize) -> String {
238        let Object::Array(items) = array else {
239            panic!("expected an array, got {array:?}");
240        };
241        let mut out = Vec::new();
242        serialize_object(&items[index], &mut out).expect("a real serializes");
243        String::from_utf8(out).expect("real syntax is ASCII")
244    }
245
246    #[test]
247    fn rect_array_keeps_the_f32_shortest_decimal() {
248        let rect = Rect::new(0.1, 0.0, 300.0, 400.0);
249        let array = rect_array(rect);
250        assert_eq!(real_at(&array, 0), "0.1");
251    }
252}