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