1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct Span {
20 pub start: u64,
21 pub end: u64,
22}
23
24impl Span {
25 pub fn new(start: u64, end: u64) -> Span {
27 Span { start, end }
28 }
29
30 pub fn len(&self) -> u64 {
32 self.end.saturating_sub(self.start)
33 }
34
35 pub fn is_empty(&self) -> bool {
37 self.end <= self.start
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum XrefKind {
44 Table,
46 Stream,
48}
49
50#[derive(Debug, Clone)]
52pub enum Element {
53 Header { version: (u8, u8), span: Span },
55 IndirectObject {
57 r: ObjRef,
58 object: Object,
59 span: Span,
62 in_objstm: Option<(ObjRef, Span)>,
65 },
66 XrefSection {
68 kind: XrefKind,
69 span: Span,
70 entries: usize,
71 },
72 Trailer { dict: Dict, span: Span },
79 StartXref { offset: u64, span: Span },
81 Eof { span: Span },
83
84 Page { index: usize, r: ObjRef },
86 Font {
88 page: Option<usize>,
89 r: ObjRef,
90 subtype: Name,
91 base_font: Option<Name>,
92 },
93 Image {
95 page: Option<usize>,
96 r: ObjRef,
97 width: u32,
98 height: u32,
99 },
100 Annotation {
102 page: usize,
103 r: ObjRef,
104 subtype: Name,
105 },
106 ContentOp {
108 page: usize,
109 op: Op,
110 span_in_content: Span,
112 },
113}
114
115#[derive(Debug, Clone)]
117pub struct ElementOpts {
118 pub physical: bool,
120 pub logical: bool,
122 pub pages: Option<Vec<usize>>,
124 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 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
164pub struct Elements<'a> {
166 doc: &'a Document,
167 opts: ElementOpts,
168 stage: Stage,
169 container_spans: FastMap<u32, Span>,
171}
172
173enum Stage {
174 Start,
175 Objects {
176 order: Vec<OrderEntry>,
177 next: usize,
178 },
179 Sections {
180 pending: VecDeque<usize>,
184 visited: Vec<usize>,
185 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
200enum PagePart {
204 PageItself,
205 Drain { queue: VecDeque<Result<Element>> },
206}
207
208struct OrderEntry {
210 num: u32,
211 entry: XrefEntry,
212 sort_offset: u64,
214 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 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; }
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 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 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 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 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 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 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 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 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 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 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 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
592fn 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 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
626fn 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
642fn 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
654fn 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
669fn 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 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 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 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 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 (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 assert_eq!(trailer.1, section.1);
879 }
880
881 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"); 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 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 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 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}