Skip to main content

pdfboss_core/
elements.rs

1//! Lazy iteration over a document's elements: the physical file structure
2//! (header, indirect objects, cross-reference sections, trailer, startxref,
3//! eof) with byte spans, and the logical document structure (pages, fonts,
4//! images, annotations, content operators). ISO 32000 §7.5 (file structure)
5//! and §7.7 (document structure).
6
7use std::collections::VecDeque;
8
9use crate::content::Op;
10use crate::document::Document;
11use crate::error::{Error, Result};
12use crate::hash::FastMap;
13use crate::lexer::{Lexer, Token};
14use crate::object::{Dict, Name, ObjRef, Object};
15use crate::xref::{parse_section_at, XrefEntry};
16
17/// Byte range in the physical file, end-exclusive.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Span {
20    pub start: u64,
21    pub end: u64,
22}
23
24impl Span {
25    /// A span from `start` (inclusive) to `end` (exclusive).
26    pub fn new(start: u64, end: u64) -> Span {
27        Span { start, end }
28    }
29
30    /// Number of bytes covered; inverted spans count as zero.
31    pub fn len(&self) -> u64 {
32        self.end.saturating_sub(self.start)
33    }
34
35    /// Whether the span covers no bytes.
36    pub fn is_empty(&self) -> bool {
37        self.end <= self.start
38    }
39}
40
41/// Kind of a cross-reference section (ISO 32000 §7.5.4 / §7.5.8).
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum XrefKind {
44    /// A classic `xref` table.
45    Table,
46    /// A cross-reference stream.
47    Stream,
48}
49
50/// One element of a document, physical or logical.
51#[derive(Debug, Clone)]
52pub enum Element {
53    /// The `%PDF-x.y` header.
54    Header { version: (u8, u8), span: Span },
55    /// One indirect object.
56    IndirectObject {
57        r: ObjRef,
58        object: Object,
59        /// Span of `N G obj … endobj` in the file. For objects stored in an
60        /// object stream this is the container stream object's span.
61        span: Span,
62        /// For objects inside an object stream: the container's reference
63        /// and this object's byte range within the *decoded* stream data.
64        in_objstm: Option<(ObjRef, Span)>,
65    },
66    /// One cross-reference section (table or stream).
67    XrefSection {
68        kind: XrefKind,
69        span: Span,
70        entries: usize,
71    },
72    /// The trailer: the merged trailer dictionary plus the byte range of the
73    /// newest trailer region (classic `trailer << … >>`, or the newest
74    /// cross-reference stream object when no classic trailer exists).
75    /// The `dict` is the MERGED trailer dictionary (keys from newer sections win),
76    /// while `span` covers only the newest trailer region — so unlike other
77    /// physical elements, re-parsing the bytes at `span` does not reproduce `dict`.
78    Trailer { dict: Dict, span: Span },
79    /// The `startxref` keyword and its offset operand.
80    StartXref { offset: u64, span: Span },
81    /// The `%%EOF` marker.
82    Eof { span: Span },
83
84    /// One page (logical).
85    Page { index: usize, r: ObjRef },
86    /// One font referenced from a page's resources.
87    Font {
88        page: Option<usize>,
89        r: ObjRef,
90        subtype: Name,
91        base_font: Option<Name>,
92    },
93    /// One image XObject referenced from a page's resources.
94    Image {
95        page: Option<usize>,
96        r: ObjRef,
97        width: u32,
98        height: u32,
99    },
100    /// One annotation on a page.
101    Annotation {
102        page: usize,
103        r: ObjRef,
104        subtype: Name,
105    },
106    /// One content-stream operator of a page.
107    ContentOp {
108        page: usize,
109        op: Op,
110        /// Byte range within the page's decoded, concatenated content.
111        span_in_content: Span,
112    },
113}
114
115/// Selects which element layers [`crate::Document::elements`] yields.
116#[derive(Debug, Clone)]
117pub struct ElementOpts {
118    /// Yield physical file-structure elements.
119    pub physical: bool,
120    /// Yield logical document-structure elements.
121    pub logical: bool,
122    /// Restrict logical elements to these 0-based page indices.
123    pub pages: Option<Vec<usize>>,
124    /// Yield [`Element::ContentOp`] items (high-volume; off by default).
125    pub content_ops: bool,
126}
127
128impl Default for ElementOpts {
129    fn default() -> Self {
130        ElementOpts {
131            physical: true,
132            logical: true,
133            pages: None,
134            content_ops: false,
135        }
136    }
137}
138
139impl Document {
140    /// Lazy iteration over the document's elements. Physical elements come
141    /// in file order (header, objects by offset with object-stream members
142    /// after their container, xref sections newest→oldest, trailer,
143    /// startxref, eof); logical elements follow in document order. Nothing
144    /// is parsed or decoded before it is yielded; an element that fails to
145    /// parse yields `Err` for that item and iteration continues.
146    ///
147    /// Physical objects sort by `(file offset, member index, object number)`:
148    /// in-file objects use their own offset with member index 0;
149    /// object-stream members use their container's offset with member index
150    /// `1 + index` so they directly follow their container in stream-index order;
151    /// members whose container is missing or free sort last (offset `u64::MAX`)
152    /// and surface as `Err` items; ties on offset and member index break by
153    /// ascending object number.
154    pub fn elements(&self, opts: ElementOpts) -> Elements<'_> {
155        Elements {
156            doc: self,
157            opts,
158            stage: Stage::Start,
159            container_spans: FastMap::default(),
160        }
161    }
162}
163
164/// Iterator state. Each `next()` parses at most one element.
165pub struct Elements<'a> {
166    doc: &'a Document,
167    opts: ElementOpts,
168    stage: Stage,
169    /// File spans of already-parsed object-stream containers.
170    container_spans: FastMap<u32, Span>,
171}
172
173enum Stage {
174    Start,
175    Objects {
176        order: Vec<OrderEntry>,
177        next: usize,
178    },
179    Sections {
180        /// Offsets still to visit: the newest section first, then each
181        /// section's hybrid `/XRefStm` (queued right after that section, so
182        /// it is visited before the `/Prev` chain continues), then `/Prev`.
183        pending: VecDeque<usize>,
184        visited: Vec<usize>,
185        /// Newest classic trailer span, or newest stream-section span.
186        trailer_span: Option<Span>,
187    },
188    Trailer {
189        span: Option<Span>,
190    },
191    StartXref,
192    Eof,
193    Logical {
194        page: usize,
195        part: PagePart,
196    },
197    Done,
198}
199
200/// Logical iteration works page-by-page: entering a page materializes that
201/// page's elements into a queue (bounded by one page), which then drains
202/// one `next()` at a time.
203enum PagePart {
204    PageItself,
205    Drain { queue: VecDeque<Result<Element>> },
206}
207
208/// One object scheduled for physical iteration, pre-sorted by file position.
209struct OrderEntry {
210    num: u32,
211    entry: XrefEntry,
212    /// The object's own offset, or its container's offset for members.
213    sort_offset: u64,
214    /// 0 for in-file objects; 1 + member index for object-stream members,
215    /// so members directly follow their container.
216    sort_member: u64,
217}
218
219impl<'a> Iterator for Elements<'a> {
220    type Item = Result<Element>;
221
222    fn next(&mut self) -> Option<Self::Item> {
223        loop {
224            match &mut self.stage {
225                Stage::Start => {
226                    let order = if self.opts.physical {
227                        build_order(self.doc)
228                    } else {
229                        Vec::new()
230                    };
231                    let header = self.opts.physical.then(|| header_element(self.doc));
232                    self.stage = Stage::Objects { order, next: 0 };
233                    if let Some(Some(header)) = header {
234                        return Some(Ok(header));
235                    }
236                }
237                Stage::Objects { order, next } => {
238                    if *next >= order.len() {
239                        self.stage = if self.opts.physical {
240                            Stage::Sections {
241                                pending: find_startxref_offset(self.doc.bytes())
242                                    .into_iter()
243                                    .collect(),
244                                visited: Vec::new(),
245                                trailer_span: None,
246                            }
247                        } else {
248                            Stage::Logical {
249                                page: 0,
250                                part: PagePart::PageItself,
251                            }
252                        };
253                        continue;
254                    }
255                    let index = *next;
256                    *next += 1;
257                    // Copy the (Copy) num/entry out of the borrowed order
258                    // slice first: `self.object_element` needs `&mut self`,
259                    // which would otherwise conflict with the still-live
260                    // borrow of `self.stage` (via `order`) that a reference
261                    // used inline as call arguments would keep alive.
262                    let (num, entry) = {
263                        let e = &order[index];
264                        (e.num, e.entry)
265                    };
266                    return Some(self.object_element(num, entry));
267                }
268                Stage::Sections {
269                    pending,
270                    visited,
271                    trailer_span,
272                } => {
273                    let Some(off) = pending.pop_front() else {
274                        self.stage = Stage::Trailer {
275                            span: *trailer_span,
276                        };
277                        continue;
278                    };
279                    if visited.contains(&off) {
280                        continue; // already visited via another path; skip
281                    }
282                    visited.push(off);
283                    match parse_section_at(self.doc.bytes(), off) {
284                        Ok(info) => {
285                            if trailer_span.is_none() {
286                                *trailer_span = info.trailer_span.or(Some(info.span));
287                            }
288                            let bytes_len = self.doc.bytes().len();
289                            // Hybrid files: the classic trailer's /XRefStm
290                            // names a supplementary cross-reference stream at
291                            // the same revision. Queue it right after this
292                            // section (and ahead of /Prev) so it is visited
293                            // before the chain walks further back; stream
294                            // sections never carry an /XRefStm of their own.
295                            if let Some(xs) = info
296                                .xrefstm
297                                .and_then(|v| usize::try_from(v).ok())
298                                .filter(|&o| o < bytes_len && !visited.contains(&o))
299                            {
300                                pending.push_back(xs);
301                            }
302                            if let Some(prev) = info
303                                .prev
304                                .and_then(|v| usize::try_from(v).ok())
305                                .filter(|&o| o < bytes_len && !visited.contains(&o))
306                            {
307                                pending.push_back(prev);
308                            }
309                            let element = Element::XrefSection {
310                                kind: info.kind,
311                                span: info.span,
312                                entries: info.xref.len(),
313                            };
314                            return Some(Ok(element));
315                        }
316                        Err(err) => {
317                            // Salvage: report the broken section, then stop
318                            // walking the whole chain.
319                            pending.clear();
320                            return Some(Err(err));
321                        }
322                    }
323                }
324                Stage::Trailer { span } => {
325                    let span = *span;
326                    self.stage = Stage::StartXref;
327                    if let Some(span) = span {
328                        return Some(Ok(Element::Trailer {
329                            dict: self.doc.xref().trailer.clone(),
330                            span,
331                        }));
332                    }
333                }
334                Stage::StartXref => {
335                    self.stage = Stage::Eof;
336                    if let Some(element) = startxref_element(self.doc.bytes()) {
337                        return Some(Ok(element));
338                    }
339                }
340                Stage::Eof => {
341                    self.stage = Stage::Logical {
342                        page: 0,
343                        part: PagePart::PageItself,
344                    };
345                    if let Some(element) = eof_element(self.doc.bytes()) {
346                        return Some(Ok(element));
347                    }
348                }
349                Stage::Logical { page, .. } => {
350                    if !self.opts.logical || *page >= self.doc.page_count() {
351                        self.stage = Stage::Done;
352                        continue;
353                    }
354                    let index = *page;
355                    let selected = self
356                        .opts
357                        .pages
358                        .as_ref()
359                        .map(|list| list.contains(&index))
360                        .unwrap_or(true);
361                    // Take `part` out of `self.stage` by value: the match
362                    // above holds `self.stage` mutably only through `page`
363                    // (an `&mut usize`, dropped by the copy above), so this
364                    // replace is the sole remaining borrow. Taking ownership
365                    // this way — rather than keeping a `&mut PagePart` live
366                    // across the `self.page_elements` call below — sidesteps
367                    // the same borrow conflict `Stage::Objects` avoids by
368                    // copying `num`/`entry` out before calling
369                    // `self.object_element`.
370                    let Stage::Logical { part, .. } =
371                        std::mem::replace(&mut self.stage, Stage::Done)
372                    else {
373                        continue;
374                    };
375                    match part {
376                        PagePart::PageItself => {
377                            if !selected {
378                                self.stage = Stage::Logical {
379                                    page: index + 1,
380                                    part: PagePart::PageItself,
381                                };
382                                continue;
383                            }
384                            let queue = self.page_elements(index);
385                            self.stage = Stage::Logical {
386                                page: index,
387                                part: PagePart::Drain { queue },
388                            };
389                        }
390                        PagePart::Drain { mut queue } => match queue.pop_front() {
391                            Some(item) => {
392                                self.stage = Stage::Logical {
393                                    page: index,
394                                    part: PagePart::Drain { queue },
395                                };
396                                return Some(item);
397                            }
398                            None => {
399                                self.stage = Stage::Logical {
400                                    page: index + 1,
401                                    part: PagePart::PageItself,
402                                };
403                            }
404                        },
405                    }
406                }
407                Stage::Done => return None,
408            }
409        }
410    }
411}
412
413impl<'a> Elements<'a> {
414    /// Builds the `IndirectObject` element for one xref entry.
415    fn object_element(&mut self, num: u32, entry: XrefEntry) -> Result<Element> {
416        match entry {
417            XrefEntry::Free => Err(Error::ObjectNotFound(num, 0)),
418            XrefEntry::InFile { offset, .. } => {
419                let offset = usize::try_from(offset)
420                    .ok()
421                    .filter(|&o| o < self.doc.bytes().len())
422                    .ok_or(Error::ObjectNotFound(num, 0))?;
423                let (r, object, span) = self.doc.object_at_spanned(offset)?;
424                self.container_spans.insert(r.num, span);
425                Ok(Element::IndirectObject {
426                    r,
427                    object,
428                    span,
429                    in_objstm: None,
430                })
431            }
432            XrefEntry::InStream { stream_num, index } => {
433                let container_span = self.container_span(stream_num)?;
434                let stm = self.doc.objstm_handle(stream_num)?;
435                let (object, (start, end)) = stm.object_spanned(index)?;
436                Ok(Element::IndirectObject {
437                    r: ObjRef { num, gen: 0 },
438                    object,
439                    span: container_span,
440                    in_objstm: Some((
441                        ObjRef {
442                            num: stream_num,
443                            gen: 0,
444                        },
445                        Span::new(start as u64, end as u64),
446                    )),
447                })
448            }
449        }
450    }
451
452    /// Materializes one page's logical elements, in document order: the page
453    /// itself, fonts, images, annotations (content ops are appended by the
454    /// content-op stage when enabled). Broken pieces surface as `Err` items.
455    ///
456    /// Covers ISO 32000-1 §12.5.2.
457    fn page_elements(&self, index: usize) -> VecDeque<Result<Element>> {
458        let mut queue = VecDeque::new();
459        let page = match self.doc.page(index) {
460            Ok(page) => page,
461            Err(err) => {
462                queue.push_back(Err(err));
463                return queue;
464            }
465        };
466        if let Some(r) = page.object_ref() {
467            queue.push_back(Ok(Element::Page { index, r }));
468        }
469        // Fonts: /Resources /Font — a dict of name → (usually) reference.
470        for (r, dict) in self.referenced_dict_entries(page.resources.get("Font")) {
471            let subtype = dict
472                .get_name("Subtype")
473                .cloned()
474                .unwrap_or_else(|| Name(String::new()));
475            let base_font = dict.get_name("BaseFont").cloned();
476            queue.push_back(Ok(Element::Font {
477                page: Some(index),
478                r,
479                subtype,
480                base_font,
481            }));
482        }
483        // Images: /Resources /XObject entries whose /Subtype is /Image.
484        for (r, dict) in self.referenced_dict_entries(page.resources.get("XObject")) {
485            if dict.get_name("Subtype").map(|n| n.0.as_str()) != Some("Image") {
486                continue;
487            }
488            let width = dict.get_int("Width").and_then(|v| u32::try_from(v).ok());
489            let height = dict.get_int("Height").and_then(|v| u32::try_from(v).ok());
490            queue.push_back(Ok(Element::Image {
491                page: Some(index),
492                r,
493                width: width.unwrap_or(0),
494                height: height.unwrap_or(0),
495            }));
496        }
497        // Annotations: the page dict's /Annots array of references.
498        if let Some(annots) = page.dict().get("Annots") {
499            if let Ok(Object::Array(items)) = self.doc.resolve(annots) {
500                for item in items {
501                    let Object::Ref(r) = item else { continue };
502                    let Ok(resolved) = self.doc.resolve(&Object::Ref(r)) else {
503                        continue;
504                    };
505                    let Some(dict) = resolved.as_dict() else {
506                        continue;
507                    };
508                    let subtype = dict
509                        .get_name("Subtype")
510                        .cloned()
511                        .unwrap_or_else(|| Name(String::new()));
512                    queue.push_back(Ok(Element::Annotation {
513                        page: index,
514                        r,
515                        subtype,
516                    }));
517                }
518            }
519        }
520        // Content operators, when requested: parsed against the page's
521        // decoded, concatenated content.
522        if self.opts.content_ops {
523            match page.content(self.doc) {
524                Ok(content) => match crate::content::parse_content_spanned(&content) {
525                    Ok(spanned) => {
526                        for (op, span) in spanned {
527                            queue.push_back(Ok(Element::ContentOp {
528                                page: index,
529                                op,
530                                span_in_content: span,
531                            }));
532                        }
533                    }
534                    Err(err) => queue.push_back(Err(err)),
535                },
536                Err(err) => queue.push_back(Err(err)),
537            }
538        }
539        queue
540    }
541
542    /// Resolves a resource-category value (e.g. the `/Font` entry) to its
543    /// dictionary and yields, in name order, each entry that is a reference
544    /// to a dictionary or stream — as `(reference, dictionary)`. Entries
545    /// inlined without a reference are skipped (they have no identity to
546    /// report); name order keeps iteration deterministic.
547    fn referenced_dict_entries(&self, category: Option<&Object>) -> Vec<(ObjRef, Dict)> {
548        let Some(category) = category else {
549            return Vec::new();
550        };
551        let Ok(resolved) = self.doc.resolve(category) else {
552            return Vec::new();
553        };
554        let Some(dict) = resolved.as_dict() else {
555            return Vec::new();
556        };
557        let mut names: Vec<&Name> = dict.iter().map(|entry| entry.0).collect();
558        names.sort();
559        let mut out = Vec::new();
560        for name in names {
561            let Some(Object::Ref(r)) = dict.get(&name.0) else {
562                continue;
563            };
564            let r = *r;
565            let Ok(target) = self.doc.resolve(&Object::Ref(r)) else {
566                continue;
567            };
568            let Some(target_dict) = target.as_dict() else {
569                continue;
570            };
571            out.push((r, target_dict.clone()));
572        }
573        out
574    }
575
576    /// The file span of an object-stream container, parsed at most once.
577    fn container_span(&mut self, stream_num: u32) -> Result<Span> {
578        if let Some(span) = self.container_spans.get(&stream_num) {
579            return Ok(*span);
580        }
581        let offset = match self.doc.xref().get(stream_num) {
582            Some(XrefEntry::InFile { offset, .. }) => usize::try_from(offset)
583                .ok()
584                .filter(|&o| o < self.doc.bytes().len())
585                .ok_or(Error::ObjectNotFound(stream_num, 0))?,
586            _ => return Err(Error::ObjectNotFound(stream_num, 0)),
587        };
588        let (.., span) = self.doc.object_at_spanned(offset)?;
589        self.container_spans.insert(stream_num, span);
590        Ok(span)
591    }
592}
593
594/// All live objects sorted into file order: in-file objects by offset, then
595/// object-stream members grouped after their container by member index.
596fn build_order(doc: &Document) -> Vec<OrderEntry> {
597    let mut order: Vec<OrderEntry> = doc
598        .xref()
599        .iter()
600        .filter_map(|(num, entry)| match entry {
601            XrefEntry::Free => None,
602            XrefEntry::InFile { offset, .. } => Some(OrderEntry {
603                num,
604                entry,
605                sort_offset: offset,
606                sort_member: 0,
607            }),
608            XrefEntry::InStream { stream_num, index } => {
609                let container_offset = match doc.xref().get(stream_num) {
610                    Some(XrefEntry::InFile { offset, .. }) => offset,
611                    // A member whose container is missing sorts last and
612                    // surfaces as Err from object_element.
613                    None | Some(XrefEntry::Free) | Some(XrefEntry::InStream { .. }) => u64::MAX,
614                };
615                Some(OrderEntry {
616                    num,
617                    entry,
618                    sort_offset: container_offset,
619                    sort_member: 1 + u64::from(index),
620                })
621            }
622        })
623        .collect();
624    order.sort_by_key(|e| (e.sort_offset, e.sort_member, e.num));
625    order
626}
627
628/// The `%PDF-x.y` header element, when a header is physically present.
629fn header_element(doc: &Document) -> Option<Element> {
630    let data = doc.bytes();
631    let window = &data[..data.len().min(1024)];
632    let pos = memchr::memmem::find(window, b"%PDF-")?;
633    let digits_end = window[pos + 5..]
634        .iter()
635        .position(|&b| !(b.is_ascii_digit() || b == b'.'))
636        .map(|rel| pos + 5 + rel)
637        .unwrap_or(window.len());
638    Some(Element::Header {
639        version: doc.version(),
640        span: Span::new(pos as u64, digits_end as u64),
641    })
642}
643
644/// The byte offset announced by the last `startxref` keyword (the offset the
645/// section walk starts from), bounded to the file.
646fn find_startxref_offset(data: &[u8]) -> Option<usize> {
647    let tail = data.len().saturating_sub(64 * 1024);
648    let rel = memchr::memmem::rfind(&data[tail..], b"startxref")?;
649    let mut lexer = Lexer::at(data, tail + rel + b"startxref".len());
650    match lexer.next_token() {
651        Ok(Token::Int(v)) => usize::try_from(v).ok().filter(|&o| o < data.len()),
652        _ => None,
653    }
654}
655
656/// The `startxref` element: keyword through its integer operand.
657fn startxref_element(data: &[u8]) -> Option<Element> {
658    let tail = data.len().saturating_sub(64 * 1024);
659    let rel = memchr::memmem::rfind(&data[tail..], b"startxref")?;
660    let start = tail + rel;
661    let mut lexer = Lexer::at(data, start + b"startxref".len());
662    match lexer.next_token() {
663        Ok(Token::Int(v)) if v >= 0 => Some(Element::StartXref {
664            offset: v as u64,
665            span: Span::new(start as u64, lexer.pos() as u64),
666        }),
667        _ => None,
668    }
669}
670
671/// The last `%%EOF` marker.
672fn eof_element(data: &[u8]) -> Option<Element> {
673    let tail = data.len().saturating_sub(64 * 1024);
674    let rel = memchr::memmem::rfind(&data[tail..], b"%%EOF")?;
675    let start = tail + rel;
676    Some(Element::Eof {
677        span: Span::new(start as u64, (start + b"%%EOF".len()) as u64),
678    })
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    #[test]
686    fn element_opts_defaults() {
687        let opts = ElementOpts::default();
688        assert!(opts.physical);
689        assert!(opts.logical);
690        assert!(opts.pages.is_none());
691        assert!(!opts.content_ops);
692    }
693
694    #[test]
695    fn span_length_and_emptiness() {
696        let span = Span::new(10, 25);
697        assert_eq!(span.len(), 15);
698        assert!(!span.is_empty());
699        assert!(Span::new(7, 7).is_empty());
700        assert_eq!(Span::new(9, 3).len(), 0);
701    }
702
703    use crate::document::Document;
704    use crate::error::Result;
705    use crate::object::ObjRef;
706    use crate::parser::{NoResolve, Parser};
707
708    fn physical(doc: &Document) -> Vec<Element> {
709        let opts = ElementOpts {
710            logical: false,
711            ..ElementOpts::default()
712        };
713        doc.elements(opts).collect::<Result<Vec<_>>>().unwrap()
714    }
715
716    #[test]
717    fn simple_doc_physical_walk() {
718        let data = pdfboss_testkit::simple_doc("walk");
719        let doc = Document::load(data).unwrap();
720        let elements = physical(&doc);
721
722        let Element::Header { version, span } = &elements[0] else {
723            panic!("first element must be the header, got {:?}", elements[0]);
724        };
725        assert_eq!(*version, (1, 7));
726        assert!(doc.bytes()[span.start as usize..].starts_with(b"%PDF-1.7"));
727
728        let mut object_count = 0usize;
729        let mut previous_end = 0u64;
730        for element in &elements {
731            if let Element::IndirectObject {
732                r,
733                object,
734                span,
735                in_objstm,
736            } = element
737            {
738                assert!(in_objstm.is_none());
739                assert!(span.start >= previous_end, "objects come in file order");
740                previous_end = span.end;
741                let slice = &doc.bytes()[span.start as usize..span.end as usize];
742                let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
743                assert_eq!(r2, *r);
744                assert_eq!(object2, *object);
745                object_count += 1;
746            }
747        }
748        // `Xref::len` counts every entry including free ones (e.g. object 0's
749        // free-list head); only non-free entries become `IndirectObject`s.
750        let live_entries = doc
751            .xref()
752            .iter()
753            .filter(|(_, entry)| !matches!(entry, crate::xref::XrefEntry::Free))
754            .count();
755        assert_eq!(object_count, live_entries);
756
757        // Exactly one of each closing element, in order, after the objects.
758        let tail_kinds: Vec<&str> = elements
759            .iter()
760            .filter_map(|e| match e {
761                Element::XrefSection { .. } => Some("xref"),
762                Element::Trailer { .. } => Some("trailer"),
763                Element::StartXref { .. } => Some("startxref"),
764                Element::Eof { .. } => Some("eof"),
765                _ => None,
766            })
767            .collect();
768        assert_eq!(tail_kinds, ["xref", "trailer", "startxref", "eof"]);
769
770        for element in &elements {
771            match element {
772                Element::XrefSection {
773                    kind,
774                    span,
775                    entries,
776                } => {
777                    assert_eq!(*kind, XrefKind::Table);
778                    assert!(*entries > 0);
779                    assert!(doc.bytes()[span.start as usize..].starts_with(b"xref"));
780                }
781                Element::Trailer { dict, span } => {
782                    assert!(dict.get("Root").is_some());
783                    assert!(doc.bytes()[span.start as usize..].starts_with(b"trailer"));
784                }
785                Element::StartXref { offset, span } => {
786                    assert!(doc.bytes()[span.start as usize..].starts_with(b"startxref"));
787                    assert!(*offset > 0);
788                }
789                Element::Eof { span } => {
790                    assert!(doc.bytes()[span.start as usize..].starts_with(b"%%EOF"));
791                }
792                _ => {}
793            }
794        }
795    }
796
797    #[test]
798    fn objstm_members_follow_their_container() {
799        let data = pdfboss_testkit::objstm_doc(&[(7, "(seven)"), (8, "(eight)")]);
800        let doc = Document::load(data).unwrap();
801        let elements = physical(&doc);
802        let order: Vec<(u32, bool)> = elements
803            .iter()
804            .filter_map(|e| match e {
805                Element::IndirectObject { r, in_objstm, .. } => Some((r.num, in_objstm.is_some())),
806                _ => None,
807            })
808            .collect();
809        // Container 4 comes first (lowest offset), then its members in
810        // index order (1, 2, 3, 7, 8), then the xref stream object 5.
811        assert_eq!(
812            order,
813            [
814                (4, false),
815                (1, true),
816                (2, true),
817                (3, true),
818                (7, true),
819                (8, true),
820                (5, false),
821            ]
822        );
823        // Member spans index into the decoded container and reparse cleanly.
824        for element in &elements {
825            let Element::IndirectObject {
826                object,
827                in_objstm: Some((container, member_span)),
828                ..
829            } = element
830            else {
831                continue;
832            };
833            assert_eq!(*container, ObjRef { num: 4, gen: 0 });
834            let stm = doc.objstm_handle(4).unwrap();
835            let (reparsed, range) = stm
836                .object_spanned(
837                    // Recover the member's index by matching its span.
838                    (0..)
839                        .map(|i| (i, stm.object_spanned(i)))
840                        .take_while(|pair| pair.1.is_ok())
841                        .find(|pair| {
842                            pair.1.as_ref().unwrap().1
843                                == (member_span.start as usize, member_span.end as usize)
844                        })
845                        .map(|pair| pair.0)
846                        .expect("member span maps to an index"),
847                )
848                .unwrap();
849            assert_eq!(reparsed, *object);
850            assert_eq!(
851                range,
852                (member_span.start as usize, member_span.end as usize)
853            );
854        }
855    }
856
857    #[test]
858    fn xref_stream_docs_yield_stream_section_and_synthetic_trailer_span() {
859        let data = pdfboss_testkit::objstm_doc(&[]);
860        let doc = Document::load(data).unwrap();
861        let elements = physical(&doc);
862        let section = elements
863            .iter()
864            .find_map(|e| match e {
865                Element::XrefSection { kind, span, .. } => Some((*kind, *span)),
866                _ => None,
867            })
868            .expect("xref section present");
869        assert_eq!(section.0, XrefKind::Stream);
870        let trailer = elements
871            .iter()
872            .find_map(|e| match e {
873                Element::Trailer { dict, span } => Some((dict.clone(), *span)),
874                _ => None,
875            })
876            .expect("trailer present");
877        assert!(trailer.0.get("Root").is_some());
878        // No classic trailer keyword exists: the trailer span is the newest
879        // xref stream object's span.
880        assert_eq!(trailer.1, section.1);
881    }
882
883    /// Builds a minimal hybrid-reference file: a classic `xref` table whose
884    /// trailer names `/XRefStm`, pointing at a separate cross-reference
885    /// stream object. Adapted from (not shared with) the hybrid fixture in
886    /// `xref::tests`, which is private to its own test module.
887    fn hybrid_xrefstm_doc() -> Vec<u8> {
888        let mut data = b"%PDF-1.5\n".to_vec();
889        let obj1 = data.len();
890        data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
891        let obj2 = data.len();
892        data.extend_from_slice(b"2 0 obj\n(hidden)\nendobj\n");
893        let stm_off = data.len();
894        let mut fields = Vec::new();
895        for offset in [obj2, stm_off] {
896            fields.push(1u8);
897            fields.extend_from_slice(&(offset as u32).to_be_bytes());
898            fields.extend_from_slice(&0u16.to_be_bytes());
899        }
900        data.extend_from_slice(
901            format!(
902                "3 0 obj\n<< /Type /XRef /Size 4 /W [1 4 2] /Index [2 1 3 1] \
903                 /Root 1 0 R /Length {} >>\nstream\n",
904                fields.len()
905            )
906            .as_bytes(),
907        );
908        data.extend_from_slice(&fields);
909        data.extend_from_slice(b"\nendstream\nendobj\n");
910        let classic_off = data.len();
911        data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
912        data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
913        data.extend_from_slice(b"0000000000 00001 f\r\n"); // object 2 hidden
914        data.extend_from_slice(
915            format!("trailer\n<< /Size 4 /Root 1 0 R /XRefStm {stm_off} >>\n").as_bytes(),
916        );
917        data.extend_from_slice(format!("startxref\n{classic_off}\n%%EOF\n").as_bytes());
918        data
919    }
920
921    #[test]
922    fn hybrid_xrefstm_yields_both_sections() {
923        let data = hybrid_xrefstm_doc();
924        let doc = Document::load(data).unwrap();
925        let elements = physical(&doc);
926
927        let sections: Vec<XrefKind> = elements
928            .iter()
929            .filter_map(|e| match e {
930                Element::XrefSection { kind, .. } => Some(*kind),
931                _ => None,
932            })
933            .collect();
934        assert_eq!(
935            sections,
936            [XrefKind::Table, XrefKind::Stream],
937            "classic section first, then its hybrid /XRefStm section"
938        );
939
940        let trailers: Vec<Span> = elements
941            .iter()
942            .filter_map(|e| match e {
943                Element::Trailer { span, .. } => Some(*span),
944                _ => None,
945            })
946            .collect();
947        assert_eq!(trailers.len(), 1, "exactly one trailer element");
948
949        // Independently re-derive the classic section's trailer region and
950        // confirm the emitted Trailer element's span matches it exactly.
951        let startxref = memchr::memmem::rfind(doc.bytes(), b"startxref").unwrap();
952        let mut lexer = Lexer::at(doc.bytes(), startxref + b"startxref".len());
953        let classic_off = match lexer.next_token().unwrap() {
954            Token::Int(v) => v as usize,
955            other => panic!("expected startxref offset, got {other:?}"),
956        };
957        let info = parse_section_at(doc.bytes(), classic_off).unwrap();
958        assert_eq!(info.kind, XrefKind::Table);
959        let expected_trailer_span = info.trailer_span.expect("classic section has a trailer");
960        assert_eq!(trailers[0], expected_trailer_span);
961    }
962
963    #[test]
964    fn broken_object_yields_err_and_iteration_continues() {
965        let mut builder = pdfboss_testkit::PdfBuilder::new();
966        builder.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
967        builder.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
968        builder.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>");
969        builder.object(6, "<< /Broken >>");
970        let mut data = builder.build(1);
971        // Corrupt object 6's header in place: same length, no valid parse.
972        let pos = memchr::memmem::find(&data, b"6 0 obj").unwrap();
973        data[pos..pos + 7].copy_from_slice(b"6 ) obj");
974        let doc = Document::load(data).unwrap();
975        let opts = ElementOpts {
976            logical: false,
977            ..ElementOpts::default()
978        };
979        let items: Vec<Result<Element>> = doc.elements(opts).collect();
980        assert!(
981            items.iter().any(|i| i.is_err()),
982            "corrupt object surfaces as Err"
983        );
984        let good: Vec<u32> = items
985            .iter()
986            .filter_map(|i| match i {
987                Ok(Element::IndirectObject { r, .. }) => Some(r.num),
988                _ => None,
989            })
990            .collect();
991        for num in [1u32, 2, 3] {
992            assert!(good.contains(&num), "object {num} still iterates");
993        }
994        assert!(items.iter().any(|i| matches!(i, Ok(Element::Eof { .. }))));
995    }
996
997    // Covers ISO 32000-1 §12.5.2.
998    #[test]
999    fn logical_walk_reports_page_fonts_images_annots() {
1000        let mut builder = pdfboss_testkit::PdfBuilder::new();
1001        builder.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1002        builder.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1003        builder.object(
1004            3,
1005            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1006             /Resources << /Font << /F1 6 0 R >> /XObject << /Im1 7 0 R >> >> \
1007             /Annots [8 0 R] >>",
1008        );
1009        builder.object(6, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
1010        builder.stream(
1011            7,
1012            "/Type /XObject /Subtype /Image /Width 2 /Height 3 \
1013             /ColorSpace /DeviceGray /BitsPerComponent 8",
1014            &[0, 1, 2, 3, 4, 5],
1015        );
1016        builder.object(8, "<< /Type /Annot /Subtype /Link >>");
1017        let doc = Document::load(builder.build(1)).unwrap();
1018
1019        let opts = ElementOpts {
1020            physical: false,
1021            ..ElementOpts::default()
1022        };
1023        let elements: Vec<Element> = doc.elements(opts).collect::<Result<Vec<_>>>().unwrap();
1024
1025        let kinds: Vec<&str> = elements
1026            .iter()
1027            .map(|e| match e {
1028                Element::Page { .. } => "page",
1029                Element::Font { .. } => "font",
1030                Element::Image { .. } => "image",
1031                Element::Annotation { .. } => "annot",
1032                other => panic!("unexpected element in logical-only walk: {other:?}"),
1033            })
1034            .collect();
1035        assert_eq!(kinds, ["page", "font", "image", "annot"]);
1036
1037        let Element::Page { index, r } = &elements[0] else {
1038            unreachable!()
1039        };
1040        assert_eq!(*index, 0);
1041        assert_eq!(*r, ObjRef { num: 3, gen: 0 });
1042        let Element::Font {
1043            page,
1044            r,
1045            subtype,
1046            base_font,
1047        } = &elements[1]
1048        else {
1049            unreachable!()
1050        };
1051        assert_eq!(*page, Some(0));
1052        assert_eq!(*r, ObjRef { num: 6, gen: 0 });
1053        assert_eq!(subtype.0, "Type1");
1054        assert_eq!(base_font.as_ref().map(|n| n.0.as_str()), Some("Helvetica"));
1055        let Element::Image {
1056            page,
1057            r,
1058            width,
1059            height,
1060        } = &elements[2]
1061        else {
1062            unreachable!()
1063        };
1064        assert_eq!(*page, Some(0));
1065        assert_eq!(*r, ObjRef { num: 7, gen: 0 });
1066        assert_eq!((*width, *height), (2, 3));
1067        let Element::Annotation { page, r, subtype } = &elements[3] else {
1068            unreachable!()
1069        };
1070        assert_eq!(*page, 0);
1071        assert_eq!(*r, ObjRef { num: 8, gen: 0 });
1072        assert_eq!(subtype.0, "Link");
1073    }
1074
1075    #[test]
1076    fn pages_filter_restricts_logical_elements() {
1077        let doc = Document::load(pdfboss_testkit::multi_page_doc(&["a", "b", "c"])).unwrap();
1078        let opts = ElementOpts {
1079            physical: false,
1080            pages: Some(vec![1]),
1081            ..ElementOpts::default()
1082        };
1083        let pages: Vec<usize> = doc
1084            .elements(opts)
1085            .filter_map(|item| match item {
1086                Ok(Element::Page { index, .. }) => Some(index),
1087                _ => None,
1088            })
1089            .collect();
1090        assert_eq!(pages, [1]);
1091    }
1092
1093    #[test]
1094    fn full_walk_yields_physical_then_logical() {
1095        let doc = Document::load(pdfboss_testkit::simple_doc("both")).unwrap();
1096        let elements: Vec<Element> = doc
1097            .elements(ElementOpts::default())
1098            .collect::<Result<Vec<_>>>()
1099            .unwrap();
1100        let eof_pos = elements
1101            .iter()
1102            .position(|e| matches!(e, Element::Eof { .. }))
1103            .expect("eof present");
1104        let first_page = elements
1105            .iter()
1106            .position(|e| matches!(e, Element::Page { .. }))
1107            .expect("page present");
1108        assert!(
1109            first_page > eof_pos,
1110            "logical elements follow physical ones"
1111        );
1112        // simple_doc has a /Font resource: it must surface.
1113        assert!(elements.iter().any(|e| matches!(e, Element::Font { .. })));
1114    }
1115
1116    #[test]
1117    fn content_ops_are_spanned_against_page_content() {
1118        let doc = Document::load(pdfboss_testkit::simple_doc("ops!")).unwrap();
1119        let opts = ElementOpts {
1120            physical: false,
1121            content_ops: true,
1122            ..ElementOpts::default()
1123        };
1124        let ops: Vec<(usize, Op, Span)> = doc
1125            .elements(opts)
1126            .filter_map(|item| match item {
1127                Ok(Element::ContentOp {
1128                    page,
1129                    op,
1130                    span_in_content,
1131                }) => Some((page, op, span_in_content)),
1132                _ => None,
1133            })
1134            .collect();
1135        assert!(!ops.is_empty(), "simple_doc paints text: ops must appear");
1136        let content = doc.page(0).unwrap().content(&doc).unwrap();
1137        for (page, op, span) in &ops {
1138            assert_eq!(*page, 0);
1139            let slice = &content[span.start as usize..span.end as usize];
1140            let reparsed = crate::content::parse_content(slice).unwrap();
1141            assert_eq!(reparsed.len(), 1);
1142            assert_eq!(&reparsed[0], op);
1143        }
1144    }
1145
1146    #[test]
1147    fn content_ops_default_off() {
1148        let doc = Document::load(pdfboss_testkit::simple_doc("quiet")).unwrap();
1149        let none = doc
1150            .elements(ElementOpts::default())
1151            .all(|item| !matches!(item, Ok(Element::ContentOp { .. })));
1152        assert!(none);
1153    }
1154}