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                        unreachable!("just matched Stage::Logical");
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    fn page_elements(&self, index: usize) -> VecDeque<Result<Element>> {
456        let mut queue = VecDeque::new();
457        let page = match self.doc.page(index) {
458            Ok(page) => page,
459            Err(err) => {
460                queue.push_back(Err(err));
461                return queue;
462            }
463        };
464        if let Some(r) = page.object_ref() {
465            queue.push_back(Ok(Element::Page { index, r }));
466        }
467        // Fonts: /Resources /Font — a dict of name → (usually) reference.
468        for (r, dict) in self.referenced_dict_entries(page.resources.get("Font")) {
469            let subtype = dict
470                .get_name("Subtype")
471                .cloned()
472                .unwrap_or_else(|| Name(String::new()));
473            let base_font = dict.get_name("BaseFont").cloned();
474            queue.push_back(Ok(Element::Font {
475                page: Some(index),
476                r,
477                subtype,
478                base_font,
479            }));
480        }
481        // Images: /Resources /XObject entries whose /Subtype is /Image.
482        for (r, dict) in self.referenced_dict_entries(page.resources.get("XObject")) {
483            if dict.get_name("Subtype").map(|n| n.0.as_str()) != Some("Image") {
484                continue;
485            }
486            let width = dict.get_int("Width").and_then(|v| u32::try_from(v).ok());
487            let height = dict.get_int("Height").and_then(|v| u32::try_from(v).ok());
488            queue.push_back(Ok(Element::Image {
489                page: Some(index),
490                r,
491                width: width.unwrap_or(0),
492                height: height.unwrap_or(0),
493            }));
494        }
495        // Annotations: the page dict's /Annots array of references.
496        if let Some(annots) = page.dict().get("Annots") {
497            if let Ok(Object::Array(items)) = self.doc.resolve(annots) {
498                for item in items {
499                    let Object::Ref(r) = item else { continue };
500                    let Ok(resolved) = self.doc.resolve(&Object::Ref(r)) else {
501                        continue;
502                    };
503                    let Some(dict) = resolved.as_dict() else {
504                        continue;
505                    };
506                    let subtype = dict
507                        .get_name("Subtype")
508                        .cloned()
509                        .unwrap_or_else(|| Name(String::new()));
510                    queue.push_back(Ok(Element::Annotation {
511                        page: index,
512                        r,
513                        subtype,
514                    }));
515                }
516            }
517        }
518        // Content operators, when requested: parsed against the page's
519        // decoded, concatenated content.
520        if self.opts.content_ops {
521            match page.content(self.doc) {
522                Ok(content) => match crate::content::parse_content_spanned(&content) {
523                    Ok(spanned) => {
524                        for (op, span) in spanned {
525                            queue.push_back(Ok(Element::ContentOp {
526                                page: index,
527                                op,
528                                span_in_content: span,
529                            }));
530                        }
531                    }
532                    Err(err) => queue.push_back(Err(err)),
533                },
534                Err(err) => queue.push_back(Err(err)),
535            }
536        }
537        queue
538    }
539
540    /// Resolves a resource-category value (e.g. the `/Font` entry) to its
541    /// dictionary and yields, in name order, each entry that is a reference
542    /// to a dictionary or stream — as `(reference, dictionary)`. Entries
543    /// inlined without a reference are skipped (they have no identity to
544    /// report); name order keeps iteration deterministic.
545    fn referenced_dict_entries(&self, category: Option<&Object>) -> Vec<(ObjRef, Dict)> {
546        let Some(category) = category else {
547            return Vec::new();
548        };
549        let Ok(resolved) = self.doc.resolve(category) else {
550            return Vec::new();
551        };
552        let Some(dict) = resolved.as_dict() else {
553            return Vec::new();
554        };
555        let mut names: Vec<&Name> = dict.iter().map(|entry| entry.0).collect();
556        names.sort();
557        let mut out = Vec::new();
558        for name in names {
559            let Some(Object::Ref(r)) = dict.get(&name.0) else {
560                continue;
561            };
562            let r = *r;
563            let Ok(target) = self.doc.resolve(&Object::Ref(r)) else {
564                continue;
565            };
566            let Some(target_dict) = target.as_dict() else {
567                continue;
568            };
569            out.push((r, target_dict.clone()));
570        }
571        out
572    }
573
574    /// The file span of an object-stream container, parsed at most once.
575    fn container_span(&mut self, stream_num: u32) -> Result<Span> {
576        if let Some(span) = self.container_spans.get(&stream_num) {
577            return Ok(*span);
578        }
579        let offset = match self.doc.xref().get(stream_num) {
580            Some(XrefEntry::InFile { offset, .. }) => usize::try_from(offset)
581                .ok()
582                .filter(|&o| o < self.doc.bytes().len())
583                .ok_or(Error::ObjectNotFound(stream_num, 0))?,
584            _ => return Err(Error::ObjectNotFound(stream_num, 0)),
585        };
586        let (.., span) = self.doc.object_at_spanned(offset)?;
587        self.container_spans.insert(stream_num, span);
588        Ok(span)
589    }
590}
591
592/// All live objects sorted into file order: in-file objects by offset, then
593/// object-stream members grouped after their container by member index.
594fn build_order(doc: &Document) -> Vec<OrderEntry> {
595    let mut order: Vec<OrderEntry> = doc
596        .xref()
597        .iter()
598        .filter_map(|(num, entry)| match entry {
599            XrefEntry::Free => None,
600            XrefEntry::InFile { offset, .. } => Some(OrderEntry {
601                num,
602                entry,
603                sort_offset: offset,
604                sort_member: 0,
605            }),
606            XrefEntry::InStream { stream_num, index } => {
607                let container_offset = match doc.xref().get(stream_num) {
608                    Some(XrefEntry::InFile { offset, .. }) => offset,
609                    // A member whose container is missing sorts last and
610                    // surfaces as Err from object_element.
611                    None | Some(XrefEntry::Free) | Some(XrefEntry::InStream { .. }) => u64::MAX,
612                };
613                Some(OrderEntry {
614                    num,
615                    entry,
616                    sort_offset: container_offset,
617                    sort_member: 1 + u64::from(index),
618                })
619            }
620        })
621        .collect();
622    order.sort_by_key(|e| (e.sort_offset, e.sort_member, e.num));
623    order
624}
625
626/// The `%PDF-x.y` header element, when a header is physically present.
627fn header_element(doc: &Document) -> Option<Element> {
628    let data = doc.bytes();
629    let window = &data[..data.len().min(1024)];
630    let pos = memchr::memmem::find(window, b"%PDF-")?;
631    let digits_end = window[pos + 5..]
632        .iter()
633        .position(|&b| !(b.is_ascii_digit() || b == b'.'))
634        .map(|rel| pos + 5 + rel)
635        .unwrap_or(window.len());
636    Some(Element::Header {
637        version: doc.version(),
638        span: Span::new(pos as u64, digits_end as u64),
639    })
640}
641
642/// The byte offset announced by the last `startxref` keyword (the offset the
643/// section walk starts from), bounded to the file.
644fn find_startxref_offset(data: &[u8]) -> Option<usize> {
645    let tail = data.len().saturating_sub(64 * 1024);
646    let rel = memchr::memmem::rfind(&data[tail..], b"startxref")?;
647    let mut lexer = Lexer::at(data, tail + rel + b"startxref".len());
648    match lexer.next_token() {
649        Ok(Token::Int(v)) => usize::try_from(v).ok().filter(|&o| o < data.len()),
650        _ => None,
651    }
652}
653
654/// The `startxref` element: keyword through its integer operand.
655fn startxref_element(data: &[u8]) -> Option<Element> {
656    let tail = data.len().saturating_sub(64 * 1024);
657    let rel = memchr::memmem::rfind(&data[tail..], b"startxref")?;
658    let start = tail + rel;
659    let mut lexer = Lexer::at(data, start + b"startxref".len());
660    match lexer.next_token() {
661        Ok(Token::Int(v)) if v >= 0 => Some(Element::StartXref {
662            offset: v as u64,
663            span: Span::new(start as u64, lexer.pos() as u64),
664        }),
665        _ => None,
666    }
667}
668
669/// The last `%%EOF` marker.
670fn eof_element(data: &[u8]) -> Option<Element> {
671    let tail = data.len().saturating_sub(64 * 1024);
672    let rel = memchr::memmem::rfind(&data[tail..], b"%%EOF")?;
673    let start = tail + rel;
674    Some(Element::Eof {
675        span: Span::new(start as u64, (start + b"%%EOF".len()) as u64),
676    })
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682
683    #[test]
684    fn element_opts_defaults() {
685        let opts = ElementOpts::default();
686        assert!(opts.physical);
687        assert!(opts.logical);
688        assert!(opts.pages.is_none());
689        assert!(!opts.content_ops);
690    }
691
692    #[test]
693    fn span_length_and_emptiness() {
694        let span = Span::new(10, 25);
695        assert_eq!(span.len(), 15);
696        assert!(!span.is_empty());
697        assert!(Span::new(7, 7).is_empty());
698        assert_eq!(Span::new(9, 3).len(), 0);
699    }
700
701    use crate::document::Document;
702    use crate::error::Result;
703    use crate::object::ObjRef;
704    use crate::parser::{NoResolve, Parser};
705
706    fn physical(doc: &Document) -> Vec<Element> {
707        let opts = ElementOpts {
708            logical: false,
709            ..ElementOpts::default()
710        };
711        doc.elements(opts).collect::<Result<Vec<_>>>().unwrap()
712    }
713
714    #[test]
715    fn simple_doc_physical_walk() {
716        let data = pdfboss_testkit::simple_doc("walk");
717        let doc = Document::load(data).unwrap();
718        let elements = physical(&doc);
719
720        let Element::Header { version, span } = &elements[0] else {
721            panic!("first element must be the header, got {:?}", elements[0]);
722        };
723        assert_eq!(*version, (1, 7));
724        assert!(doc.bytes()[span.start as usize..].starts_with(b"%PDF-1.7"));
725
726        let mut object_count = 0usize;
727        let mut previous_end = 0u64;
728        for element in &elements {
729            if let Element::IndirectObject {
730                r,
731                object,
732                span,
733                in_objstm,
734            } = element
735            {
736                assert!(in_objstm.is_none());
737                assert!(span.start >= previous_end, "objects come in file order");
738                previous_end = span.end;
739                let slice = &doc.bytes()[span.start as usize..span.end as usize];
740                let (r2, object2) = Parser::new(slice).parse_indirect(&NoResolve).unwrap();
741                assert_eq!(r2, *r);
742                assert_eq!(object2, *object);
743                object_count += 1;
744            }
745        }
746        // `Xref::len` counts every entry including free ones (e.g. object 0's
747        // free-list head); only non-free entries become `IndirectObject`s.
748        let live_entries = doc
749            .xref()
750            .iter()
751            .filter(|(_, entry)| !matches!(entry, crate::xref::XrefEntry::Free))
752            .count();
753        assert_eq!(object_count, live_entries);
754
755        // Exactly one of each closing element, in order, after the objects.
756        let tail_kinds: Vec<&str> = elements
757            .iter()
758            .filter_map(|e| match e {
759                Element::XrefSection { .. } => Some("xref"),
760                Element::Trailer { .. } => Some("trailer"),
761                Element::StartXref { .. } => Some("startxref"),
762                Element::Eof { .. } => Some("eof"),
763                _ => None,
764            })
765            .collect();
766        assert_eq!(tail_kinds, ["xref", "trailer", "startxref", "eof"]);
767
768        for element in &elements {
769            match element {
770                Element::XrefSection {
771                    kind,
772                    span,
773                    entries,
774                } => {
775                    assert_eq!(*kind, XrefKind::Table);
776                    assert!(*entries > 0);
777                    assert!(doc.bytes()[span.start as usize..].starts_with(b"xref"));
778                }
779                Element::Trailer { dict, span } => {
780                    assert!(dict.get("Root").is_some());
781                    assert!(doc.bytes()[span.start as usize..].starts_with(b"trailer"));
782                }
783                Element::StartXref { offset, span } => {
784                    assert!(doc.bytes()[span.start as usize..].starts_with(b"startxref"));
785                    assert!(*offset > 0);
786                }
787                Element::Eof { span } => {
788                    assert!(doc.bytes()[span.start as usize..].starts_with(b"%%EOF"));
789                }
790                _ => {}
791            }
792        }
793    }
794
795    #[test]
796    fn objstm_members_follow_their_container() {
797        let data = pdfboss_testkit::objstm_doc(&[(7, "(seven)"), (8, "(eight)")]);
798        let doc = Document::load(data).unwrap();
799        let elements = physical(&doc);
800        let order: Vec<(u32, bool)> = elements
801            .iter()
802            .filter_map(|e| match e {
803                Element::IndirectObject { r, in_objstm, .. } => Some((r.num, in_objstm.is_some())),
804                _ => None,
805            })
806            .collect();
807        // Container 4 comes first (lowest offset), then its members in
808        // index order (1, 2, 3, 7, 8), then the xref stream object 5.
809        assert_eq!(
810            order,
811            [
812                (4, false),
813                (1, true),
814                (2, true),
815                (3, true),
816                (7, true),
817                (8, true),
818                (5, false),
819            ]
820        );
821        // Member spans index into the decoded container and reparse cleanly.
822        for element in &elements {
823            let Element::IndirectObject {
824                object,
825                in_objstm: Some((container, member_span)),
826                ..
827            } = element
828            else {
829                continue;
830            };
831            assert_eq!(*container, ObjRef { num: 4, gen: 0 });
832            let stm = doc.objstm_handle(4).unwrap();
833            let (reparsed, range) = stm
834                .object_spanned(
835                    // Recover the member's index by matching its span.
836                    (0..)
837                        .map(|i| (i, stm.object_spanned(i)))
838                        .take_while(|pair| pair.1.is_ok())
839                        .find(|pair| {
840                            pair.1.as_ref().unwrap().1
841                                == (member_span.start as usize, member_span.end as usize)
842                        })
843                        .map(|pair| pair.0)
844                        .expect("member span maps to an index"),
845                )
846                .unwrap();
847            assert_eq!(reparsed, *object);
848            assert_eq!(
849                range,
850                (member_span.start as usize, member_span.end as usize)
851            );
852        }
853    }
854
855    #[test]
856    fn xref_stream_docs_yield_stream_section_and_synthetic_trailer_span() {
857        let data = pdfboss_testkit::objstm_doc(&[]);
858        let doc = Document::load(data).unwrap();
859        let elements = physical(&doc);
860        let section = elements
861            .iter()
862            .find_map(|e| match e {
863                Element::XrefSection { kind, span, .. } => Some((*kind, *span)),
864                _ => None,
865            })
866            .expect("xref section present");
867        assert_eq!(section.0, XrefKind::Stream);
868        let trailer = elements
869            .iter()
870            .find_map(|e| match e {
871                Element::Trailer { dict, span } => Some((dict.clone(), *span)),
872                _ => None,
873            })
874            .expect("trailer present");
875        assert!(trailer.0.get("Root").is_some());
876        // No classic trailer keyword exists: the trailer span is the newest
877        // xref stream object's span.
878        assert_eq!(trailer.1, section.1);
879    }
880
881    /// Builds a minimal hybrid-reference file: a classic `xref` table whose
882    /// trailer names `/XRefStm`, pointing at a separate cross-reference
883    /// stream object. Adapted from (not shared with) the hybrid fixture in
884    /// `xref::tests`, which is private to its own test module.
885    fn hybrid_xrefstm_doc() -> Vec<u8> {
886        let mut data = b"%PDF-1.5\n".to_vec();
887        let obj1 = data.len();
888        data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
889        let obj2 = data.len();
890        data.extend_from_slice(b"2 0 obj\n(hidden)\nendobj\n");
891        let stm_off = data.len();
892        let mut fields = Vec::new();
893        for offset in [obj2, stm_off] {
894            fields.push(1u8);
895            fields.extend_from_slice(&(offset as u32).to_be_bytes());
896            fields.extend_from_slice(&0u16.to_be_bytes());
897        }
898        data.extend_from_slice(
899            format!(
900                "3 0 obj\n<< /Type /XRef /Size 4 /W [1 4 2] /Index [2 1 3 1] \
901                 /Root 1 0 R /Length {} >>\nstream\n",
902                fields.len()
903            )
904            .as_bytes(),
905        );
906        data.extend_from_slice(&fields);
907        data.extend_from_slice(b"\nendstream\nendobj\n");
908        let classic_off = data.len();
909        data.extend_from_slice(b"xref\n0 3\n0000000000 65535 f\r\n");
910        data.extend_from_slice(format!("{obj1:010} 00000 n\r\n").as_bytes());
911        data.extend_from_slice(b"0000000000 00001 f\r\n"); // object 2 hidden
912        data.extend_from_slice(
913            format!("trailer\n<< /Size 4 /Root 1 0 R /XRefStm {stm_off} >>\n").as_bytes(),
914        );
915        data.extend_from_slice(format!("startxref\n{classic_off}\n%%EOF\n").as_bytes());
916        data
917    }
918
919    #[test]
920    fn hybrid_xrefstm_yields_both_sections() {
921        let data = hybrid_xrefstm_doc();
922        let doc = Document::load(data).unwrap();
923        let elements = physical(&doc);
924
925        let sections: Vec<XrefKind> = elements
926            .iter()
927            .filter_map(|e| match e {
928                Element::XrefSection { kind, .. } => Some(*kind),
929                _ => None,
930            })
931            .collect();
932        assert_eq!(
933            sections,
934            [XrefKind::Table, XrefKind::Stream],
935            "classic section first, then its hybrid /XRefStm section"
936        );
937
938        let trailers: Vec<Span> = elements
939            .iter()
940            .filter_map(|e| match e {
941                Element::Trailer { span, .. } => Some(*span),
942                _ => None,
943            })
944            .collect();
945        assert_eq!(trailers.len(), 1, "exactly one trailer element");
946
947        // Independently re-derive the classic section's trailer region and
948        // confirm the emitted Trailer element's span matches it exactly.
949        let startxref = memchr::memmem::rfind(doc.bytes(), b"startxref").unwrap();
950        let mut lexer = Lexer::at(doc.bytes(), startxref + b"startxref".len());
951        let classic_off = match lexer.next_token().unwrap() {
952            Token::Int(v) => v as usize,
953            other => panic!("expected startxref offset, got {other:?}"),
954        };
955        let info = parse_section_at(doc.bytes(), classic_off).unwrap();
956        assert_eq!(info.kind, XrefKind::Table);
957        let expected_trailer_span = info.trailer_span.expect("classic section has a trailer");
958        assert_eq!(trailers[0], expected_trailer_span);
959    }
960
961    #[test]
962    fn broken_object_yields_err_and_iteration_continues() {
963        let mut builder = pdfboss_testkit::PdfBuilder::new();
964        builder.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
965        builder.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
966        builder.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>");
967        builder.object(6, "<< /Broken >>");
968        let mut data = builder.build(1);
969        // Corrupt object 6's header in place: same length, no valid parse.
970        let pos = memchr::memmem::find(&data, b"6 0 obj").unwrap();
971        data[pos..pos + 7].copy_from_slice(b"6 ) obj");
972        let doc = Document::load(data).unwrap();
973        let opts = ElementOpts {
974            logical: false,
975            ..ElementOpts::default()
976        };
977        let items: Vec<Result<Element>> = doc.elements(opts).collect();
978        assert!(
979            items.iter().any(|i| i.is_err()),
980            "corrupt object surfaces as Err"
981        );
982        let good: Vec<u32> = items
983            .iter()
984            .filter_map(|i| match i {
985                Ok(Element::IndirectObject { r, .. }) => Some(r.num),
986                _ => None,
987            })
988            .collect();
989        for num in [1u32, 2, 3] {
990            assert!(good.contains(&num), "object {num} still iterates");
991        }
992        assert!(items.iter().any(|i| matches!(i, Ok(Element::Eof { .. }))));
993    }
994
995    #[test]
996    fn logical_walk_reports_page_fonts_images_annots() {
997        let mut builder = pdfboss_testkit::PdfBuilder::new();
998        builder.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
999        builder.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1000        builder.object(
1001            3,
1002            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1003             /Resources << /Font << /F1 6 0 R >> /XObject << /Im1 7 0 R >> >> \
1004             /Annots [8 0 R] >>",
1005        );
1006        builder.object(6, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
1007        builder.stream(
1008            7,
1009            "/Type /XObject /Subtype /Image /Width 2 /Height 3 \
1010             /ColorSpace /DeviceGray /BitsPerComponent 8",
1011            &[0, 1, 2, 3, 4, 5],
1012        );
1013        builder.object(8, "<< /Type /Annot /Subtype /Link >>");
1014        let doc = Document::load(builder.build(1)).unwrap();
1015
1016        let opts = ElementOpts {
1017            physical: false,
1018            ..ElementOpts::default()
1019        };
1020        let elements: Vec<Element> = doc.elements(opts).collect::<Result<Vec<_>>>().unwrap();
1021
1022        let kinds: Vec<&str> = elements
1023            .iter()
1024            .map(|e| match e {
1025                Element::Page { .. } => "page",
1026                Element::Font { .. } => "font",
1027                Element::Image { .. } => "image",
1028                Element::Annotation { .. } => "annot",
1029                other => panic!("unexpected element in logical-only walk: {other:?}"),
1030            })
1031            .collect();
1032        assert_eq!(kinds, ["page", "font", "image", "annot"]);
1033
1034        let Element::Page { index, r } = &elements[0] else {
1035            unreachable!()
1036        };
1037        assert_eq!(*index, 0);
1038        assert_eq!(*r, ObjRef { num: 3, gen: 0 });
1039        let Element::Font {
1040            page,
1041            r,
1042            subtype,
1043            base_font,
1044        } = &elements[1]
1045        else {
1046            unreachable!()
1047        };
1048        assert_eq!(*page, Some(0));
1049        assert_eq!(*r, ObjRef { num: 6, gen: 0 });
1050        assert_eq!(subtype.0, "Type1");
1051        assert_eq!(base_font.as_ref().map(|n| n.0.as_str()), Some("Helvetica"));
1052        let Element::Image {
1053            page,
1054            r,
1055            width,
1056            height,
1057        } = &elements[2]
1058        else {
1059            unreachable!()
1060        };
1061        assert_eq!(*page, Some(0));
1062        assert_eq!(*r, ObjRef { num: 7, gen: 0 });
1063        assert_eq!((*width, *height), (2, 3));
1064        let Element::Annotation { page, r, subtype } = &elements[3] else {
1065            unreachable!()
1066        };
1067        assert_eq!(*page, 0);
1068        assert_eq!(*r, ObjRef { num: 8, gen: 0 });
1069        assert_eq!(subtype.0, "Link");
1070    }
1071
1072    #[test]
1073    fn pages_filter_restricts_logical_elements() {
1074        let doc = Document::load(pdfboss_testkit::multi_page_doc(&["a", "b", "c"])).unwrap();
1075        let opts = ElementOpts {
1076            physical: false,
1077            pages: Some(vec![1]),
1078            ..ElementOpts::default()
1079        };
1080        let pages: Vec<usize> = doc
1081            .elements(opts)
1082            .filter_map(|item| match item {
1083                Ok(Element::Page { index, .. }) => Some(index),
1084                _ => None,
1085            })
1086            .collect();
1087        assert_eq!(pages, [1]);
1088    }
1089
1090    #[test]
1091    fn full_walk_yields_physical_then_logical() {
1092        let doc = Document::load(pdfboss_testkit::simple_doc("both")).unwrap();
1093        let elements: Vec<Element> = doc
1094            .elements(ElementOpts::default())
1095            .collect::<Result<Vec<_>>>()
1096            .unwrap();
1097        let eof_pos = elements
1098            .iter()
1099            .position(|e| matches!(e, Element::Eof { .. }))
1100            .expect("eof present");
1101        let first_page = elements
1102            .iter()
1103            .position(|e| matches!(e, Element::Page { .. }))
1104            .expect("page present");
1105        assert!(
1106            first_page > eof_pos,
1107            "logical elements follow physical ones"
1108        );
1109        // simple_doc has a /Font resource: it must surface.
1110        assert!(elements.iter().any(|e| matches!(e, Element::Font { .. })));
1111    }
1112
1113    #[test]
1114    fn content_ops_are_spanned_against_page_content() {
1115        let doc = Document::load(pdfboss_testkit::simple_doc("ops!")).unwrap();
1116        let opts = ElementOpts {
1117            physical: false,
1118            content_ops: true,
1119            ..ElementOpts::default()
1120        };
1121        let ops: Vec<(usize, Op, Span)> = doc
1122            .elements(opts)
1123            .filter_map(|item| match item {
1124                Ok(Element::ContentOp {
1125                    page,
1126                    op,
1127                    span_in_content,
1128                }) => Some((page, op, span_in_content)),
1129                _ => None,
1130            })
1131            .collect();
1132        assert!(!ops.is_empty(), "simple_doc paints text: ops must appear");
1133        let content = doc.page(0).unwrap().content(&doc).unwrap();
1134        for (page, op, span) in &ops {
1135            assert_eq!(*page, 0);
1136            let slice = &content[span.start as usize..span.end as usize];
1137            let reparsed = crate::content::parse_content(slice).unwrap();
1138            assert_eq!(reparsed.len(), 1);
1139            assert_eq!(&reparsed[0], op);
1140        }
1141    }
1142
1143    #[test]
1144    fn content_ops_default_off() {
1145        let doc = Document::load(pdfboss_testkit::simple_doc("quiet")).unwrap();
1146        let none = doc
1147            .elements(ElementOpts::default())
1148            .all(|item| !matches!(item, Ok(Element::ContentOp { .. })));
1149        assert!(none);
1150    }
1151}