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