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