Skip to main content

pdfboss_core/
document.rs

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