Skip to main content

pdfboss_core/
document.rs

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