Skip to main content

otf_pdf_doc/
document.rs

1//! A document opened for reading.
2
3use std::collections::HashSet;
4use std::sync::{Arc, OnceLock};
5
6use otf_pdf_syntax::{
7    Diagnostic, Dict, Error, File, LimitKind, ObjId, Object, OpenOptions, Result, SharedSource,
8    Source, Version,
9};
10
11use crate::pages::{Page, collect_pages};
12use crate::store::Store;
13use crate::text::decode_text_string;
14
15/// The document information dictionary (§14.3.3), decoded.
16#[derive(Debug, Clone, Default, PartialEq)]
17pub struct Info {
18    /// `/Title`.
19    pub title: Option<String>,
20    /// `/Author`.
21    pub author: Option<String>,
22    /// `/Subject`.
23    pub subject: Option<String>,
24    /// `/Keywords`.
25    pub keywords: Option<String>,
26    /// `/Creator`.
27    pub creator: Option<String>,
28    /// `/Producer`.
29    pub producer: Option<String>,
30    /// `/CreationDate`, as written (`D:YYYYMMDDHHmmSSOHH'mm`).
31    pub creation_date: Option<String>,
32    /// `/ModDate`, as written.
33    pub mod_date: Option<String>,
34    /// `/Trapped`, as written.
35    pub trapped: Option<String>,
36}
37
38/// One outline (bookmark) item.
39#[derive(Debug, Clone, PartialEq)]
40pub struct OutlineItem {
41    /// The item's id.
42    pub id: Option<ObjId>,
43    /// `/Title`, decoded.
44    pub title: String,
45    /// `/Dest`, resolved one level, if present.
46    pub dest: Option<Object>,
47    /// `/A`, the action, resolved one level, if present.
48    pub action: Option<Dict>,
49    /// Whether the item starts open (`/Count` positive).
50    pub open: bool,
51    /// Child items, in order.
52    pub children: Vec<OutlineItem>,
53    /// Other entries of the item (`/C` colour, `/F` style, `/SE`), kept so
54    /// rewriting the outline preserves them.
55    pub extra: Dict,
56}
57
58/// An embedded file from the `/EmbeddedFiles` name tree.
59#[derive(Debug, Clone, PartialEq)]
60pub struct Attachment {
61    /// The key in the name tree.
62    pub name: String,
63    /// `/UF` or `/F` from the file specification.
64    pub filename: Option<String>,
65    /// `/Desc`.
66    pub description: Option<String>,
67    /// `/Subtype` of the embedded file stream (a MIME type, `#2F`-escaped).
68    pub mime_type: Option<String>,
69    /// `/Params /Size`, the declared uncompressed size.
70    pub size: Option<u64>,
71    /// The embedded file stream.
72    pub stream: Option<ObjId>,
73}
74
75/// A document opened for reading.
76#[derive(Debug)]
77pub struct Document {
78    pub(crate) store: Store,
79    pages: OnceLock<std::result::Result<Arc<Vec<Page>>, Error>>,
80}
81
82impl Document {
83    /// Opens a document from `source`.
84    pub fn open(source: impl Source + Send + Sync + 'static, options: OpenOptions) -> Result<Self> {
85        Self::open_shared(Arc::new(source), options)
86    }
87
88    /// Opens a document from a shared source.
89    pub fn open_shared(source: SharedSource, options: OpenOptions) -> Result<Self> {
90        let file = File::open(source, options)?;
91        let root = file.trailer().get(b"Root").cloned().unwrap_or_default();
92        file.resolve_dict(&root).ok_or(Error::NoRoot)?;
93        Ok(Self {
94            store: Store::new(file),
95            pages: OnceLock::new(),
96        })
97    }
98
99    /// The base file as read, without edits.
100    pub fn file(&self) -> &File {
101        self.store.file()
102    }
103
104    /// The object store: the base file plus edits.
105    pub fn store(&self) -> &Store {
106        &self.store
107    }
108
109    /// Mutable access to the store for low-level edits. Cached derived data
110    /// (the page list) is dropped, since any edit may change it.
111    pub fn store_mut(&mut self) -> &mut Store {
112        self.invalidate();
113        &mut self.store
114    }
115
116    /// Drops cached derived data after an edit.
117    pub(crate) fn invalidate(&mut self) {
118        self.pages = OnceLock::new();
119    }
120
121    /// The catalog's id, if it is an indirect object (it always is in a
122    /// valid file).
123    pub fn catalog_id(&self) -> Option<ObjId> {
124        self.store.trailer().get_ref(b"Root")
125    }
126
127    /// The document catalog, with edits.
128    pub fn catalog(&self) -> Dict {
129        self.store
130            .trailer()
131            .get(b"Root")
132            .and_then(|r| self.store.resolve_dict(r))
133            .unwrap_or_default()
134    }
135
136    /// The PDF version: the header's, or the catalog's `/Version` when that
137    /// is later (§7.2.2).
138    pub fn version(&self) -> Option<Version> {
139        let header = self.file().header().map(|h| h.version);
140        let catalog = self
141            .catalog()
142            .get_name(b"Version")
143            .and_then(|v| otf_pdf_syntax::find_header(&[b"%PDF-", v].concat()[..]).ok())
144            .map(|h| h.version);
145        match (header, catalog) {
146            (Some(h), Some(c)) => Some(h.max(c)),
147            (h, c) => h.or(c),
148        }
149    }
150
151    /// Repairs made while reading, so far.
152    pub fn diagnostics(&self) -> Vec<Diagnostic> {
153        self.file().diagnostics()
154    }
155
156    /// Whether the file is encrypted.
157    pub fn is_encrypted(&self) -> bool {
158        self.file().security().is_some()
159    }
160
161    /// The pages, in order. Computed once.
162    pub fn pages(&self) -> Result<Arc<Vec<Page>>> {
163        self.pages
164            .get_or_init(|| collect_pages(&self.store, &self.catalog()).map(Arc::new))
165            .clone()
166    }
167
168    /// The number of pages.
169    pub fn page_count(&self) -> Result<usize> {
170        self.pages().map(|p| p.len())
171    }
172
173    /// The document information dictionary, decoded.
174    pub fn info(&self) -> Info {
175        let Some(dict) = self
176            .store
177            .trailer()
178            .get(b"Info")
179            .and_then(|i| self.store.resolve_dict(i))
180        else {
181            return Info::default();
182        };
183        let text = |key: &[u8]| match self.store.resolve(dict.get(key)?) {
184            Object::String(s) => Some(decode_text_string(&s.bytes)),
185            Object::Name(n) => Some(String::from_utf8_lossy(n.as_bytes()).into_owned()),
186            _ => None,
187        };
188        Info {
189            title: text(b"Title"),
190            author: text(b"Author"),
191            subject: text(b"Subject"),
192            keywords: text(b"Keywords"),
193            creator: text(b"Creator"),
194            producer: text(b"Producer"),
195            creation_date: text(b"CreationDate"),
196            mod_date: text(b"ModDate"),
197            trapped: text(b"Trapped"),
198        }
199    }
200
201    /// The catalog's XMP metadata packet, decoded, if present.
202    pub fn xmp_metadata(&self) -> Result<Option<Vec<u8>>> {
203        let Some(meta) = self.catalog().get(b"Metadata").cloned() else {
204            return Ok(None);
205        };
206        let Object::Stream(stream) = self.store.resolve(&meta) else {
207            return Ok(None);
208        };
209        Ok(Some(self.store.decode_stream(&stream)?.data))
210    }
211
212    /// The outline (bookmark) tree.
213    pub fn outlines(&self) -> Result<Vec<OutlineItem>> {
214        let Some(root) = self
215            .catalog()
216            .get(b"Outlines")
217            .and_then(|o| self.store.resolve_dict(o))
218        else {
219            return Ok(Vec::new());
220        };
221        let mut seen = HashSet::new();
222        let mut budget = self.store.limits().max_objects as usize;
223        self.outline_children(&root, 0, &mut seen, &mut budget)
224    }
225
226    fn outline_children(
227        &self,
228        parent: &Dict,
229        depth: u32,
230        seen: &mut HashSet<ObjId>,
231        budget: &mut usize,
232    ) -> Result<Vec<OutlineItem>> {
233        if depth > self.store.limits().max_depth {
234            return Err(Error::Limit(LimitKind::Depth));
235        }
236        let mut items = Vec::new();
237        let mut next = parent.get(b"First").cloned();
238        while let Some(node) = next.take() {
239            let id = node.as_ref();
240            // Cycles through /Next or /First end the walk there.
241            if id.is_some_and(|id| !seen.insert(id)) {
242                break;
243            }
244            *budget = budget
245                .checked_sub(1)
246                .ok_or(Error::Limit(LimitKind::Objects))?;
247            self.store.limits().charge(1)?;
248            let Some(dict) = self.store.resolve_dict(&node) else {
249                break;
250            };
251            let title = match self
252                .store
253                .resolve(dict.get(b"Title").unwrap_or(&Object::Null))
254            {
255                Object::String(s) => decode_text_string(&s.bytes),
256                _ => String::new(),
257            };
258            let children = self.outline_children(&dict, depth + 1, seen, budget)?;
259            let mut extra = dict.clone();
260            for key in [
261                &b"Title"[..],
262                b"Parent",
263                b"Prev",
264                b"Next",
265                b"First",
266                b"Last",
267                b"Count",
268                b"Dest",
269                b"A",
270            ] {
271                extra.remove(key);
272            }
273            items.push(OutlineItem {
274                id,
275                extra,
276                title,
277                dest: dict.get(b"Dest").map(|d| self.store.resolve(d)),
278                action: dict.get(b"A").and_then(|a| self.store.resolve_dict(a)),
279                open: dict.get_int(b"Count").is_some_and(|c| c > 0),
280                children,
281            });
282            next = dict.get(b"Next").cloned();
283        }
284        Ok(items)
285    }
286
287    /// The embedded files listed in the `/EmbeddedFiles` name tree.
288    pub fn attachments(&self) -> Result<Vec<Attachment>> {
289        let Some(tree) = self
290            .catalog()
291            .get(b"Names")
292            .and_then(|n| self.store.resolve_dict(n))
293            .and_then(|names| names.get(b"EmbeddedFiles").cloned())
294        else {
295            return Ok(Vec::new());
296        };
297        let entries = name_tree(&self.store, &tree)?;
298        Ok(entries
299            .into_iter()
300            .filter_map(|(key, value)| {
301                let spec = self.store.resolve_dict(&value)?;
302                let text = |k: &[u8]| match self.store.resolve(spec.get(k)?) {
303                    Object::String(s) => Some(decode_text_string(&s.bytes)),
304                    _ => None,
305                };
306                let ef = spec.get(b"EF").and_then(|e| self.store.resolve_dict(e));
307                let stream_ref = ef
308                    .as_ref()
309                    .and_then(|ef| ef.get(b"UF").or_else(|| ef.get(b"F")))
310                    .and_then(Object::as_ref);
311                let stream_dict = stream_ref.and_then(|r| self.store.resolve_dict(&Object::Ref(r)));
312                let params = stream_dict
313                    .as_ref()
314                    .and_then(|d| d.get(b"Params"))
315                    .and_then(|p| self.store.resolve_dict(p));
316                Some(Attachment {
317                    name: decode_text_string(&key),
318                    filename: text(b"UF").or_else(|| text(b"F")),
319                    description: text(b"Desc"),
320                    mime_type: stream_dict
321                        .as_ref()
322                        .and_then(|d| d.get_name(b"Subtype"))
323                        .map(|n| String::from_utf8_lossy(n).into_owned()),
324                    size: params
325                        .and_then(|p| p.get_int(b"Size"))
326                        .and_then(|s| u64::try_from(s).ok()),
327                    stream: stream_ref,
328                })
329            })
330            .collect())
331    }
332
333    /// The decoded contents of an attachment.
334    pub fn attachment_data(&self, attachment: &Attachment) -> Result<Vec<u8>> {
335        let id = attachment.stream.ok_or(Error::WrongType {
336            expected: "embedded file stream",
337            found: "null",
338        })?;
339        if let Some(sec) = self.file().security()
340            && !sec.is_authenticated()
341        {
342            return Err(Error::InvalidPassword);
343        }
344        let obj = self.store.get(id)?;
345        let stream = obj.as_stream().ok_or(Error::WrongType {
346            expected: "stream",
347            found: obj.type_name(),
348        })?;
349        Ok(self.store.decode_stream(stream)?.data)
350    }
351}
352
353/// Every (key, value) pair of a name tree (§7.9.6), in tree order.
354pub fn name_tree(file: &Store, root: &Object) -> Result<Vec<(Vec<u8>, Object)>> {
355    let mut out = Vec::new();
356    let mut seen = HashSet::new();
357    let mut stack = vec![(root.clone(), 0u32)];
358    while let Some((node, depth)) = stack.pop() {
359        if depth > file.limits().max_depth {
360            return Err(Error::Limit(LimitKind::Depth));
361        }
362        if node.as_ref().is_some_and(|id| !seen.insert(id)) {
363            continue;
364        }
365        file.limits().charge(1)?;
366        let Some(dict) = file.resolve_dict(&node) else {
367            continue;
368        };
369        if let Some(names) = dict.get(b"Names").map(|n| file.resolve(n)) {
370            for pair in names.as_array().unwrap_or_default().chunks(2) {
371                if let [key, value] = pair
372                    && let Object::String(k) = file.resolve(key)
373                {
374                    out.push((k.bytes, value.clone()));
375                }
376            }
377        }
378        if let Some(kids) = dict.get(b"Kids").map(|k| file.resolve(k)) {
379            for kid in kids.as_array().unwrap_or_default().iter().rev() {
380                stack.push((kid.clone(), depth + 1));
381            }
382        }
383    }
384    Ok(out)
385}