Skip to main content

pdfboss_core/
document.rs

1//! The document model: loading, object resolution with caching, the
2//! lazily-flattened page tree with attribute inheritance, and document
3//! metadata.
4
5use crate::hash::{FastMap, FastSet};
6use std::cell::{OnceCell, RefCell};
7use std::path::Path;
8use std::rc::Rc;
9use std::sync::Arc;
10
11use crate::crypt::{direct_crypt_filters, Decryptor};
12use crate::date::Date;
13use crate::elements::Span;
14use crate::error::{Error, Result};
15use crate::filters;
16use crate::geom::Rect;
17use crate::object::{Dict, ObjRef, Object, Stream};
18use crate::objstm;
19use crate::parser::{Parser, Resolve};
20use crate::source::{block_on, AsyncObjectSource, Immediate};
21use crate::tree::Entries;
22use crate::xref::{load_xref, Xref, XrefEntry};
23
24/// Page-tree traversal depth cap.
25const MAX_TREE_DEPTH: usize = 256;
26
27/// A loaded PDF document.
28pub struct Document {
29    data: Arc<Vec<u8>>,
30    version: (u8, u8),
31    xref: Arc<Xref>,
32    /// Interior cache of fetched indirect objects.
33    cache: RefCell<FastMap<(u32, u16), Rc<Object>>>,
34    /// Object numbers currently being parsed, guarding re-entrant fetches
35    /// (e.g. a stream whose `/Length` refers back to the stream itself).
36    loading: RefCell<FastSet<u32>>,
37    /// Decoded object streams, keyed by their stream object number, so a
38    /// stream is decompressed and its header parsed at most once even when
39    /// many compressed objects are read from it.
40    objstms: RefCell<FastMap<u32, Rc<objstm::ObjStm>>>,
41    /// Present when the file uses the Standard security handler (RC4 or AES)
42    /// and opens under the empty user password; decrypts strings and stream
43    /// data as objects are loaded from the file.
44    decryptor: Option<Decryptor>,
45    /// The flattened page tree, built lazily on the first page access so that
46    /// merely opening a document (or reading its page count) never parses
47    /// every page dictionary. See [`Document::pages`]. Shared so a
48    /// [`Document::fork`] does not re-walk the tree.
49    pages: OnceCell<Arc<Vec<PageRec>>>,
50}
51
52/// A document's immutable core, detached from its single-threaded caches:
53/// the file bytes, the merged cross-reference table, the decryption key
54/// material, and the flattened page tree. `Send + Sync` (asserted in this
55/// module's tests), which the [`Document`] deliberately is not — this is
56/// the value that crosses a thread boundary, one fresh document
57/// materializing from it on each side. See [`Document::seed`],
58/// [`Document::from_seed`] and [`map_pages`].
59pub struct DocumentSeed {
60    data: Arc<Vec<u8>>,
61    version: (u8, u8),
62    xref: Arc<Xref>,
63    decryptor: Option<Decryptor>,
64    pages: Arc<Vec<PageRec>>,
65}
66
67impl Clone for DocumentSeed {
68    fn clone(&self) -> DocumentSeed {
69        DocumentSeed {
70            data: Arc::clone(&self.data),
71            version: self.version,
72            xref: Arc::clone(&self.xref),
73            decryptor: self.decryptor.clone(),
74            pages: Arc::clone(&self.pages),
75        }
76    }
77}
78
79impl DocumentSeed {
80    /// Number of pages in the flattened page tree the seed carries: what
81    /// every document built from it reports, without materializing one.
82    pub fn page_count(&self) -> usize {
83        self.pages.len()
84    }
85}
86
87/// The flattened, inheritance-applied record for one page.
88///
89/// Covers ISO 32000-1 §7.7.3.4.
90struct PageRec {
91    obj_ref: Option<ObjRef>,
92    media_box: Rect,
93    crop_box: Rect,
94    bleed_box: Rect,
95    trim_box: Rect,
96    art_box: Rect,
97    rotate: i32,
98    resources: Dict,
99    dict: Dict,
100}
101
102/// Attributes inherited down the page tree (ISO 32000 §7.7.3.4).
103#[derive(Clone, Default)]
104struct Inherited {
105    resources: Option<Dict>,
106    media_box: Option<Rect>,
107    crop_box: Option<Rect>,
108    rotate: Option<i32>,
109}
110
111/// Parses the `%PDF-x.y` header, scanning the first 1 KiB; absent or
112/// malformed headers default to version 1.4.
113///
114/// Covers ISO 32000-1 §7.5.2 and Annex I.
115fn parse_version(data: &[u8]) -> (u8, u8) {
116    try_parse_version(data).unwrap_or((1, 4))
117}
118
119/// Covers ISO 32000-1 §7.5.2.
120fn try_parse_version(data: &[u8]) -> Option<(u8, u8)> {
121    let window = &data[..data.len().min(1024)];
122    let pos = memchr::memmem::find(window, b"%PDF-")?;
123    let rest = &window[pos + 5..];
124    let (major, used) = read_version_component(rest)?;
125    if rest.get(used) != Some(&b'.') {
126        return None;
127    }
128    let (minor, _) = read_version_component(&rest[used + 1..])?;
129    Some((major, minor))
130}
131
132/// Reads a run of 1–3 ASCII digits as a `u8`, returning the value and the
133/// number of bytes consumed.
134fn read_version_component(bytes: &[u8]) -> Option<(u8, usize)> {
135    let end = bytes
136        .iter()
137        .position(|b| !b.is_ascii_digit())
138        .unwrap_or(bytes.len());
139    if end == 0 || end > 3 {
140        return None;
141    }
142    let value = std::str::from_utf8(&bytes[..end]).ok()?.parse().ok()?;
143    Some((value, end))
144}
145
146/// Normalizes a `/Rotate` value to one of {0, 90, 180, 270}; values that
147/// are not multiples of 90 fall back to 0 (lenient).
148fn normalize_rotation(deg: i32) -> i32 {
149    let r = deg.rem_euclid(360);
150    if r % 90 == 0 {
151        r
152    } else {
153        0
154    }
155}
156
157impl Document {
158    /// Loads a document from bytes: locates the `%PDF-x.y` header (scanning
159    /// the first 1 KiB, defaulting to 1.4), loads the xref, and sets up
160    /// decryption for files using the Standard security handler (RC4 or AES)
161    /// under the empty user password (password-protected files yield
162    /// [`Error::Encrypted`]).
163    ///
164    /// The page tree is **not** walked here: it is flattened lazily on the
165    /// first page access, so opening a document (or reading `page_count`) does
166    /// not parse every page dictionary.
167    pub fn load(data: Vec<u8>) -> Result<Document> {
168        Document::load_with_password(data, "")
169    }
170
171    /// [`Document::load`] with the password that opens the file, accepted as
172    /// either the user or the owner password. The empty string is the
173    /// transparent empty-user-password case [`Document::load`] always
174    /// handles; a password the file does not accept yields
175    /// [`Error::Encrypted`]. Non-ASCII passwords are tried UTF-8 encoded
176    /// and, for the legacy RC4/AES-128 revisions, Latin-1 encoded as well,
177    /// covering both encodings real files use.
178    pub fn load_with_password(data: Vec<u8>, password: &str) -> Result<Document> {
179        let version = parse_version(&data);
180        let xref = load_xref(&data)?;
181        let mut doc = Document {
182            data: Arc::new(data),
183            version,
184            xref: Arc::new(xref),
185            cache: RefCell::new(FastMap::default()),
186            loading: RefCell::new(FastSet::default()),
187            objstms: RefCell::new(FastMap::default()),
188            decryptor: None,
189            pages: OnceCell::new(),
190        };
191        if doc
192            .xref
193            .trailer
194            .get("Encrypt")
195            .is_some_and(|o| !o.is_null())
196        {
197            doc.setup_decryption(password)?;
198        }
199        Ok(doc)
200    }
201
202    /// Reads the file at `path` and loads it via
203    /// [`Document::load_with_password`].
204    pub fn open_with_password(path: impl AsRef<Path>, password: &str) -> Result<Document> {
205        Document::load_with_password(std::fs::read(path)?, password)
206    }
207
208    /// Configures decryption for an encrypted file. Supports the Standard
209    /// security handler with RC4 (`/V` 1–2), AESV2 (`/V` 4) and AESV3 (`/V` 5)
210    /// with `password` as the user or owner password (the empty string is
211    /// the transparent empty-user-password case); a password the file does
212    /// not accept is reported as [`Error::Encrypted`]. Must run before any
213    /// content object is fetched, and reads `/Encrypt` and `/ID` while
214    /// decryption is still off (those values are stored unencrypted).
215    fn setup_decryption(&mut self, password: &str) -> Result<()> {
216        let enc_obj = self
217            .xref
218            .trailer
219            .get("Encrypt")
220            .cloned()
221            .unwrap_or(Object::Null);
222        let enc = self.resolve(&enc_obj)?;
223        let enc_dict = enc.as_dict().ok_or(Error::Encrypted)?;
224        // Only the strings of an encryption dictionary must be direct, so
225        // the crypt filters may sit in objects of their own.
226        let enc_dict = direct_crypt_filters(enc_dict, |r| self.get(r).ok());
227        let id0: Vec<u8> = self
228            .xref
229            .trailer
230            .get("ID")
231            .and_then(Object::as_array)
232            .and_then(<[Object]>::first)
233            .and_then(Object::as_str_bytes)
234            .unwrap_or(&[])
235            .to_vec();
236        match Decryptor::from_standard_with_password_str(&enc_dict, &id0, password) {
237            Some(dec) => {
238                self.decryptor = Some(dec);
239                // Objects fetched while resolving /Encrypt were cached without
240                // decryption; drop them so they are re-read through the
241                // decrypting path if referenced again.
242                self.cache.borrow_mut().clear();
243                Ok(())
244            }
245            None => Err(Error::Encrypted),
246        }
247    }
248
249    /// Reads the file at `path` and loads it via [`Document::load`].
250    pub fn open(path: impl AsRef<Path>) -> Result<Document> {
251        Document::load(std::fs::read(path)?)
252    }
253
254    /// The PDF version from the header, e.g. `(1, 7)`.
255    pub fn version(&self) -> (u8, u8) {
256        self.version
257    }
258
259    /// Raw bytes of the loaded file.
260    pub fn bytes(&self) -> &[u8] {
261        &self.data
262    }
263
264    /// The merged cross-reference table and trailer.
265    pub fn xref(&self) -> &Xref {
266        &self.xref
267    }
268
269    /// True when the trailer carries a non-null `/Encrypt` entry: the file
270    /// declares encryption, whether or not this handle decrypted it.
271    pub fn is_encrypted(&self) -> bool {
272        self.xref()
273            .trailer
274            .get("Encrypt")
275            .is_some_and(|o| !o.is_null())
276    }
277
278    /// True when the file is encrypted and not opened with a working
279    /// password: [`Document::is_encrypted`] but no [`Decryptor`] was
280    /// configured, so object strings and stream data would decode as
281    /// ciphertext rather than the document's actual content.
282    pub fn is_locked(&self) -> bool {
283        self.is_encrypted() && self.decryptor.is_none()
284    }
285
286    /// Fetches an indirect object by reference (xref lookup, object-stream
287    /// indirection, cached). A generation mismatch between the request and
288    /// the file is tolerated (lenient).
289    pub fn get(&self, r: ObjRef) -> Result<Object> {
290        if let Some(cached) = self.cache.borrow().get(&(r.num, r.gen)) {
291            return Ok((**cached).clone());
292        }
293        if !self.loading.borrow_mut().insert(r.num) {
294            return Err(Error::CircularReference(r.num));
295        }
296        let result = self.load_object(r);
297        self.loading.borrow_mut().remove(&r.num);
298        let object = result?;
299        self.cache
300            .borrow_mut()
301            .insert((r.num, r.gen), Rc::new(object.clone()));
302        Ok(object)
303    }
304
305    /// Uncached fetch: parses the object at its file offset or extracts it
306    /// from its containing object stream.
307    ///
308    /// Covers ISO 32000-1 §7.5.3.
309    fn load_object(&self, r: ObjRef) -> Result<Object> {
310        match self.xref.get(r.num) {
311            None | Some(XrefEntry::Free) => Err(Error::ObjectNotFound(r.num, r.gen)),
312            Some(XrefEntry::InFile { offset, .. }) => {
313                let offset = usize::try_from(offset)
314                    .ok()
315                    .filter(|&o| o < self.data.len())
316                    .ok_or(Error::ObjectNotFound(r.num, r.gen))?;
317                self.object_at_spanned(offset).map(|parsed| parsed.1)
318            }
319            Some(XrefEntry::InStream { stream_num, index }) => {
320                self.load_from_object_stream(stream_num, index)
321            }
322        }
323    }
324
325    /// Parses the indirect object at `offset`, applying decryption, and
326    /// reports the byte range consumed (`N G obj … endobj`).
327    pub(crate) fn object_at_spanned(&self, offset: usize) -> Result<(ObjRef, Object, Span)> {
328        let mut parser = Parser::at(&self.data, offset);
329        let (r, mut object) = parser.parse_indirect(self)?;
330        // Objects stored directly in the file carry encrypted strings and
331        // stream data; decrypt with this object's key. (Objects living in
332        // object streams are decrypted with their container.)
333        if let Some(dec) = &self.decryptor {
334            dec.decrypt_object(&mut object, r.num, r.gen);
335        }
336        Ok((r, object, Span::new(offset as u64, parser.pos() as u64)))
337    }
338
339    /// The decoded, header-parsed object stream `stream_num`, built at most
340    /// once and cached.
341    pub(crate) fn objstm_handle(&self, stream_num: u32) -> Result<Rc<objstm::ObjStm>> {
342        if let Some(stm) = self.objstms.borrow().get(&stream_num) {
343            return Ok(Rc::clone(stm));
344        }
345        let container = self.get(ObjRef {
346            num: stream_num,
347            gen: 0,
348        })?;
349        let stream = container.as_stream().ok_or_else(|| Error::TypeMismatch {
350            expected: "stream",
351            found: type_name(&container),
352        })?;
353        let n = self
354            .resolve(stream.dict.get("N").unwrap_or(&Object::Null))?
355            .as_int()
356            .and_then(|v| usize::try_from(v).ok())
357            .ok_or(Error::MissingKey("N"))?;
358        let first = self
359            .resolve(stream.dict.get("First").unwrap_or(&Object::Null))?
360            .as_int()
361            .and_then(|v| usize::try_from(v).ok())
362            .ok_or(Error::MissingKey("First"))?;
363        let decoded = self.stream_data(stream)?;
364        let stm = Rc::new(objstm::ObjStm::parse(decoded, n, first)?);
365        self.objstms
366            .borrow_mut()
367            .insert(stream_num, Rc::clone(&stm));
368        Ok(stm)
369    }
370
371    /// Extracts a compressed object from the object stream `stream_num`.
372    fn load_from_object_stream(&self, stream_num: u32, index: u32) -> Result<Object> {
373        self.objstm_handle(stream_num)?.object(index)
374    }
375
376    /// Chases reference chains with a depth guard of
377    /// [`MAX_RESOLVE_DEPTH`](crate::source::MAX_RESOLVE_DEPTH) (beyond that:
378    /// [`Error::CircularReference`], naming the last reference followed); a
379    /// reference to a missing or unreadable object resolves to `Null`
380    /// (lenient).
381    ///
382    /// The loop is [`crate::source::resolve_sync_with`], shared with the
383    /// provided [`crate::source::ObjectSource::resolve`], so the two cannot
384    /// drift apart. It reaches `Document::get` through this type's
385    /// `ObjectSource` implementation, which forwards to it unchanged.
386    pub fn resolve(&self, o: &Object) -> Result<Object> {
387        crate::source::resolve_sync_with(self, o)
388    }
389
390    /// Decodes a stream's data through its filter chain, resolving indirect
391    /// filter parameters against this document.
392    pub fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
393        filters::decode_stream(s, self)
394    }
395
396    /// The flattened page tree, built once on first access.
397    fn pages(&self) -> &[PageRec] {
398        self.pages.get_or_init(|| Arc::new(self.flatten_pages()))
399    }
400
401    /// The document's shareable core: everything immutable, nothing cached.
402    /// Forces the page tree first, so no document built from the seed
403    /// re-walks it.
404    pub fn seed(&self) -> DocumentSeed {
405        DocumentSeed {
406            data: Arc::clone(&self.data),
407            version: self.version,
408            xref: Arc::clone(&self.xref),
409            decryptor: self.decryptor.clone(),
410            pages: Arc::clone(self.pages.get_or_init(|| Arc::new(self.flatten_pages()))),
411        }
412    }
413
414    /// A handle to the same document for another thread: share a
415    /// [`DocumentSeed`] across the thread boundary — the seed is `Send` and
416    /// `Sync`; the document, whose caches are single-threaded by design, is
417    /// neither — and materialize one of these per thread.
418    ///
419    /// The immutable core is shared — the file bytes, the merged
420    /// cross-reference table, the decryption key material, and the flattened
421    /// page tree — while the interior caches start fresh, private to this
422    /// document. That is the whole design: per-page work is independent and
423    /// lock-free precisely because nothing mutable is shared, so N documents
424    /// on N threads contend on nothing.
425    ///
426    /// [`map_pages`] is the ready-made consumer.
427    pub fn from_seed(seed: DocumentSeed) -> Document {
428        Document {
429            data: seed.data,
430            version: seed.version,
431            xref: seed.xref,
432            cache: RefCell::new(FastMap::default()),
433            loading: RefCell::new(FastSet::default()),
434            objstms: RefCell::new(FastMap::default()),
435            decryptor: seed.decryptor,
436            pages: OnceCell::from(seed.pages),
437        }
438    }
439
440    /// [`Document::seed`] and [`Document::from_seed`] in one step, for a
441    /// fork used on the calling thread.
442    pub fn fork(&self) -> Document {
443        Document::from_seed(self.seed())
444    }
445
446    /// Number of pages.
447    ///
448    /// Reports the page tree's declared `/Count` — the same value mature
449    /// engines return — without walking the tree, so it is cheap on an
450    /// otherwise-untouched document. If `/Count` is absent or implausible the
451    /// tree is flattened and its true (lenient, cycle- and depth-guarded)
452    /// length is returned instead. Once the tree has been flattened for any
453    /// reason, that flattened length is authoritative.
454    pub fn page_count(&self) -> usize {
455        if let Some(pages) = self.pages.get() {
456            return pages.len();
457        }
458        if let Some(count) = self.declared_page_count() {
459            return count;
460        }
461        self.pages().len()
462    }
463
464    /// Number of pages the page tree actually holds: the flattened tree's
465    /// length, walking the tree on first use. This is the count
466    /// [`Document::page`] indexes into; it differs from
467    /// [`Document::page_count`] only while the tree is unwalked and its
468    /// declared `/Count` is wrong.
469    pub fn flattened_page_count(&self) -> usize {
470        self.pages().len()
471    }
472
473    /// Reads the page tree root's `/Count` cheaply (Root → `/Pages` →
474    /// `/Count`) without descending into `/Kids`. Returns `None` when the
475    /// entry is missing, non-integer, negative, or larger than the file could
476    /// possibly hold (a corrupt count), so the caller falls back to a real
477    /// walk.
478    ///
479    /// Covers ISO 32000-1 §7.7.3 and §7.7.3.2.
480    fn declared_page_count(&self) -> Option<usize> {
481        let root = self.xref.trailer.get("Root")?;
482        let catalog = self.resolve(root).ok()?;
483        let pages = self.resolve(catalog.as_dict()?.get("Pages")?).ok()?;
484        let count = usize::try_from(self.int_value(pages.as_dict()?, "Count")?).ok()?;
485        // A page occupies at least a handful of bytes on disk, so a count that
486        // exceeds the file length is corrupt: fall back to walking the tree.
487        (count <= self.data.len()).then_some(count)
488    }
489
490    /// The page at 0-based `index`.
491    pub fn page(&self, index: usize) -> Result<Page> {
492        let pages = self.pages();
493        let rec = pages
494            .get(index)
495            .ok_or(Error::PageNotFound(index, pages.len()))?;
496        Ok(Page {
497            index,
498            media_box: rec.media_box,
499            crop_box: rec.crop_box,
500            bleed_box: rec.bleed_box,
501            trim_box: rec.trim_box,
502            art_box: rec.art_box,
503            rotate: rec.rotate,
504            resources: rec.resources.clone(),
505            dict: rec.dict.clone(),
506            obj_ref: rec.obj_ref,
507        })
508    }
509
510    /// Document metadata from the trailer `/Info` dictionary (lenient:
511    /// absent or malformed entries are simply `None`).
512    ///
513    /// Covers ISO 32000-1 §14.3.3, §7.9.4 and §8.11.4.3.
514    pub fn metadata(&self) -> Metadata {
515        let meta = Metadata::default();
516        let Some(info) = self.xref.trailer.get("Info") else {
517            return meta;
518        };
519        let Ok(info) = self.resolve(info) else {
520            return meta;
521        };
522        let Some(dict) = info.as_dict() else {
523            return meta;
524        };
525        block_on(metadata_with(&Immediate(self), dict))
526    }
527
528    /// The document's optional-content visibility as a viewer shows it on
529    /// screen: the default configuration (ISO 32000-1 §8.11.4.3) with the
530    /// View usage application (§8.11.4.4) applied, or `None` when the
531    /// catalog declares no `/OCProperties` — no optional content, everything
532    /// visible. Computed per call from a handful of object reads.
533    ///
534    /// Covers ISO 32000-1 §8.11.4.2.
535    pub fn oc_state(&self) -> Option<crate::oc::OcState> {
536        block_on(crate::oc::OcState::load_with(
537            &Immediate(self),
538            &self.xref.trailer,
539        ))
540    }
541
542    /// The document's optional-content visibility under one usage
543    /// application event (ISO 32000-1 §8.11.4.4), or under the default
544    /// configuration alone for `None`; `None` in the result when the catalog
545    /// declares no `/OCProperties`. See [`crate::OcState::load_for_with`].
546    ///
547    /// Covers ISO 32000-1 §8.11.4.5.
548    pub fn oc_state_for(&self, event: Option<crate::oc::OcEvent>) -> Option<crate::oc::OcState> {
549        block_on(crate::oc::OcState::load_for_with(
550            &Immediate(self),
551            &self.xref.trailer,
552            event,
553        ))
554    }
555
556    /// The document's optional content groups (ISO 32000-1 §8.11.2.1) in
557    /// `/OCGs` order, each with its state under `event`; empty without
558    /// `/OCProperties`.
559    ///
560    /// Covers ISO 32000-1 §8.11.4.2.
561    pub fn optional_content_groups(
562        &self,
563        event: Option<crate::oc::OcEvent>,
564    ) -> Vec<crate::oc::OcGroup> {
565        block_on(crate::oc::optional_content_groups_with(
566            &Immediate(self),
567            &self.xref.trailer,
568            event,
569        ))
570    }
571
572    /// The document's structure tree (ISO 32000-1 §14.7), or `None` when the
573    /// catalog names no `/StructTreeRoot`. Loaded per call: the root, its
574    /// role and class maps and every entry of its parent tree, so a caller
575    /// walking the pages loads it once and forks it per worker (its
576    /// `Clone`, see [`map_pages_forking`]); the tree ranks a page's marked
577    /// content on request and keeps what each walk read for the next.
578    pub fn structure_tree(&self) -> Option<crate::structure::StructureTree> {
579        block_on(crate::structure::StructureTree::load_with(
580            &Immediate(self),
581            &self.xref.trailer,
582        ))
583    }
584
585    /// A page's content items in structure-tree order (ISO 32000-1
586    /// §14.7.4): the marked-content sequences of its content stream and
587    /// the annotations the tree holds through object references, ranked
588    /// together, so each annotation sits where §14.8.2.3.2 puts it in the
589    /// page's content order. Empty for a page the tree does not reach, or
590    /// a document without a tree.
591    pub fn content_items(&self, page: &Page) -> Vec<crate::structure::PlacedItem> {
592        let Some(tree) = self.structure_tree() else {
593            return Vec::new();
594        };
595        block_on(tree.content_items_with(&Immediate(self), page))
596    }
597
598    /// The object that `key` names in the catalog's `tree` (ISO 32000-1
599    /// §7.7.4), resolved: a destination for `NameTree::Dests`, a file
600    /// specification for `NameTree::EmbeddedFiles`. `None` when the
601    /// document has no such tree or the tree has no such name.
602    pub fn named(&self, tree: crate::names::NameTree, key: &[u8]) -> Option<Object> {
603        block_on(crate::names::named_with(
604            &Immediate(self),
605            &self.xref.trailer,
606            tree,
607            key,
608        ))
609    }
610
611    /// Every name in the catalog's `tree` with the object it names,
612    /// unresolved, in tree order; empty when the document has no such tree.
613    pub fn names(&self, tree: crate::names::NameTree) -> Vec<(Vec<u8>, Object)> {
614        block_on(crate::names::names_with(
615            &Immediate(self),
616            &self.xref.trailer,
617            tree,
618        ))
619    }
620
621    /// The destination `name` designates (ISO 32000-1 §12.3.2.3), from the
622    /// `/Names` `/Dests` tree or the catalog's `/Dests` dictionary.
623    pub fn named_destination(&self, name: &[u8]) -> Option<crate::destination::Destination> {
624        block_on(crate::destination::named_destination_with(
625            &Immediate(self),
626            &self.xref.trailer,
627            name,
628        ))
629    }
630
631    /// Every named destination of the document, sorted by name.
632    pub fn named_destinations(&self) -> Vec<(Vec<u8>, crate::destination::Destination)> {
633        block_on(crate::destination::named_destinations_with(
634            &Immediate(self),
635            &self.xref.trailer,
636        ))
637    }
638
639    /// The destination a `/Dest` or `/D` value denotes: an explicit array,
640    /// a name or string looked up among the named destinations, or a
641    /// dictionary whose `/D` holds one of those (ISO 32000-1 §12.3.2).
642    pub fn destination(&self, value: &Object) -> Option<crate::destination::Destination> {
643        block_on(crate::destination::destination_value_with(
644            &Immediate(self),
645            &self.xref.trailer,
646            value,
647        ))
648    }
649
650    /// The document outline (ISO 32000-1 §12.3.3): the top-level items with
651    /// their descendants, in display order; empty when there is none.
652    pub fn outline(&self) -> Vec<crate::outline::OutlineItem> {
653        block_on(crate::outline::outline_with(
654            &Immediate(self),
655            &self.xref.trailer,
656        ))
657    }
658
659    /// The document's page labelling ranges (ISO 32000-1 §12.4.2), sorted
660    /// by first page; `None` when the catalog has no `/PageLabels`.
661    pub fn page_labels(&self) -> Option<Vec<crate::page_label::PageLabel>> {
662        block_on(crate::page_label::page_labels_with(
663            &Immediate(self),
664            &self.xref.trailer,
665        ))
666    }
667
668    /// The language the catalog declares for the document's text
669    /// (`/Lang`, ISO 32000-1 §14.9.2), or `None` when it declares none.
670    pub fn language(&self) -> Option<String> {
671        block_on(crate::language::language_with(
672            &Immediate(self),
673            &self.xref.trailer,
674        ))
675    }
676
677    /// The document's language as a parsed identifier (ISO 32000-1
678    /// §14.9.2.2), `None` when the catalog declares none or the tag is not
679    /// well formed.
680    pub fn language_tag(&self) -> Option<crate::language::LanguageTag> {
681        crate::language::LanguageTag::parse(&self.language()?)
682    }
683
684    /// The developer extensions the catalog declares (ISO 32000-1 §7.12),
685    /// sorted by prefix; empty when the catalog declares none.
686    pub fn extensions(&self) -> Vec<crate::extension::DeveloperExtension> {
687        block_on(crate::extension::extensions_with(
688            &Immediate(self),
689            &self.xref.trailer,
690        ))
691    }
692
693    /// The catalog's viewer preferences (ISO 32000-1 §12.2), `None` when
694    /// the catalog declares none.
695    pub fn viewer_preferences(&self) -> Option<crate::viewer_preferences::ViewerPreferences> {
696        block_on(crate::viewer_preferences::viewer_preferences_with(
697            &Immediate(self),
698            &self.xref.trailer,
699        ))
700    }
701
702    /// The linearization parameter dictionary (ISO 32000-1 Annex F.3) as
703    /// written, `None` when the file's first object is not one. It may be
704    /// stale after an appended update; [`Document::is_linearized`] tells.
705    pub fn linearization(&self) -> Option<crate::linearization::Linearization> {
706        crate::linearization::linearization_dictionary(&self.data)
707    }
708
709    /// Whether the file is linearized and its parameter dictionary still
710    /// applies, which Table F.1 ties to `/L` naming the file's actual
711    /// length.
712    ///
713    /// Covers ISO 32000-1 Annex F.3.
714    pub fn is_linearized(&self) -> bool {
715        self.linearization()
716            .is_some_and(|record| record.is_current(self.data.len() as u64))
717    }
718
719    /// The catalog's output intents (ISO 32000-1 §14.11.5) in array order,
720    /// empty without `/OutputIntents`.
721    pub fn output_intents(&self) -> Vec<crate::output_intent::OutputIntent> {
722        block_on(crate::output_intent::output_intents_with(
723            &Immediate(self),
724            &self.xref.trailer,
725        ))
726    }
727
728    /// The catalog's page-piece dictionary (ISO 32000-1 §14.5): one record
729    /// per product, empty without `/PieceInfo`.
730    pub fn piece_info(&self) -> Vec<crate::piece_info::PagePiece> {
731        block_on(crate::piece_info::document_piece_info_with(
732            &Immediate(self),
733            &self.xref.trailer,
734        ))
735    }
736
737    /// A page's page-piece dictionary (ISO 32000-1 §14.5): one record per
738    /// product, empty without `/PieceInfo` on the page.
739    pub fn page_piece_info(&self, page: &Page) -> Vec<crate::piece_info::PagePiece> {
740        block_on(crate::piece_info::piece_info_with(
741            &Immediate(self),
742            page.dict(),
743        ))
744    }
745
746    /// A page's thumbnail image (ISO 32000-1 §12.3.4), `None` when the
747    /// page has no `/Thumb` stream.
748    pub fn thumbnail(&self, page: &Page) -> Option<crate::thumbnail::Thumbnail> {
749        block_on(crate::thumbnail::thumbnail_with(&Immediate(self), page))
750    }
751
752    /// The catalog's article threads (ISO 32000-1 §12.4.3) in array order,
753    /// empty without `/Threads`.
754    pub fn articles(&self) -> Vec<crate::article::ArticleThread> {
755        block_on(crate::article::articles_with(
756            &Immediate(self),
757            &self.xref.trailer,
758        ))
759    }
760
761    /// The beads on a page in drawing order (ISO 32000-1 §12.4.3), empty
762    /// without `/B`.
763    pub fn page_beads(&self, page: &Page) -> Vec<ObjRef> {
764        block_on(crate::article::page_beads_with(&Immediate(self), page))
765    }
766
767    /// A page's presentation entries (ISO 32000-1 §12.4.4), `None` when
768    /// the page has neither `/Dur` nor a `/Trans` dictionary.
769    pub fn presentation(&self, page: &Page) -> Option<crate::presentation::Presentation> {
770        block_on(crate::presentation::presentation_with(
771            &Immediate(self),
772            page,
773        ))
774    }
775
776    /// The viewports of a page's `/VP` array (ISO 32000-1 §12.9), empty
777    /// without one.
778    pub fn viewports(&self, page: &Page) -> Vec<crate::measure::Viewport> {
779        block_on(crate::measure::viewports_with(&Immediate(self), page))
780    }
781
782    /// A page's separation dictionary (ISO 32000-1 §14.11.4), `None` when
783    /// the page has no `/SeparationInfo` naming a colorant.
784    pub fn separation_info(&self, page: &Page) -> Option<crate::separation_info::SeparationInfo> {
785        block_on(crate::separation_info::separation_info_with(
786            &Immediate(self),
787            page,
788        ))
789    }
790
791    /// The annotations of a page's `/Annots` array (ISO 32000-1 §12.5.2),
792    /// in order, with their actions read as data.
793    pub fn annotations(&self, page: &Page) -> Vec<crate::annotation::Annotation> {
794        block_on(crate::annotation::annotations_with(
795            &Immediate(self),
796            &self.xref.trailer,
797            page,
798        ))
799    }
800
801    /// The actions a page's `/AA` dictionary fires when it opens and closes
802    /// (ISO 32000-1 §12.6.3), empty without one.
803    pub fn page_additional_actions(&self, page: &Page) -> Vec<crate::annotation::TriggeredAction> {
804        block_on(crate::annotation::page_additional_actions_with(
805            &Immediate(self),
806            &self.xref.trailer,
807            page,
808        ))
809    }
810
811    /// The actions the catalog's `/AA` dictionary fires around closing,
812    /// saving and printing (ISO 32000-1 §12.6.3), empty without one.
813    pub fn additional_actions(&self) -> Vec<crate::annotation::TriggeredAction> {
814        block_on(crate::annotation::document_additional_actions_with(
815            &Immediate(self),
816            &self.xref.trailer,
817        ))
818    }
819
820    /// The action an `/A` or `/AA` value holds, with its `/Next` chain
821    /// (ISO 32000-1 §12.6.2); `None` when the value is no action
822    /// dictionary.
823    pub fn action(&self, value: &Object) -> Option<crate::annotation::Action> {
824        block_on(crate::annotation::action_with(
825            &Immediate(self),
826            &self.xref.trailer,
827            value,
828        ))
829    }
830
831    /// The permission handlers of the catalog's `/Perms` dictionary (ISO
832    /// 32000-1 §12.8.4), `None` without one.
833    pub fn permission_handlers(&self) -> Option<crate::permission::PermissionHandlers> {
834        block_on(crate::permission::permission_handlers_with(
835            &Immediate(self),
836            &self.xref.trailer,
837        ))
838    }
839
840    /// The requirements the catalog's `/Requirements` array lists (ISO
841    /// 32000-1 §12.10.1), empty without one.
842    pub fn requirements(&self) -> Vec<crate::requirement::Requirement> {
843        block_on(crate::requirement::requirements_with(
844            &Immediate(self),
845            &self.xref.trailer,
846        ))
847    }
848
849    /// The catalog's legal attestation dictionary (ISO 32000-1 §12.8.5),
850    /// `None` without one.
851    pub fn legal_attestation(&self) -> Option<crate::legal::LegalAttestation> {
852        block_on(crate::legal::legal_attestation_with(
853            &Immediate(self),
854            &self.xref.trailer,
855        ))
856    }
857
858    /// The document's interactive form dictionary (ISO 32000-1 §12.7.2),
859    /// `None` when the catalog has no `/AcroForm`.
860    pub fn interactive_form(&self) -> Option<crate::form::InteractiveForm> {
861        block_on(crate::form::interactive_form_with(
862            &Immediate(self),
863            &self.xref.trailer,
864        ))
865    }
866
867    /// Every field of the document's interactive form (ISO 32000-1
868    /// §12.7.3), depth first from the root fields; empty without a form.
869    pub fn form_fields(&self) -> Vec<crate::form::FormField> {
870        block_on(crate::form::form_fields_with(
871            &Immediate(self),
872            &self.xref.trailer,
873        ))
874    }
875
876    /// The label the page at `index` shows, or `None` when the document
877    /// has no page labels or no such page.
878    pub fn page_label(&self, index: usize) -> Option<String> {
879        if index >= self.page_count() {
880            return None;
881        }
882        let ranges = self.page_labels()?;
883        Some(crate::page_label::page_label(&ranges, index))
884    }
885
886    /// The document-level embedded files (ISO 32000-1 §7.11.4): every entry
887    /// of the catalog's `/EmbeddedFiles` name tree, in tree order.
888    pub fn embedded_files(&self) -> Vec<crate::embedded_file::EmbeddedFile> {
889        block_on(crate::embedded_file::embedded_files_with(
890            &Immediate(self),
891            &self.xref.trailer,
892        ))
893    }
894
895    /// The decoded bytes of one embedded file.
896    ///
897    /// # Errors
898    ///
899    /// `MissingKey("EF")` when the file specification embeds no stream,
900    /// and the stream's own decoding errors.
901    pub fn embedded_file_data(&self, file: &crate::embedded_file::EmbeddedFile) -> Result<Vec<u8>> {
902        block_on(crate::embedded_file::embedded_file_data_with(
903            &Immediate(self),
904            file,
905        ))
906    }
907
908    /// The decoded bytes of the stream a file specification embeds, such
909    /// as a file attachment annotation's (ISO 32000-1 §7.11.4).
910    ///
911    /// # Errors
912    ///
913    /// `MissingKey("EF")` when the specification embeds no stream, and the
914    /// stream's own decoding errors.
915    pub fn file_spec_data(&self, spec: &crate::embedded_file::FileSpec) -> Result<Vec<u8>> {
916        block_on(crate::embedded_file::file_spec_data_with(
917            &Immediate(self),
918            spec,
919        ))
920    }
921
922    /// Flattens the page tree by iterative depth-first traversal of `/Kids`
923    /// with a visited-reference cycle guard and a depth cap, applying
924    /// attribute inheritance. Any structural problem simply truncates or
925    /// skips (lenient) — this never fails.
926    ///
927    /// Covers ISO 32000-1 §14.11.2, §7.7.2, §7.7.3, §7.7.3.2, §7.7.3.3, §7.7.3.4 and §8.3.2.2.
928    fn flatten_pages(&self) -> Vec<PageRec> {
929        let mut pages = Vec::new();
930        let Some(root) = self.xref.trailer.get("Root") else {
931            return pages;
932        };
933        let Ok(catalog) = self.resolve(root) else {
934            return pages;
935        };
936        let Some(tree_root) = catalog.as_dict().and_then(|d| d.get("Pages")) else {
937            return pages;
938        };
939        let mut visited: FastSet<ObjRef> = FastSet::default();
940        let mut stack: Vec<(Object, Inherited, usize)> =
941            vec![(tree_root.clone(), Inherited::default(), 0)];
942        while let Some((node, mut inherited, depth)) = stack.pop() {
943            if depth > MAX_TREE_DEPTH {
944                continue;
945            }
946            let node_ref = if let Object::Ref(r) = node {
947                Some(r)
948            } else {
949                None
950            };
951            if let Some(r) = node_ref {
952                if !visited.insert(r) {
953                    continue; // cycle: this node was already traversed
954                }
955            }
956            let Ok(resolved) = self.resolve(&node) else {
957                continue;
958            };
959            let Some(dict) = resolved.as_dict() else {
960                continue;
961            };
962            if let Some(res) = self.dict_value(dict, "Resources") {
963                inherited.resources = Some(res);
964            }
965            if let Some(mb) = self.rect_value(dict, "MediaBox") {
966                inherited.media_box = Some(mb);
967            }
968            if let Some(cb) = self.rect_value(dict, "CropBox") {
969                inherited.crop_box = Some(cb);
970            }
971            if let Some(rot) = self.int_value(dict, "Rotate") {
972                inherited.rotate = Some(rot);
973            }
974            let is_page = dict.get_name("Type").is_some_and(|n| n.0 == "Page");
975            let kids = if is_page {
976                None
977            } else {
978                self.array_value(dict, "Kids")
979            };
980            match kids {
981                Some(kids) => {
982                    // Reverse push so pop order matches document order.
983                    for kid in kids.iter().rev() {
984                        stack.push((kid.clone(), inherited.clone(), depth + 1));
985                    }
986                }
987                None => {
988                    // BleedBox, TrimBox and ArtBox are not inheritable
989                    // (ISO 32000 §7.7.3.3, Table 30): read them from the
990                    // leaf dictionary only.
991                    let bleed = self.rect_value(dict, "BleedBox");
992                    let trim = self.rect_value(dict, "TrimBox");
993                    let art = self.rect_value(dict, "ArtBox");
994                    pages.push(make_page_rec(
995                        node_ref,
996                        dict.clone(),
997                        &inherited,
998                        bleed,
999                        trim,
1000                        art,
1001                    ));
1002                }
1003            }
1004        }
1005        pages
1006    }
1007
1008    /// Resolves `dict[key]` to a dictionary, if present and well-formed.
1009    fn dict_value(&self, dict: &Dict, key: &str) -> Option<Dict> {
1010        self.resolve(dict.get(key)?).ok()?.as_dict().cloned()
1011    }
1012
1013    /// Resolves `dict[key]` to an array, if present and well-formed.
1014    fn array_value(&self, dict: &Dict, key: &str) -> Option<Vec<Object>> {
1015        match self.resolve(dict.get(key)?).ok()? {
1016            Object::Array(items) => Some(items),
1017            _ => None,
1018        }
1019    }
1020
1021    /// Resolves `dict[key]` to an integer (reals truncate, lenient).
1022    fn int_value(&self, dict: &Dict, key: &str) -> Option<i32> {
1023        let v = self.resolve(dict.get(key)?).ok()?.as_f64()?;
1024        if v.is_finite() {
1025            Some(v as i32)
1026        } else {
1027            None
1028        }
1029    }
1030
1031    /// Resolves `dict[key]` to a normalized rectangle: a four-number array
1032    /// whose elements may themselves be references.
1033    ///
1034    /// Covers ISO 32000-1 §7.9.5.
1035    fn rect_value(&self, dict: &Dict, key: &str) -> Option<Rect> {
1036        let items = self.array_value(dict, key)?;
1037        if items.len() != 4 {
1038            return None;
1039        }
1040        let mut coords = [0.0f32; 4];
1041        for (slot, item) in coords.iter_mut().zip(&items) {
1042            let n = self.resolve(item).ok()?.as_f64()?;
1043            if !n.is_finite() {
1044                return None;
1045            }
1046            *slot = n as f32;
1047        }
1048        Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
1049    }
1050}
1051
1052/// Applies `work` to every page of `doc` in parallel and returns the
1053/// results in page order.
1054///
1055/// Per-page work over a `Document` — text extraction, rasterization — is
1056/// independent and CPU-bound, so this fans it out over
1057/// `std::thread::available_parallelism()` threads, each holding its own
1058/// [`Document::fork`]: the immutable core is shared, the caches are private,
1059/// and the workers contend on nothing.
1060///
1061/// Workers pull page indexes from a shared counter rather than a static
1062/// stride. This is not a style choice: pages vary wildly in cost, and on a
1063/// machine with heterogeneous cores a static partition ends with the fast
1064/// cores idle while a slow one finishes its stripe — measured as the
1065/// difference between 3.9x and 5.9x on twelve cores. A counter keeps every
1066/// worker busy until the pages run out.
1067///
1068/// A single-page document, or a single-core machine, runs inline on the
1069/// calling thread with no fork and no thread.
1070pub fn map_pages<T, F>(doc: &Document, work: F) -> Vec<Result<T>>
1071where
1072    T: Send + Sync,
1073    F: Fn(&Document, &Page) -> Result<T> + Send + Sync,
1074{
1075    map_pages_forking(doc, || (), |(), doc, page| work(doc, page))
1076}
1077
1078/// [`map_pages`] with state forked once per worker: `fork` runs on each
1079/// worker before its first page (on the calling thread when the work runs
1080/// inline) and `work` gets that worker's copy with every page it handles.
1081/// For state a page walk fills as it goes, such as a
1082/// [`crate::StructureTree`], whose fork is the same tree over an empty
1083/// cache, this keeps what one page read for the pages the same worker
1084/// takes next, without the workers sharing a lock.
1085pub fn map_pages_forking<W, T, K, F>(doc: &Document, fork: K, work: F) -> Vec<Result<T>>
1086where
1087    T: Send + Sync,
1088    K: Fn() -> W + Sync,
1089    F: Fn(&W, &Document, &Page) -> Result<T> + Send + Sync,
1090{
1091    let count = doc.pages().len();
1092    let workers = std::thread::available_parallelism()
1093        .map(std::num::NonZeroUsize::get)
1094        .unwrap_or(1)
1095        .min(count);
1096    if workers <= 1 {
1097        let state = fork();
1098        return (0..count)
1099            .map(|i| doc.page(i).and_then(|page| work(&state, doc, &page)))
1100            .collect();
1101    }
1102
1103    let seed = doc.seed();
1104    let next = std::sync::atomic::AtomicUsize::new(0);
1105    let slots: Vec<std::sync::OnceLock<Result<T>>> =
1106        (0..count).map(|_| std::sync::OnceLock::new()).collect();
1107    std::thread::scope(|scope| {
1108        for _ in 0..workers {
1109            // The seed crosses the thread boundary; the document — whose
1110            // caches are single-threaded by design — materializes inside,
1111            // and so does the worker's fork of the state.
1112            let seed = seed.clone();
1113            let next = &next;
1114            let slots = &slots;
1115            let fork = &fork;
1116            let work = &work;
1117            scope.spawn(move || {
1118                let worker = Document::from_seed(seed);
1119                let state = fork();
1120                loop {
1121                    let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1122                    if i >= count {
1123                        break;
1124                    }
1125                    let outcome = worker.page(i).and_then(|page| work(&state, &worker, &page));
1126                    let _ = slots[i].set(outcome);
1127                }
1128            });
1129        }
1130    });
1131    slots
1132        .into_iter()
1133        .map(|slot| {
1134            slot.into_inner()
1135                .unwrap_or_else(|| Err(Error::Other("page index was never dispatched".to_owned())))
1136        })
1137        .collect()
1138}
1139
1140/// Builds the final page record from a leaf dictionary and its inherited
1141/// attributes. The defaults live in [`Page::from_tree_attrs`] — the one
1142/// implementation of page defaulting, shared with the asynchronous API —
1143/// and this only reshapes its output into the index-less cache record.
1144///
1145/// Covers ISO 32000-1 §7.7.3.3.
1146fn make_page_rec(
1147    obj_ref: Option<ObjRef>,
1148    dict: Dict,
1149    inherited: &Inherited,
1150    bleed_box: Option<Rect>,
1151    trim_box: Option<Rect>,
1152    art_box: Option<Rect>,
1153) -> PageRec {
1154    let page = Page::from_tree_attrs(
1155        0,
1156        inherited.resources.clone(),
1157        inherited.media_box,
1158        inherited.crop_box,
1159        bleed_box,
1160        trim_box,
1161        art_box,
1162        inherited.rotate,
1163        dict,
1164        obj_ref,
1165    );
1166    PageRec {
1167        obj_ref: page.obj_ref,
1168        media_box: page.media_box,
1169        crop_box: page.crop_box,
1170        bleed_box: page.bleed_box,
1171        trim_box: page.trim_box,
1172        art_box: page.art_box,
1173        rotate: page.rotate,
1174        resources: page.resources,
1175        dict: page.dict,
1176    }
1177}
1178
1179/// Human-readable object type name for error messages.
1180pub(crate) fn type_name(o: &Object) -> &'static str {
1181    match o {
1182        Object::Null => "null",
1183        Object::Bool(_) => "boolean",
1184        Object::Int(_) => "integer",
1185        Object::Real(_) => "real",
1186        Object::String(_) => "string",
1187        Object::Name(_) => "name",
1188        Object::Array(_) => "array",
1189        Object::Dict(_) => "dictionary",
1190        Object::Stream(_) => "stream",
1191        Object::Ref(_) => "reference",
1192    }
1193}
1194
1195impl Resolve for Document {
1196    fn resolve_ref(&self, r: ObjRef) -> Option<Object> {
1197        self.get(r).ok()
1198    }
1199}
1200
1201/// Forwards to the inherent methods, so reading a document through the trait
1202/// is bit-identical to reading it directly. `resolve` is forwarded too, even
1203/// though the provided implementation is now the same shared chase
1204/// ([`crate::source::resolve_sync_with`]) that the inherent method delegates
1205/// to: routing it through the inherent method keeps the two entry points a
1206/// single call, so they cannot drift should `Document::resolve` ever grow
1207/// document-specific behaviour.
1208impl crate::source::ObjectSource for Document {
1209    fn get(&self, r: ObjRef) -> Result<Object> {
1210        Document::get(self, r)
1211    }
1212
1213    fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
1214        Document::stream_data(self, s)
1215    }
1216
1217    fn resolve(&self, o: &Object) -> Result<Object> {
1218        Document::resolve(self, o)
1219    }
1220}
1221
1222/// Document information from the trailer `/Info` dictionary. Only present,
1223/// well-formed entries are populated. The two dates hold the text as
1224/// written; [`Metadata::creation_date_parsed`] and
1225/// [`Metadata::mod_date_parsed`] read them as [`Date`]s.
1226#[derive(Debug, Clone, Default, PartialEq, Eq)]
1227pub struct Metadata {
1228    pub title: Option<String>,
1229    pub author: Option<String>,
1230    pub subject: Option<String>,
1231    pub keywords: Option<String>,
1232    pub creator: Option<String>,
1233    pub producer: Option<String>,
1234    pub creation_date: Option<String>,
1235    pub mod_date: Option<String>,
1236}
1237
1238/// The document information entries `dict` holds (ISO 32000-1 §14.3.3,
1239/// Table 317), each decoded as a text string. The same reader serves the
1240/// trailer's `/Info` dictionary and an article thread's `/I` (§12.4.3).
1241///
1242/// Covers ISO 32000-1 §7.9.2.2 and §14.3.3.
1243pub async fn metadata_with<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Metadata {
1244    let entries = Entries { src, dict };
1245    Metadata {
1246        title: entries.text("Title").await,
1247        author: entries.text("Author").await,
1248        subject: entries.text("Subject").await,
1249        keywords: entries.text("Keywords").await,
1250        creator: entries.text("Creator").await,
1251        producer: entries.text("Producer").await,
1252        creation_date: entries.text("CreationDate").await,
1253        mod_date: entries.text("ModDate").await,
1254    }
1255}
1256
1257impl Metadata {
1258    /// `/CreationDate` as a date (ISO 32000-1 §7.9.4), `None` when absent
1259    /// or not a date string; the text as written stays in `creation_date`.
1260    ///
1261    /// Covers ISO 32000-1 §7.9.4.
1262    pub fn creation_date_parsed(&self) -> Option<Date> {
1263        Date::parse_pdf(self.creation_date.as_deref()?)
1264    }
1265
1266    /// `/ModDate` as a date, like [`Metadata::creation_date_parsed`].
1267    ///
1268    /// Covers ISO 32000-1 §7.9.4.
1269    pub fn mod_date_parsed(&self) -> Option<Date> {
1270        Date::parse_pdf(self.mod_date.as_deref()?)
1271    }
1272}
1273
1274/// A single page with inherited attributes already applied.
1275///
1276/// Defaults: `media_box` falls back to US Letter (612x792) when absent or
1277/// invalid, `crop_box` falls back to (and is intersected with) `media_box`,
1278/// `bleed_box`, `trim_box` and `art_box` fall back to `crop_box` (and are
1279/// clipped to `media_box`) per ISO 32000 §14.11.2, and `rotate` is
1280/// normalized to one of {0, 90, 180, 270}. Every construction path
1281/// normalizes `rotate` — [`Document::page`] and [`Page::from_parts`] alike —
1282/// so the invariant holds however a `Page` was built. `rotate` is a public
1283/// field, so a caller may still overwrite it afterwards; [`Page::size`]
1284/// reads it modulo a full turn and so stays correct if they do.
1285///
1286/// All five boxes are in unrotated PDF user space: [`Page::size`] swaps
1287/// width and height under a quarter-turn `/Rotate`, the boxes never do.
1288#[derive(Clone)]
1289pub struct Page {
1290    /// 0-based page index.
1291    pub index: usize,
1292    pub media_box: Rect,
1293    pub crop_box: Rect,
1294    pub bleed_box: Rect,
1295    pub trim_box: Rect,
1296    pub art_box: Rect,
1297    pub rotate: i32,
1298    /// The page's (inherited) `/Resources` dictionary.
1299    pub resources: Dict,
1300    dict: Dict,
1301    obj_ref: Option<ObjRef>,
1302}
1303
1304impl Page {
1305    /// Builds a page from already-resolved attributes.
1306    ///
1307    /// [`Document::page`] resolves page-tree inheritance itself; this is for
1308    /// a caller that has done that resolution some other way — notably the
1309    /// asynchronous API, which flattens the page tree while reading it — and
1310    /// needs the same [`Page`] type back.
1311    ///
1312    /// `rotate` is normalized here to one of `{0, 90, 180, 270}` exactly as
1313    /// [`Document::page`] normalizes it, so a caller may pass a raw `/Rotate`
1314    /// straight through: a negative or over-a-full-turn multiple of 90 is
1315    /// reduced, and a value that is not a multiple of 90 falls back to 0
1316    /// (lenient). The caller does not have to pre-normalize, and the [`Page`]
1317    /// invariant holds whichever constructor built it.
1318    ///
1319    /// `bleed_box`, `trim_box` and `art_box` are set to `crop_box` — their
1320    /// ISO 32000 §14.11.2 default. A caller that resolved real values
1321    /// overwrites the public fields afterwards.
1322    ///
1323    /// Covers ISO 32000-1 §14.11.2.
1324    pub fn from_parts(
1325        index: usize,
1326        media_box: Rect,
1327        crop_box: Rect,
1328        rotate: i32,
1329        resources: Dict,
1330        dict: Dict,
1331        obj_ref: Option<ObjRef>,
1332    ) -> Page {
1333        Page {
1334            index,
1335            media_box,
1336            crop_box,
1337            bleed_box: crop_box,
1338            trim_box: crop_box,
1339            art_box: crop_box,
1340            rotate: normalize_rotation(rotate),
1341            resources,
1342            dict,
1343            obj_ref,
1344        }
1345    }
1346
1347    /// The default media box for a page that declares none: US Letter
1348    /// (612 by 792 points). Public because the default is part of the
1349    /// observable contract — a caller comparing two APIs' pages needs to
1350    /// know which rectangle "the file said nothing" maps to.
1351    pub const US_LETTER: Rect = Rect::new(0.0, 0.0, 612.0, 792.0);
1352
1353    /// Builds a page from raw, possibly missing page-tree attributes,
1354    /// applying the same defaults [`Document::page`] applies (ISO 32000
1355    /// §7.7.3.3, §14.11.2): a missing or degenerate `/MediaBox` reads as
1356    /// [`Page::US_LETTER`], `/CropBox` clips to the media box and falls back
1357    /// to it, `/BleedBox`, `/TrimBox` and `/ArtBox` clip to the media box
1358    /// and fall back to the crop box, a missing `/Rotate` reads as 0 and is
1359    /// normalized, and missing `/Resources` read as empty.
1360    ///
1361    /// This is the one implementation of page defaulting. The synchronous
1362    /// tree walk routes through it, and a caller that resolved inheritance
1363    /// some other way — notably the asynchronous API, which flattens the
1364    /// page tree while reading it — gets the identical `Page` back, so the
1365    /// two APIs cannot disagree about what an attribute-less page looks
1366    /// like.
1367    #[allow(clippy::too_many_arguments)]
1368    pub fn from_tree_attrs(
1369        index: usize,
1370        resources: Option<Dict>,
1371        media_box: Option<Rect>,
1372        crop_box: Option<Rect>,
1373        bleed_box: Option<Rect>,
1374        trim_box: Option<Rect>,
1375        art_box: Option<Rect>,
1376        rotate: Option<i32>,
1377        dict: Dict,
1378        obj_ref: Option<ObjRef>,
1379    ) -> Page {
1380        let media_box = media_box
1381            .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1382            .unwrap_or(Page::US_LETTER);
1383        let crop_box = crop_box
1384            .and_then(|c| c.intersect(media_box))
1385            .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1386            .unwrap_or(media_box);
1387        let clip = |declared: Option<Rect>| {
1388            declared
1389                .and_then(|b| b.intersect(media_box))
1390                .filter(|r| r.width() > 0.0 && r.height() > 0.0)
1391                .unwrap_or(crop_box)
1392        };
1393        Page {
1394            index,
1395            media_box,
1396            crop_box,
1397            bleed_box: clip(bleed_box),
1398            trim_box: clip(trim_box),
1399            art_box: clip(art_box),
1400            rotate: normalize_rotation(rotate.unwrap_or(0)),
1401            resources: resources.unwrap_or_default(),
1402            dict,
1403            obj_ref,
1404        }
1405    }
1406
1407    /// The page's indirect object reference, when the page came from an
1408    /// indirect kid in the page tree (pages inlined directly into a `/Kids`
1409    /// array have none).
1410    pub fn object_ref(&self) -> Option<ObjRef> {
1411        self.obj_ref
1412    }
1413
1414    /// The page's decoded content: the `/Contents` stream, or all streams
1415    /// of a `/Contents` array decoded and joined with `b"\n"`. A missing
1416    /// `/Contents` yields empty content (lenient).
1417    ///
1418    /// This drives [`page_content_with`] to completion on the calling thread.
1419    /// There is one implementation of the algorithm, so this and the
1420    /// asynchronous API cannot drift apart in what they consider a page's
1421    /// content to be.
1422    pub fn content(&self, doc: &Document) -> Result<Vec<u8>> {
1423        block_on(page_content_with(Immediate(doc), self))
1424    }
1425
1426    /// Crop-box width and height, swapped when `/Rotate` is a quarter turn.
1427    ///
1428    /// Both constructors normalize `rotate`, but the field is public and so
1429    /// can be overwritten with a raw `/Rotate`; the rotation is therefore read
1430    /// modulo a full turn rather than compared against 90 and 270 exactly, so
1431    /// that -90 and 450 swap the dimensions just as 270 and 90 do. For an
1432    /// already-normalized value this is the same test as before: 0, 90, 180
1433    /// and 270 are unchanged by `rem_euclid(360)`.
1434    pub fn size(&self) -> (f32, f32) {
1435        let (w, h) = (self.crop_box.width(), self.crop_box.height());
1436        let turn = self.rotate.rem_euclid(360);
1437        if turn == 90 || turn == 270 {
1438            (h, w)
1439        } else {
1440            (w, h)
1441        }
1442    }
1443
1444    /// The raw page dictionary.
1445    pub fn dict(&self) -> &Dict {
1446        &self.dict
1447    }
1448}
1449
1450/// A page's decoded content, awaiting each fetch: the `/Contents` stream, or
1451/// all streams of a `/Contents` array decoded and joined with `b"\n"`. A
1452/// missing `/Contents` yields empty content (lenient).
1453///
1454/// This is the only implementation of that algorithm. [`Page::content`] is a
1455/// [`block_on`] wrapper over it, which is what stops the synchronous and
1456/// asynchronous APIs from disagreeing about what a page's content is.
1457///
1458/// # Choosing this signature
1459///
1460/// `src` is taken **by value** rather than by reference. A future holding
1461/// `&'a S` is `Send` but never `'static`, and the asynchronous consumers —
1462/// spawning onto a runtime, or crossing into the Python bindings — need both.
1463/// By value costs nothing in practice: an asynchronous document is an `Arc`
1464/// handle, and [`Immediate`] over a borrowed document is `Copy`.
1465///
1466/// There is deliberately **no `Send` or `Sync` bound** on `S`. Auto traits are
1467/// inferred per instantiation, so this one function yields a `Send` future over
1468/// a genuinely asynchronous source and a non-`Send` future over
1469/// `Immediate<&Document>` — which is what is wanted, because [`block_on`]
1470/// drives the latter on the calling thread and never sends it anywhere. A
1471/// `S: Sync` bound would exclude `Immediate<&Document>` outright; that is also
1472/// why this calls `src.resolve` directly rather than
1473/// [`crate::source::resolve_with`], which does require `Sync`.
1474///
1475/// The returned future is `'static` only when the caller owns the [`Page`]
1476/// inside its own `async` block — the `&Page` borrow is what otherwise
1477/// prevents it.
1478///
1479/// # Errors
1480///
1481/// Propagates whatever `src` reports for a fetch or a stream decode.
1482pub async fn page_content_with<S: AsyncObjectSource>(src: S, page: &Page) -> Result<Vec<u8>> {
1483    let Some(contents) = page.dict.get("Contents") else {
1484        return Ok(Vec::new());
1485    };
1486    match src.resolve(contents).await? {
1487        Object::Stream(ref s) => content_stream_data_with(&src, s).await,
1488        Object::Array(items) => {
1489            let mut out = Vec::new();
1490            let mut first = true;
1491            for item in &items {
1492                let part = src.resolve(item).await?;
1493                let Some(stream) = part.as_stream() else {
1494                    continue; // non-stream entries are skipped (lenient)
1495                };
1496                if !first {
1497                    out.push(b'\n');
1498                }
1499                out.extend_from_slice(&content_stream_data_with(&src, stream).await?);
1500                first = false;
1501            }
1502            Ok(out)
1503        }
1504        _ => Ok(Vec::new()),
1505    }
1506}
1507
1508/// Fetches one stream's bytes GUARANTEED DECODED — refusing the streams
1509/// whose bytes `decode_stream` leaves encoded for the image layer. This is
1510/// the fetch for **every consumer that is not an image decoder**: content
1511/// streams, font programs, `/ToUnicode` CMaps, `/CIDToGIDMap` tables —
1512/// anything that parses what it fetches.
1513///
1514/// This is [`AsyncObjectSource::stream_data`] with two refusals in front.
1515/// A stream whose trailing `/Filter` entry is an image codec (`DCTDecode`,
1516/// `JPXDecode`) fails with [`Error::UnsupportedFilter`] instead of handing
1517/// back the passthrough (ISO 32000-1 7.4.9): a raw JPEG or JPEG 2000
1518/// codestream is indistinguishable from decoded data to anything that is
1519/// not an image decoder, and a parser fed one chews binary garbage into
1520/// operators, tables, or mappings with a clean result. And a `/Filter`
1521/// that cannot be READ at all — a reference cycle, or a chain deeper than
1522/// [`crate::source::MAX_RESOLVE_DEPTH`] — is refused with the resolve
1523/// error, because a value that cannot be read might name an image codec,
1524/// and `decode_stream` would leniently return the bytes as stored. Either
1525/// refusal is the same reportable error as any other filter this library
1526/// cannot run.
1527///
1528/// One leniency is kept, deliberately: a `/Filter` that READS as `null`
1529/// (a dangling reference included — ISO 32000-1 7.3.10 makes one
1530/// equivalent to `null`) or as an unusable value says "no filter", which
1531/// is exactly what `decode_stream` does with it; the stored bytes are the
1532/// decoded bytes by that reading, and no codec name can hide in a value
1533/// that is fully visible.
1534///
1535/// Raw [`AsyncObjectSource::stream_data`] remains correct in exactly one
1536/// place: the image layer, which reads the trailing filter itself and
1537/// decodes the passthrough. A new call site that is not decoding images
1538/// belongs here instead. Same calling convention as [`page_content_with`]
1539/// (`src` by value; see that function's docs).
1540pub async fn decoded_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1541    if let Some(name) = filters::trailing_filter_checked_with(&src, &s.dict).await? {
1542        if filters::is_image_codec(&name.0) {
1543            return Err(Error::UnsupportedFilter(name.0));
1544        }
1545    }
1546    src.stream_data(s).await
1547}
1548
1549/// Fetches and decodes one CONTENT stream — page `/Contents`, a form
1550/// XObject, a Type3 CharProc, a pattern cell: anything whose bytes feed a
1551/// content parser rather than an image decoder.
1552///
1553/// The content-flavored name for [`decoded_stream_data_with`], which is
1554/// the same refusal for every non-image consumer; see it for why a
1555/// passthrough image codec must never reach a parser. For a content
1556/// stream the refusal is what turns the mislabelled stream into the same
1557/// reported skip as any other filter this library cannot run.
1558pub async fn content_stream_data_with<S: AsyncObjectSource>(src: S, s: &Stream) -> Result<Vec<u8>> {
1559    decoded_stream_data_with(src, s).await
1560}
1561
1562#[cfg(test)]
1563mod tests {
1564    use super::*;
1565    use crate::object::Name;
1566    use crate::parser::{NoResolve, Parser};
1567    use crate::xref::XrefEntry;
1568    use pdfboss_testkit::{multi_page_doc, objstm_doc, objstm_payload, simple_doc, PdfBuilder};
1569
1570    /// The synchronous accessor delegates to the asynchronous one, so the two
1571    /// cannot report different content for the same page. This asserts the
1572    /// equality directly rather than trusting the delegation to stay in place.
1573    #[test]
1574    fn async_page_content_matches_the_sync_accessor() {
1575        let doc = Document::load(multi_page_doc(&["one", "two", "three"])).expect("load");
1576        let mut saw_content = false;
1577        for index in 0..3 {
1578            let page = doc.page(index).expect("page");
1579            let direct = page.content(&doc).expect("sync content");
1580            let awaited =
1581                block_on(page_content_with(Immediate(&doc), &page)).expect("async content");
1582            assert_eq!(direct, awaited, "page {index}");
1583            saw_content |= !direct.is_empty();
1584        }
1585        // Without this the loop above would pass on three empty vectors.
1586        assert!(
1587            saw_content,
1588            "fixture must give at least one page real content"
1589        );
1590    }
1591
1592    /// A page `/Contents` stream whose trailing filter is an image codec is
1593    /// refused, never parsed: `decode_stream` would pass its bytes through
1594    /// STILL ENCODED (ISO 32000-1 7.4.9 reserves that for the image layer),
1595    /// and the stream below is deliberately valid operator syntax to prove
1596    /// the refusal happens on the label, not on the bytes. The error is the
1597    /// same `UnsupportedFilter` any genuinely undecodable filter raises, so
1598    /// every caller reports it identically.
1599    #[test]
1600    fn image_codec_page_contents_are_refused_not_returned() {
1601        for codec in ["JPXDecode", "DCTDecode"] {
1602            let mut b = PdfBuilder::new();
1603            b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1604            b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1605            b.object(
1606                3,
1607                "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1608            );
1609            b.stream(
1610                4,
1611                &format!("/Filter /{codec}"),
1612                b"1 0 0 rg 0 0 100 100 re f",
1613            );
1614            let doc = Document::load(b.build(1)).expect("load");
1615            let page = doc.page(0).expect("page");
1616            match page.content(&doc) {
1617                Err(Error::UnsupportedFilter(n)) => assert_eq!(n, codec),
1618                other => panic!("{codec} content must be refused, got {other:?}"),
1619            }
1620        }
1621    }
1622
1623    /// A `/Contents` whose `/Filter` is a reference CYCLE is refused as
1624    /// unreadable, never fetched: `decode_stream` cannot resolve the value
1625    /// either and would leniently return the bytes AS STORED — and a value
1626    /// nobody can read might name an image codec, so the stored bytes may
1627    /// be a passthrough codestream. The bytes below are deliberately valid
1628    /// operator syntax to prove the refusal is about the unreadable label.
1629    #[test]
1630    fn an_unreadable_filter_refuses_the_content_not_returns_it_raw() {
1631        let mut b = PdfBuilder::new();
1632        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1633        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1634        b.object(
1635            3,
1636            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R >>",
1637        );
1638        b.stream(4, "/Filter 6 0 R", b"1 0 0 rg 0 0 100 100 re f");
1639        b.object(6, "7 0 R");
1640        b.object(7, "6 0 R");
1641        let doc = Document::load(b.build(1)).expect("load");
1642        let page = doc.page(0).expect("page");
1643        assert!(
1644            page.content(&doc).is_err(),
1645            "an unreadable /Filter must refuse, not pass stored bytes"
1646        );
1647    }
1648
1649    /// The composition every page-reading algorithm actually uses: the caller
1650    /// owns its source so that its own future can be `'static`, and reaches this
1651    /// helper — which owns its source for the same reason — by handing out a
1652    /// reference. That works because `&S` is itself an `AsyncObjectSource`.
1653    #[test]
1654    fn a_shared_helper_is_reachable_from_a_caller_that_owns_its_source() {
1655        let doc = Document::load(simple_doc("Hello")).expect("load");
1656        let page = doc.page(0).expect("page");
1657
1658        let owner = Immediate(&doc);
1659        let through_reference = block_on(page_content_with(&owner, &page)).expect("async content");
1660
1661        assert_eq!(through_reference, page.content(&doc).expect("sync content"));
1662        assert!(!through_reference.is_empty());
1663    }
1664
1665    /// The seed is what crosses a thread boundary, so this is the compile
1666    /// gate for the whole fan-out design. The `Document` itself must stay
1667    /// out of this list: its caches are single-threaded by design.
1668    #[test]
1669    fn the_seed_is_shareable_across_threads() {
1670        fn assert_send_sync<T: Send + Sync>() {}
1671        assert_send_sync::<DocumentSeed>();
1672    }
1673
1674    /// A fork shares the file and the page tree but none of the caches, and
1675    /// reads identically to its parent — including through decryption, whose
1676    /// key material is part of the shared immutable core.
1677    #[test]
1678    fn a_fork_reads_exactly_what_its_parent_reads() {
1679        let doc = Document::load(pdfboss_testkit::encrypted_rc4_doc("forked secret")).unwrap();
1680        let fork = doc.fork();
1681        assert_eq!(fork.page_count(), doc.page_count());
1682        let (a, b) = (doc.page(0).unwrap(), fork.page(0).unwrap());
1683        assert_eq!(a.media_box, b.media_box);
1684        assert_eq!(
1685            a.content(&doc).expect("parent content"),
1686            b.content(&fork).expect("fork content"),
1687            "decrypted content agrees"
1688        );
1689    }
1690
1691    /// The seed's page count is the flattened tree's length: the number
1692    /// every document built from it reports, available without building one.
1693    #[test]
1694    fn the_seed_reports_the_flattened_page_count() {
1695        let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1696        let seed = doc.seed();
1697        assert_eq!(seed.page_count(), 3);
1698        assert_eq!(Document::from_seed(seed).page_count(), 3);
1699    }
1700
1701    /// The results come back in page order whatever order the workers finish
1702    /// in, and a per-page error occupies its own slot without disturbing the
1703    /// others.
1704    #[test]
1705    fn map_pages_preserves_page_order() {
1706        let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1707        let contents = map_pages(&doc, |doc, page| page.content(doc));
1708        assert_eq!(contents.len(), 3);
1709        let texts: Vec<String> = contents
1710            .into_iter()
1711            .map(|c| String::from_utf8_lossy(&c.unwrap()).into_owned())
1712            .collect();
1713        assert!(texts[0].contains("(one)"), "{}", texts[0]);
1714        assert!(texts[1].contains("(two)"), "{}", texts[1]);
1715        assert!(texts[2].contains("(three)"), "{}", texts[2]);
1716    }
1717
1718    /// The state is forked once per worker, never per page, and every page
1719    /// sees a fork: over a three-page document the forks number at most
1720    /// three and at least one, and each page's work reads the fork it got.
1721    #[test]
1722    fn map_pages_forking_forks_the_state_once_per_worker() {
1723        let doc = Document::load(multi_page_doc(&["one", "two", "three"])).unwrap();
1724        let forks = std::sync::atomic::AtomicUsize::new(0);
1725        let tags = map_pages_forking(
1726            &doc,
1727            || forks.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1,
1728            |fork, doc, page| Ok((*fork, page.content(doc)?.len())),
1729        );
1730        let forked = forks.load(std::sync::atomic::Ordering::Relaxed);
1731        assert!((1..=3).contains(&forked), "{forked} forks for three pages");
1732        for outcome in tags {
1733            let (fork, len) = outcome.unwrap();
1734            assert!((1..=forked).contains(&fork));
1735            assert!(len > 0);
1736        }
1737    }
1738
1739    /// Replaces the first occurrence of `from` with `to`. Splicing happens
1740    /// after the xref section, so byte offsets stay valid.
1741    fn replace_once(data: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
1742        let pos = memchr::memmem::find(data, from).expect("pattern present in fixture");
1743        let mut out = Vec::with_capacity(data.len() - from.len() + to.len());
1744        out.extend_from_slice(&data[..pos]);
1745        out.extend_from_slice(to);
1746        out.extend_from_slice(&data[pos + from.len()..]);
1747        out
1748    }
1749
1750    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
1751        memchr::memmem::find(haystack, needle).is_some()
1752    }
1753
1754    const FONT: &str = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
1755
1756    // Covers ISO 32000-1 §7.5.3 and §7.7.2.
1757    #[test]
1758    fn loads_simple_doc() {
1759        let doc = Document::load(simple_doc("Greetings, cosmos!")).unwrap();
1760        assert_eq!(doc.version(), (1, 7));
1761        assert_eq!(doc.page_count(), 1);
1762        let page = doc.page(0).unwrap();
1763        assert_eq!(page.index, 0);
1764        assert_eq!(page.media_box, Rect::new(0.0, 0.0, 612.0, 792.0));
1765        assert_eq!(page.crop_box, page.media_box);
1766        assert_eq!(page.rotate, 0);
1767        assert_eq!(page.size(), (612.0, 792.0));
1768        assert!(page.resources.get("Font").is_some());
1769        let content = page.content(&doc).unwrap();
1770        assert!(contains(&content, b"Greetings, cosmos!"));
1771    }
1772
1773    // Covers ISO 32000-1 §7.7.3 and §7.7.3.2.
1774    #[test]
1775    fn multi_page_ordering() {
1776        let doc = Document::load(multi_page_doc(&["alpha", "beta", "gamma"])).unwrap();
1777        assert_eq!(doc.page_count(), 3);
1778        for (i, text) in ["alpha", "beta", "gamma"].iter().enumerate() {
1779            let content = doc.page(i).unwrap().content(&doc).unwrap();
1780            assert!(
1781                contains(&content, text.as_bytes()),
1782                "page {i} should show {text}"
1783            );
1784        }
1785    }
1786
1787    #[test]
1788    fn page_index_out_of_bounds() {
1789        let doc = Document::load(simple_doc("x")).unwrap();
1790        assert!(matches!(doc.page(5), Err(Error::PageNotFound(5, 1))));
1791    }
1792
1793    #[test]
1794    fn open_reads_from_disk() {
1795        let dir = std::env::temp_dir();
1796        let path = dir.join(format!("pdfboss-doc-test-{}.pdf", std::process::id()));
1797        std::fs::write(&path, simple_doc("from disk")).unwrap();
1798        let doc = Document::open(&path).unwrap();
1799        std::fs::remove_file(&path).ok();
1800        assert_eq!(doc.page_count(), 1);
1801        let content = doc.page(0).unwrap().content(&doc).unwrap();
1802        assert!(contains(&content, b"from disk"));
1803        assert!(matches!(
1804            Document::open(dir.join("pdfboss-doc-test-missing.pdf")),
1805            Err(Error::Io(_))
1806        ));
1807    }
1808
1809    // Covers ISO 32000-1 §7.5.5.
1810    #[test]
1811    fn encrypt_in_trailer_is_rejected() {
1812        let data = replace_once(
1813            &simple_doc("secret"),
1814            b"trailer\n<< /Size",
1815            b"trailer\n<< /Encrypt 9 0 R /Size",
1816        );
1817        assert!(matches!(Document::load(data), Err(Error::Encrypted)));
1818    }
1819
1820    // Covers ISO 32000-1 §14.3.3, §7.5.5 and §7.9.2.2.
1821    #[test]
1822    fn metadata_utf16be_round_trip() {
1823        let mut b = PdfBuilder::new();
1824        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1825        b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1826        // /Title is UTF-16BE with BOM: "H\u{151}" (H + o with double acute).
1827        b.object(6, "<< /Title <FEFF00480151> /Author (plain author) >>");
1828        let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1829        let doc = Document::load(data).unwrap();
1830        let meta = doc.metadata();
1831        assert_eq!(meta.title.as_deref(), Some("H\u{151}"));
1832        assert_eq!(meta.author.as_deref(), Some("plain author"));
1833        assert_eq!(meta.subject, None);
1834        assert_eq!(meta.keywords, None);
1835        assert_eq!(meta.creation_date, None);
1836    }
1837
1838    // Covers ISO 32000-1 §7.9.4 and §14.3.3: the dates read as dates, and a
1839    // string that is not one keeps only its text.
1840    #[test]
1841    fn metadata_dates_parse_per_the_date_clause() {
1842        let mut b = PdfBuilder::new();
1843        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1844        b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
1845        // The clause's own example, and a month 20 no calendar has.
1846        b.object(
1847            6,
1848            "<< /CreationDate (D:199812231952-08'00) /ModDate (D:191020717014604) >>",
1849        );
1850        let data = replace_once(&b.build(1), b"<< /Size", b"<< /Info 6 0 R /Size");
1851        let doc = Document::load(data).unwrap();
1852        let meta = doc.metadata();
1853        assert_eq!(meta.creation_date.as_deref(), Some("D:199812231952-08'00"));
1854        assert_eq!(
1855            meta.creation_date_parsed(),
1856            Some(Date {
1857                year: 1998,
1858                month: 12,
1859                day: 23,
1860                hour: 19,
1861                minute: 52,
1862                second: 0,
1863                utc_offset_minutes: -480,
1864            })
1865        );
1866        assert_eq!(meta.mod_date.as_deref(), Some("D:191020717014604"));
1867        assert_eq!(meta.mod_date_parsed(), None, "month 20 is not a date");
1868    }
1869
1870    #[test]
1871    fn metadata_without_info_is_all_none() {
1872        let doc = Document::load(simple_doc("x")).unwrap();
1873        assert_eq!(doc.metadata(), Metadata::default());
1874    }
1875
1876    // Covers ISO 32000-1 §7.3.10 and §7.3.9.
1877    #[test]
1878    fn missing_object_resolves_to_null() {
1879        let doc = Document::load(simple_doc("x")).unwrap();
1880        let missing = Object::Ref(ObjRef { num: 99, gen: 0 });
1881        assert_eq!(doc.resolve(&missing).unwrap(), Object::Null);
1882        assert!(matches!(
1883            doc.get(ObjRef { num: 99, gen: 0 }),
1884            Err(Error::ObjectNotFound(99, 0))
1885        ));
1886    }
1887
1888    #[test]
1889    fn self_reference_is_circular() {
1890        let mut b = PdfBuilder::new();
1891        b.object(1, "<< /Type /Catalog >>");
1892        b.object(6, "6 0 R");
1893        let doc = Document::load(b.build(1)).unwrap();
1894        let loops = Object::Ref(ObjRef { num: 6, gen: 0 });
1895        assert!(matches!(
1896            doc.resolve(&loops),
1897            Err(Error::CircularReference(6))
1898        ));
1899    }
1900
1901    // Covers ISO 32000-1 §7.3.10.
1902    #[test]
1903    fn generation_mismatch_is_tolerated() {
1904        let doc = Document::load(simple_doc("x")).unwrap();
1905        let catalog = doc.get(ObjRef { num: 1, gen: 7 }).unwrap();
1906        let dict = catalog.as_dict().unwrap();
1907        assert_eq!(dict.get_name("Type").map(|n| n.0.as_str()), Some("Catalog"));
1908    }
1909
1910    // Covers ISO 32000-1 §7.5.7.
1911    #[test]
1912    fn objects_in_object_streams_are_fetched() {
1913        let mut b = PdfBuilder::new();
1914        let (dict, payload) =
1915            objstm_payload(&[(1, "<< /Type /Catalog /Pages 2 0 R >>"), (5, FONT)]);
1916        b.stream(6, &dict, &payload);
1917        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1918        b.object(
1919            3,
1920            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1921             /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1922        );
1923        b.stream(4, "", b"BT /F1 12 Tf (compressed hello) Tj ET");
1924        let doc = Document::load(b.build_xref_stream(1)).unwrap();
1925        assert_eq!(doc.page_count(), 1);
1926        let page = doc.page(0).unwrap();
1927        assert!(contains(&page.content(&doc).unwrap(), b"compressed hello"));
1928        let font = doc.get(ObjRef { num: 5, gen: 0 }).unwrap();
1929        assert_eq!(
1930            font.as_dict()
1931                .and_then(|d| d.get_name("BaseFont"))
1932                .map(|n| n.0.as_str()),
1933            Some("Helvetica")
1934        );
1935    }
1936
1937    // Covers ISO 32000-1 §7.7.3.3 and §7.8.2.
1938    #[test]
1939    fn contents_array_is_joined_with_newlines() {
1940        let mut b = PdfBuilder::new();
1941        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1942        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1943        b.object(
1944            3,
1945            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1946             /Contents [4 0 R null 5 0 R] >>",
1947        );
1948        b.stream(4, "", b"q");
1949        b.stream(5, "", b"Q");
1950        let doc = Document::load(b.build(1)).unwrap();
1951        let content = doc.page(0).unwrap().content(&doc).unwrap();
1952        assert_eq!(content, b"q\nQ", "streams joined by \\n, null skipped");
1953    }
1954
1955    // Covers ISO 32000-1 §7.7.3 and §7.7.3.4.
1956    #[test]
1957    fn inheritance_from_pages_node_and_rotate_swap() {
1958        let mut b = PdfBuilder::new();
1959        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1960        b.object(
1961            2,
1962            "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 \
1963             /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 600] >>",
1964        );
1965        b.object(3, "<< /Type /Page /Parent 2 0 R >>");
1966        b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate 270 >>");
1967        b.object(5, FONT);
1968        let doc = Document::load(b.build(1)).unwrap();
1969        assert_eq!(doc.page_count(), 2);
1970
1971        let first = doc.page(0).unwrap();
1972        assert_eq!(first.media_box, Rect::new(0.0, 0.0, 400.0, 600.0));
1973        assert_eq!(first.crop_box, first.media_box);
1974        assert!(first.resources.get("Font").is_some(), "inherited resources");
1975        assert_eq!(first.rotate, 0);
1976        assert!(
1977            first.content(&doc).unwrap().is_empty(),
1978            "no /Contents means empty content"
1979        );
1980        assert_eq!(first.size(), (400.0, 600.0));
1981
1982        let second = doc.page(1).unwrap();
1983        assert_eq!(second.rotate, 270);
1984        assert_eq!(second.size(), (600.0, 400.0), "rotate 270 swaps w/h");
1985    }
1986
1987    // Covers ISO 32000-1 §7.7.3.4 and §7.9.5.
1988    #[test]
1989    fn crop_box_intersected_and_rotate_normalized() {
1990        let mut b = PdfBuilder::new();
1991        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1992        b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>");
1993        b.object(
1994            3,
1995            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
1996             /CropBox [100 100 400 400] /Rotate 450 >>",
1997        );
1998        b.object(4, "<< /Type /Page /Parent 2 0 R /Rotate -90 >>");
1999        b.object(
2000            5,
2001            "<< /Type /Page /Parent 2 0 R /Rotate 45 /MediaBox [0 0 0 0] >>",
2002        );
2003        let doc = Document::load(b.build(1)).unwrap();
2004
2005        let clipped = doc.page(0).unwrap();
2006        assert_eq!(clipped.crop_box, Rect::new(100.0, 100.0, 200.0, 200.0));
2007        assert_eq!(clipped.rotate, 90, "450 normalizes to 90");
2008        assert_eq!(clipped.size(), (100.0, 100.0));
2009
2010        assert_eq!(doc.page(1).unwrap().rotate, 270, "-90 normalizes to 270");
2011        let odd = doc.page(2).unwrap();
2012        assert_eq!(odd.rotate, 0, "non-multiple of 90 falls back to 0");
2013        assert_eq!(
2014            odd.media_box,
2015            Page::US_LETTER,
2016            "degenerate media box defaults"
2017        );
2018    }
2019
2020    // Covers ISO 32000-1 §14.11.2, §7.7.3.3 and §7.9.5.
2021    #[test]
2022    fn page_boxes_declared_and_defaulted() {
2023        let mut b = PdfBuilder::new();
2024        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2025        b.object(2, "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>");
2026        b.object(
2027            3,
2028            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] \
2029             /CropBox [50 50 550 750] /BleedBox [40 40 560 760] \
2030             /TrimBox [5 0 R 6 0 R 7 0 R 8 0 R] >>",
2031        );
2032        b.object(4, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] >>");
2033        b.object(5, "60");
2034        b.object(6, "70");
2035        b.object(7, "540");
2036        b.object(8, "730");
2037        let doc = Document::load(b.build(1)).unwrap();
2038
2039        let page = doc.page(0).unwrap();
2040        assert_eq!(page.bleed_box, Rect::new(40.0, 40.0, 560.0, 760.0));
2041        assert_eq!(
2042            page.trim_box,
2043            Rect::new(60.0, 70.0, 540.0, 730.0),
2044            "indirect array elements resolve"
2045        );
2046        assert_eq!(
2047            page.art_box, page.crop_box,
2048            "undeclared box is the crop box"
2049        );
2050
2051        let bare = doc.page(1).unwrap();
2052        assert_eq!(bare.bleed_box, bare.crop_box);
2053        assert_eq!(bare.trim_box, bare.crop_box);
2054        assert_eq!(bare.art_box, bare.crop_box);
2055    }
2056
2057    // Covers ISO 32000-1 §14.11.2 and §7.7.3.3.
2058    #[test]
2059    fn page_boxes_clip_to_media_and_fall_back() {
2060        let mut b = PdfBuilder::new();
2061        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2062        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
2063        b.object(
2064            3,
2065            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] \
2066             /CropBox [10 10 190 190] /TrimBox [100 100 400 400] \
2067             /BleedBox [0 0 0 0] /ArtBox [300 300 400 400] >>",
2068        );
2069        let doc = Document::load(b.build(1)).unwrap();
2070
2071        let page = doc.page(0).unwrap();
2072        assert_eq!(
2073            page.trim_box,
2074            Rect::new(100.0, 100.0, 200.0, 200.0),
2075            "a trim box past the media box clips to it"
2076        );
2077        assert_eq!(
2078            page.bleed_box, page.crop_box,
2079            "a degenerate bleed box falls back to the crop box"
2080        );
2081        assert_eq!(
2082            page.art_box, page.crop_box,
2083            "an art box disjoint from the media box falls back to the crop box"
2084        );
2085    }
2086
2087    /// `/BleedBox`, `/TrimBox` and `/ArtBox` are not in ISO 32000 Table 30's
2088    /// inheritable set: a value on a `/Pages` node must not leak into its
2089    /// leaves, which read their spec default (the crop box) instead.
2090    // Covers ISO 32000-1 §14.11.2 and §7.7.3.3.
2091    #[test]
2092    fn page_boxes_are_not_inherited() {
2093        let mut b = PdfBuilder::new();
2094        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2095        b.object(
2096            2,
2097            "<< /Type /Pages /Kids [3 0 R] /Count 1 \
2098             /TrimBox [10 10 90 90] /BleedBox [5 5 95 95] /ArtBox [20 20 80 80] >>",
2099        );
2100        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2101        let doc = Document::load(b.build(1)).unwrap();
2102
2103        let page = doc.page(0).unwrap();
2104        assert_eq!(page.trim_box, page.crop_box);
2105        assert_eq!(page.bleed_box, page.crop_box);
2106        assert_eq!(page.art_box, page.crop_box);
2107    }
2108
2109    // Covers ISO 32000-1 §7.7.3.2.
2110    #[test]
2111    fn kids_cycle_truncates_without_hanging() {
2112        let mut b = PdfBuilder::new();
2113        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2114        // 2 → 3 → {4, back to 2}: the back-edge must be ignored.
2115        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
2116        b.object(3, "<< /Type /Pages /Kids [4 0 R 2 0 R] /Count 1 >>");
2117        b.object(
2118            4,
2119            "<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 100] /Contents 5 0 R >>",
2120        );
2121        b.stream(5, "", b"0 0 50 50 re f");
2122        let doc = Document::load(b.build(1)).unwrap();
2123        assert_eq!(doc.page_count(), 1, "cycle back-edge yields no extra pages");
2124        assert!(contains(
2125            &doc.page(0).unwrap().content(&doc).unwrap(),
2126            b"re f"
2127        ));
2128    }
2129
2130    // Covers ISO 32000-1 §7.7.3.2 and Annex C.2.
2131    #[test]
2132    fn tree_depth_is_capped() {
2133        let mut b = PdfBuilder::new();
2134        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2135        // A unary chain of 300 intermediate nodes, page leaf at the bottom.
2136        let last = 302u32;
2137        for num in 2..last {
2138            b.object(
2139                num,
2140                &format!("<< /Type /Pages /Kids [{} 0 R] /Count 1 >>", num + 1),
2141            );
2142        }
2143        b.object(last, "<< /Type /Page >>");
2144        let doc = Document::load(b.build(1)).unwrap();
2145        // `page_count` reports the tree's declared `/Count` (1) cheaply, as
2146        // mature engines do; the leaf itself lies beyond the traversal depth
2147        // cap, so the flattened tree is empty and the page cannot be
2148        // materialized.
2149        assert_eq!(doc.page_count(), 1, "declared /Count is reported cheaply");
2150        assert!(
2151            matches!(doc.page(0), Err(Error::PageNotFound(0, 0))),
2152            "leaf beyond the depth cap cannot be materialized"
2153        );
2154    }
2155
2156    // Covers ISO 32000-1 §7.7.3.2.
2157    #[test]
2158    fn page_count_reports_declared_count_cheaply() {
2159        // The tree declares five pages but supplies only one kid. `page_count`
2160        // reports the declared `/Count` (as mature engines do) without walking,
2161        // while page access is bounded by the pages that actually materialize.
2162        let mut b = PdfBuilder::new();
2163        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2164        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 5 >>");
2165        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2166        let doc = Document::load(b.build(1)).unwrap();
2167        assert_eq!(doc.page_count(), 5, "declared /Count reported verbatim");
2168        assert!(doc.page(0).is_ok(), "the one real page materializes");
2169        assert!(
2170            matches!(doc.page(1), Err(Error::PageNotFound(1, 1))),
2171            "access past the real pages fails with the true length"
2172        );
2173    }
2174
2175    // Covers ISO 32000-1 §7.7.3.2.
2176    #[test]
2177    fn page_count_falls_back_to_walk_when_count_absent() {
2178        let mut b = PdfBuilder::new();
2179        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2180        b.object(2, "<< /Type /Pages /Kids [3 0 R] >>"); // no /Count
2181        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2182        let doc = Document::load(b.build(1)).unwrap();
2183        assert_eq!(
2184            doc.page_count(),
2185            1,
2186            "missing /Count is recovered by walking"
2187        );
2188    }
2189
2190    // Covers ISO 32000-1 §7.7.3.2.
2191    #[test]
2192    fn page_count_ignores_corrupt_oversized_count() {
2193        // A `/Count` larger than the whole file is impossible: fall back to a
2194        // real walk rather than trust it.
2195        let mut b = PdfBuilder::new();
2196        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2197        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 999999999 >>");
2198        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
2199        let doc = Document::load(b.build(1)).unwrap();
2200        assert_eq!(doc.page_count(), 1, "implausible /Count is rejected");
2201    }
2202
2203    // Covers ISO 32000-1 §7.5.2 and Annex I.
2204    #[test]
2205    fn version_scan_and_default() {
2206        let mut b = PdfBuilder::new().version(2, 0);
2207        b.object(1, "<< /Type /Catalog >>");
2208        assert_eq!(Document::load(b.build(1)).unwrap().version(), (2, 0));
2209        // Corrupting the header magic (same length) falls back to 1.4.
2210        let data = replace_once(&simple_doc("v"), b"%PDF-", b"%QQQ-");
2211        assert_eq!(Document::load(data).unwrap().version(), (1, 4));
2212    }
2213
2214    // Covers ISO 32000-1 Annex C.2.
2215    #[test]
2216    fn deeply_nested_root_object_does_not_overflow_the_stack() {
2217        // A ~100 KB file whose Root is a 50k-deep array used to drive the
2218        // object parser's recursion into a fatal stack overflow during
2219        // `Document::load`. Run on a small stack so a regression aborts
2220        // loudly rather than depending on the main thread's stack size.
2221        let mut data = b"%PDF-1.7\n1 0 obj\n".to_vec();
2222        data.extend(std::iter::repeat_n(b'[', 50_000));
2223        data.extend(std::iter::repeat_n(b']', 50_000));
2224        data.extend_from_slice(b"\nendobj\ntrailer\n<</Root 1 0 R>>\n%%EOF\n");
2225        let outcome = std::thread::Builder::new()
2226            .stack_size(1024 * 1024)
2227            .spawn(move || Document::load(data).map(|doc| doc.page_count()))
2228            .expect("spawn test thread")
2229            .join()
2230            .expect("Document::load must not overflow the stack");
2231        // The over-nested Root is rejected or ignored (lenient), but the
2232        // process survives and no page is fabricated from it.
2233        assert!(matches!(outcome, Ok(0) | Err(_)));
2234    }
2235
2236    #[test]
2237    fn bytes_and_xref_accessors() {
2238        let data = simple_doc("accessors");
2239        let doc = Document::load(data.clone()).unwrap();
2240        assert_eq!(doc.bytes(), &data[..]);
2241        assert!(!doc.xref().is_empty());
2242        assert!(doc.xref().trailer.get("Root").is_some());
2243    }
2244
2245    #[test]
2246    fn object_at_spanned_reparses_identically() {
2247        let data = simple_doc("spanned");
2248        let doc = Document::load(data).unwrap();
2249        for (num, entry) in doc.xref().iter() {
2250            let XrefEntry::InFile { offset, gen } = entry else {
2251                continue;
2252            };
2253            let (r, object, span) = doc.object_at_spanned(offset as usize).unwrap();
2254            assert_eq!(r.num, num);
2255            assert_eq!(r.gen, gen);
2256            assert_eq!(span.start, offset);
2257            assert!(span.end as usize <= doc.bytes().len());
2258            // The bytes at the span parse back to the same object.
2259            let slice = &doc.bytes()[span.start as usize..span.end as usize];
2260            let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
2261            assert_eq!(r2, r);
2262            assert_eq!(object2, object);
2263        }
2264    }
2265
2266    #[test]
2267    fn page_object_ref_points_at_a_page_dict() {
2268        let doc = Document::load(multi_page_doc(&["one", "two"])).unwrap();
2269        for index in 0..doc.page_count() {
2270            let page = doc.page(index).unwrap();
2271            let r = page.object_ref().expect("builder pages are indirect");
2272            let resolved = doc.get(r).unwrap();
2273            assert_eq!(
2274                resolved
2275                    .as_dict()
2276                    .unwrap()
2277                    .get_name("Type")
2278                    .map(|n| n.0.as_str()),
2279                Some("Page")
2280            );
2281        }
2282    }
2283
2284    // --- Minimal Standard-handler (RC4 V2/R3) fixture builder, duplicating
2285    // the key-derivation mechanism `crypt::tests` uses under the empty user
2286    // password (those helpers are private to that module's tests). Needed
2287    // only to pin the decrypt-identity regression test below: RC4's
2288    // per-object key depends on the object's num/gen, so it is the cipher
2289    // that can actually distinguish "decrypt with the parsed header's
2290    // identity" from "decrypt with the caller's requested identity".
2291
2292    const RC4_FIXTURE_KEY_LEN: usize = 16; // 128-bit key
2293    const RC4_FIXTURE_P: i32 = -44;
2294    const RC4_FIXTURE_ID0: &[u8] = b"0123456789abcdef";
2295    const RC4_FIXTURE_PAD: [u8; 32] = [
2296        0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01,
2297        0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53,
2298        0x69, 0x7A,
2299    ];
2300
2301    #[rustfmt::skip]
2302    const RC4_FIXTURE_MD5_S: [u32; 64] = [
2303        7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
2304        5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
2305        4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
2306        6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
2307    ];
2308    #[rustfmt::skip]
2309    const RC4_FIXTURE_MD5_K: [u32; 64] = [
2310        0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
2311        0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
2312        0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
2313        0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
2314        0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
2315        0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
2316        0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
2317        0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
2318    ];
2319
2320    fn rc4_fixture_md5(input: &[u8]) -> [u8; 16] {
2321        let (mut a0, mut b0, mut c0, mut d0) = (
2322            0x6745_2301u32,
2323            0xefcd_ab89u32,
2324            0x98ba_dcfeu32,
2325            0x1032_5476u32,
2326        );
2327        let mut msg = input.to_vec();
2328        let bitlen = (input.len() as u64).wrapping_mul(8);
2329        msg.push(0x80);
2330        while msg.len() % 64 != 56 {
2331            msg.push(0);
2332        }
2333        msg.extend_from_slice(&bitlen.to_le_bytes());
2334        for chunk in msg.as_chunks::<64>().0 {
2335            let mut m = [0u32; 16];
2336            for (word, bytes) in m.iter_mut().zip(chunk.as_chunks::<4>().0) {
2337                *word = u32::from_le_bytes(*bytes);
2338            }
2339            let (mut a, mut b, mut c, mut d) = (a0, b0, c0, d0);
2340            for i in 0..64 {
2341                let (f, g) = match i {
2342                    0..=15 => ((b & c) | (!b & d), i),
2343                    16..=31 => ((d & b) | (!d & c), (5 * i + 1) % 16),
2344                    32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
2345                    _ => (c ^ (b | !d), (7 * i) % 16),
2346                };
2347                let f = f
2348                    .wrapping_add(a)
2349                    .wrapping_add(RC4_FIXTURE_MD5_K[i])
2350                    .wrapping_add(m[g]);
2351                a = d;
2352                d = c;
2353                c = b;
2354                b = b.wrapping_add(f.rotate_left(RC4_FIXTURE_MD5_S[i]));
2355            }
2356            a0 = a0.wrapping_add(a);
2357            b0 = b0.wrapping_add(b);
2358            c0 = c0.wrapping_add(c);
2359            d0 = d0.wrapping_add(d);
2360        }
2361        let mut out = [0u8; 16];
2362        out[0..4].copy_from_slice(&a0.to_le_bytes());
2363        out[4..8].copy_from_slice(&b0.to_le_bytes());
2364        out[8..12].copy_from_slice(&c0.to_le_bytes());
2365        out[12..16].copy_from_slice(&d0.to_le_bytes());
2366        out
2367    }
2368
2369    fn rc4_fixture_rc4(key: &[u8], data: &[u8]) -> Vec<u8> {
2370        let mut s: [u8; 256] = core::array::from_fn(|i| i as u8);
2371        let mut j = 0u8;
2372        for i in 0..256 {
2373            j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
2374            s.swap(i, j as usize);
2375        }
2376        let mut out = Vec::with_capacity(data.len());
2377        let (mut i, mut j) = (0u8, 0u8);
2378        for &byte in data {
2379            i = i.wrapping_add(1);
2380            j = j.wrapping_add(s[i as usize]);
2381            s.swap(i as usize, j as usize);
2382            let k = s[s[i as usize].wrapping_add(s[j as usize]) as usize];
2383            out.push(byte ^ k);
2384        }
2385        out
2386    }
2387
2388    /// `/O` for empty owner and user passwords (Algorithm 3, R3).
2389    fn rc4_fixture_owner_entry() -> Vec<u8> {
2390        let mut d = rc4_fixture_md5(&RC4_FIXTURE_PAD);
2391        for _ in 0..50 {
2392            d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
2393        }
2394        let rc4key = d[..RC4_FIXTURE_KEY_LEN].to_vec();
2395        let mut o = rc4_fixture_rc4(&rc4key, &RC4_FIXTURE_PAD);
2396        for i in 1u8..=19 {
2397            let k: Vec<u8> = rc4key.iter().map(|b| b ^ i).collect();
2398            o = rc4_fixture_rc4(&k, &o);
2399        }
2400        o
2401    }
2402
2403    /// File key from `/O` for the empty user password (Algorithm 2, R3).
2404    fn rc4_fixture_file_key(o: &[u8]) -> Vec<u8> {
2405        let mut input = Vec::new();
2406        input.extend_from_slice(&RC4_FIXTURE_PAD);
2407        input.extend_from_slice(o);
2408        input.extend_from_slice(&(RC4_FIXTURE_P as u32).to_le_bytes());
2409        input.extend_from_slice(RC4_FIXTURE_ID0);
2410        let mut d = rc4_fixture_md5(&input);
2411        for _ in 0..50 {
2412            d = rc4_fixture_md5(&d[..RC4_FIXTURE_KEY_LEN]);
2413        }
2414        d[..RC4_FIXTURE_KEY_LEN].to_vec()
2415    }
2416
2417    /// `/U` for the empty user password (Algorithm 5, R3).
2418    fn rc4_fixture_user_entry(key: &[u8]) -> Vec<u8> {
2419        let mut input = Vec::new();
2420        input.extend_from_slice(&RC4_FIXTURE_PAD);
2421        input.extend_from_slice(RC4_FIXTURE_ID0);
2422        let mut x = rc4_fixture_md5(&input).to_vec();
2423        x = rc4_fixture_rc4(key, &x);
2424        for i in 1u8..=19 {
2425            let k: Vec<u8> = key.iter().map(|b| b ^ i).collect();
2426            x = rc4_fixture_rc4(&k, &x);
2427        }
2428        x.resize(32, 0); // trailing padding is arbitrary
2429        x
2430    }
2431
2432    fn rc4_fixture_obj_key(key: &[u8], num: u32, gen: u16) -> Vec<u8> {
2433        let mut input = key.to_vec();
2434        input.extend_from_slice(&num.to_le_bytes()[..3]);
2435        input.extend_from_slice(&gen.to_le_bytes()[..2]);
2436        rc4_fixture_md5(&input)[..(key.len() + 5).min(16)].to_vec()
2437    }
2438
2439    fn rc4_fixture_hexstr(b: &[u8]) -> String {
2440        let mut s = String::from("<");
2441        for x in b {
2442            s.push_str(&format!("{x:02x}"));
2443        }
2444        s.push('>');
2445        s
2446    }
2447
2448    /// Builds a V2/R3 (128-bit RC4) file, encrypted under the empty
2449    /// password, with a single indirect object (`3 0 obj`, i.e. gen 0)
2450    /// holding an encrypted string.
2451    fn rc4_encrypted_fixture() -> Vec<u8> {
2452        let o = rc4_fixture_owner_entry();
2453        let key = rc4_fixture_file_key(&o);
2454        let u = rc4_fixture_user_entry(&key);
2455        let msg = rc4_fixture_rc4(&rc4_fixture_obj_key(&key, 3, 0), b"Top secret message");
2456
2457        let mut b = PdfBuilder::new().version(1, 4);
2458        b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
2459        b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
2460        b.object(3, &format!("<< /Msg {} >>", rc4_fixture_hexstr(&msg)));
2461        b.object(
2462            9,
2463            &format!(
2464                "<< /Filter /Standard /V 2 /R 3 /Length 128 /P {} /O {} /U {} >>",
2465                RC4_FIXTURE_P,
2466                rc4_fixture_hexstr(&o),
2467                rc4_fixture_hexstr(&u)
2468            ),
2469        );
2470        let trailer = format!(
2471            "/Encrypt 9 0 R /ID [{}{}]",
2472            rc4_fixture_hexstr(RC4_FIXTURE_ID0),
2473            rc4_fixture_hexstr(RC4_FIXTURE_ID0)
2474        );
2475        b.trailer_extra(&trailer).build(1)
2476    }
2477
2478    // Covers ISO 32000-1 §7.6.2.
2479    #[test]
2480    fn encrypted_generation_mismatch_still_decrypts() {
2481        // `object_at_spanned` derives the per-object RC4/AESV2 decrypt key
2482        // from the PARSED "N G obj" header at the object's file offset
2483        // (`r.num`, `r.gen` from `parser.parse_indirect`), never from the
2484        // caller's requested `ObjRef` — mirroring how plain (unencrypted)
2485        // lookups already tolerate a generation mismatch
2486        // (`generation_mismatch_is_tolerated`). Request object 3 (really
2487        // "3 0 obj" in the file) under a deliberately wrong generation: if
2488        // decryption instead used the requested (wrong) gen to derive the
2489        // RC4 object key, the result would be garbage, not the plaintext.
2490        let doc = Document::load(rc4_encrypted_fixture()).expect("empty password opens the file");
2491        let obj3 = doc.get(ObjRef { num: 3, gen: 7 }).unwrap();
2492        let msg = obj3
2493            .as_dict()
2494            .unwrap()
2495            .get("Msg")
2496            .unwrap()
2497            .as_str_bytes()
2498            .unwrap();
2499        assert_eq!(
2500            msg, b"Top secret message",
2501            "decrypted using the file's real gen (0), not the mismatched request (7)"
2502        );
2503    }
2504
2505    // Covers ISO 32000-1 §7.5.7.
2506    #[test]
2507    fn objstm_doc_fixture_loads_and_resolves_members() {
2508        let data = objstm_doc(&[(7, "<< /Marker (inside) >>")]);
2509        let doc = Document::load(data).unwrap();
2510        assert_eq!(doc.page_count(), 1);
2511        let member = doc.get(ObjRef { num: 7, gen: 0 }).unwrap();
2512        let text = member.as_dict().unwrap().get("Marker").unwrap();
2513        assert_eq!(text.as_str_bytes(), Some(&b"inside"[..]));
2514        // The member really is xref'd into the object stream.
2515        assert!(matches!(
2516            doc.xref().get(7),
2517            Some(XrefEntry::InStream { stream_num: 4, .. })
2518        ));
2519    }
2520
2521    /// A page built from parts must expose the same accessors as one that
2522    /// came out of the page tree — this is the constructor aio uses.
2523    #[test]
2524    fn page_from_parts_exposes_its_accessors() {
2525        let mut dict = Dict::default();
2526        dict.insert(Name("Type".into()), Object::Name(Name("Page".into())));
2527        let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2528        let obj_ref = ObjRef { num: 7, gen: 0 };
2529
2530        let page = Page::from_parts(
2531            3,
2532            media,
2533            media,
2534            90,
2535            Dict::default(),
2536            dict.clone(),
2537            Some(obj_ref),
2538        );
2539
2540        assert_eq!(page.index, 3);
2541        assert_eq!(page.object_ref(), Some(obj_ref));
2542        assert_eq!(page.dict(), &dict);
2543        // /Rotate 90 swaps the reported page size.
2544        assert_eq!(page.size(), (400.0, 200.0));
2545        // The boxes from_parts does not take read as their spec default.
2546        assert_eq!(page.bleed_box, page.crop_box);
2547        assert_eq!(page.trim_box, page.crop_box);
2548        assert_eq!(page.art_box, page.crop_box);
2549    }
2550
2551    /// `from_parts` normalizes `/Rotate` just as the page tree does, so an
2552    /// unnormalized quarter-turn stores — and reports the size of — its
2553    /// canonical equivalent.
2554    #[test]
2555    fn page_from_parts_normalizes_rotation() {
2556        let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2557        let build = |rotate: i32| {
2558            Page::from_parts(
2559                0,
2560                media,
2561                media,
2562                rotate,
2563                Dict::default(),
2564                Dict::default(),
2565                None,
2566            )
2567        };
2568
2569        for (given, canonical) in [(-90, 270), (450, 90), (540, 180), (720, 0), (-360, 0)] {
2570            let page = build(given);
2571            assert_eq!(
2572                page.rotate, canonical,
2573                "from_parts must normalize /Rotate {given} to {canonical}"
2574            );
2575            assert_eq!(
2576                page.size(),
2577                build(canonical).size(),
2578                "/Rotate {given} must report the same size as {canonical}"
2579            );
2580        }
2581    }
2582
2583    /// `rotate` is a public field, so `size()` cannot rely on the constructor
2584    /// having normalized it: any multiple of 90 must be read modulo a full
2585    /// turn. The already-canonical values are listed too, pinning that this
2586    /// is unchanged for them.
2587    #[test]
2588    fn page_size_handles_unnormalized_rotation() {
2589        let media = Rect::new(0.0, 0.0, 200.0, 400.0);
2590        let mut page = Page::from_parts(0, media, media, 0, Dict::default(), Dict::default(), None);
2591
2592        for rotate in [90, 270, -90, -270, 450, 630] {
2593            page.rotate = rotate;
2594            assert_eq!(
2595                page.size(),
2596                (400.0, 200.0),
2597                "/Rotate {rotate} is a quarter turn and must swap the size"
2598            );
2599        }
2600        for rotate in [0, 180, -180, 360, 540, 720] {
2601            page.rotate = rotate;
2602            assert_eq!(
2603                page.size(),
2604                (200.0, 400.0),
2605                "/Rotate {rotate} is a half turn and must leave the size alone"
2606            );
2607        }
2608    }
2609}