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 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 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>> {
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 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 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 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 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 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 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
594fn 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 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
628fn 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
644fn 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
656fn 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
671fn 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 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 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 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 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 (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 assert_eq!(trailer.1, section.1);
881 }
882
883 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"); 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 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 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 #[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 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}