1use super::*;
8
9impl PdfDocument {
10 fn inheritable_value_is_resolvable(&self, value: &Object) -> bool {
11 let Some(reference) = value.as_reference() else {
12 return true;
13 };
14 matches!(self.load_object(reference), Ok(resolved) if !matches!(resolved, Object::Null | Object::Reference(_)))
16 }
17
18 pub fn page_count(&self) -> Result<usize> {
42 let primary: Result<usize> = match self.get_page_count_standard() {
43 Ok(count) => {
44 tracing::debug!(target: LOG_TARGET, "Page count from /Count: {}", count);
45 Ok(count)
46 }
47 Err(Error::EncryptedPdf) => return Err(Error::EncryptedPdf),
48 Err(e) => {
49 if self.is_encrypted() {
52 trace_recoverable_pdf_error("count_encrypted_pages", &e);
53 return Err(Error::EncryptedPdf);
54 }
55 trace_recoverable_pdf_error("count_pages_from_tree", &e);
56 match self.get_page_count_by_scanning() {
57 Ok(count) => {
58 tracing::warn!(target: LOG_TARGET, "Page count from scanning: {}", count);
59 Ok(count)
60 }
61 Err(scan_err) => {
62 tracing::error!(target: LOG_TARGET,
63 operation = "count_pages",
64 primary_error_code = e.telemetry_code(),
65 primary_error_offset = ?e.telemetry_offset(),
66 fallback_error_code = scan_err.telemetry_code(),
67 fallback_error_offset = ?scan_err.telemetry_offset(),
68 "all PDF page count strategies failed"
69 );
70 Err(e)
71 }
72 }
73 }
74 };
75
76 if matches!(primary, Ok(0)) && !self.is_encrypted() {
87 let mut n = 0usize;
100 while n < 1_000_000 && self.get_page(n).is_ok() {
101 n += 1;
102 }
103 if n > 0 {
104 tracing::warn!(target: LOG_TARGET, "Page /Count was 0; enumerated {} pages via get_page", n);
105 return Ok(n);
106 }
107 }
108 primary
109 }
110
111 pub fn get_page_media_box(&self, page_index: usize) -> Result<(f32, f32, f32, f32)> {
115 let page = self.get_page(page_index)?;
116 let page_dict = page
117 .as_dict()
118 .ok_or_else(|| Error::InvalidPdf("Page is not a dictionary".to_string()))?;
119
120 let media_box_obj_raw = page_dict
124 .get("MediaBox")
125 .ok_or_else(|| Error::InvalidPdf("MediaBox not found or not an array".to_string()))?;
126 let media_box_obj = self.resolve_obj_ref(media_box_obj_raw);
127 let media_box = media_box_obj
128 .as_array()
129 .ok_or_else(|| Error::InvalidPdf("MediaBox not found or not an array".to_string()))?;
130
131 if media_box.len() < 4 {
132 return Err(Error::InvalidPdf("MediaBox must have at least 4 elements".to_string()));
133 }
134
135 fn to_f32(obj: &Object) -> f32 {
136 match obj {
137 Object::Integer(v) => *v as f32,
138 Object::Real(v) => *v as f32,
139 _ => 0.0,
140 }
141 }
142
143 Ok((
149 to_f32(&self.resolve_obj_ref(&media_box[0])),
150 to_f32(&self.resolve_obj_ref(&media_box[1])),
151 to_f32(&self.resolve_obj_ref(&media_box[2])),
152 to_f32(&self.resolve_obj_ref(&media_box[3])),
153 ))
154 }
155
156 pub fn get_page_rotation(&self, page_index: usize) -> Result<i32> {
166 let page = self.get_page(page_index)?;
167 let dict = page
168 .as_dict()
169 .ok_or_else(|| Error::InvalidPdf("Page is not a dictionary".to_string()))?;
170 let raw = match dict.get("Rotate") {
171 Some(r) => match self.resolve_obj_ref(r) {
172 Object::Integer(v) => v as i32,
173 Object::Real(v) => v as i32,
174 _ => 0,
175 },
176 None => 0,
177 };
178 let n = ((raw % 360) + 360) % 360;
182 Ok(if n % 90 == 0 { n } else { 0 })
183 }
184
185 pub fn get_page_rotation_status(&self, page_index: usize) -> Result<PageRotation> {
190 let page = self.get_page(page_index)?;
191 let dict = page
192 .as_dict()
193 .ok_or_else(|| Error::InvalidPdf("Page is not a dictionary".to_string()))?;
194 let Some(r) = dict.get("Rotate") else {
195 return Ok(PageRotation::Absent);
196 };
197 let raw = match self.resolve_obj_ref(r) {
198 Object::Integer(v) => v as i32,
199 Object::Real(v) if v.fract() == 0.0 => v as i32,
204 _ => return Ok(PageRotation::Malformed),
205 };
206 let n = ((raw % 360) + 360) % 360;
207 Ok(if n % 90 == 0 {
208 PageRotation::Valid(n)
209 } else {
210 PageRotation::Malformed
211 })
212 }
213
214 pub(super) fn get_page_count_standard(&self) -> Result<usize> {
216 let catalog = self.catalog()?;
217 let catalog_dict = catalog.as_dict().ok_or_else(|| Error::InvalidObjectType {
218 expected: "Dictionary".to_string(),
219 found: catalog.type_name().to_string(),
220 })?;
221
222 let pages_ref = catalog_dict
223 .get("Pages")
224 .ok_or_else(|| Error::InvalidPdf("Catalog missing /Pages entry".to_string()))?
225 .as_reference()
226 .ok_or_else(|| Error::InvalidPdf("/Pages is not a reference".to_string()))?;
227
228 let pages_obj = self.load_object(pages_ref)?;
229 let pages_dict = match pages_obj.as_dict() {
230 Some(d) => d,
231 None => {
232 if matches!(pages_obj, crate::object::Object::Null) && self.is_encrypted() {
236 return Err(Error::EncryptedPdf);
237 }
238 tracing::warn!(target: LOG_TARGET,
239 "Page tree root is {} (expected Dictionary), treating as 0 pages",
240 pages_obj.type_name()
241 );
242 return Ok(0);
243 }
244 };
245
246 let count = pages_dict
247 .get("Count")
248 .ok_or_else(|| Error::InvalidPdf("Page tree missing /Count entry".to_string()))?
249 .as_integer()
250 .ok_or_else(|| Error::InvalidPdf("/Count is not an integer".to_string()))?;
251
252 const MAX_PAGES: i64 = 8_388_607;
254 if !(0..=MAX_PAGES).contains(&count) {
255 tracing::warn!(target: LOG_TARGET,
256 "/Count value {} is unreasonable (max {}), falling back to tree scan",
257 count,
258 MAX_PAGES
259 );
260 return self.get_page_count_by_scanning();
261 }
262
263 let max_objects = self.xref.len();
264 if (count as usize) > max_objects {
265 tracing::warn!(target: LOG_TARGET,
266 "/Count {} exceeds total objects {}, falling back to tree scan",
267 count,
268 max_objects
269 );
270 return self.get_page_count_by_scanning();
271 }
272
273 Ok(count as usize)
274 }
275
276 fn get_page_count_by_scanning(&self) -> Result<usize> {
278 let catalog = self.catalog()?;
279 let catalog_dict = catalog.as_dict().ok_or_else(|| Error::InvalidObjectType {
280 expected: "Dictionary".to_string(),
281 found: catalog.type_name().to_string(),
282 })?;
283
284 let pages_ref = catalog_dict
285 .get("Pages")
286 .ok_or_else(|| Error::InvalidPdf("Catalog missing /Pages entry".to_string()))?
287 .as_reference()
288 .ok_or_else(|| Error::InvalidPdf("/Pages is not a reference".to_string()))?;
289
290 self.count_pages_recursive(pages_ref, 0)
291 }
292
293 pub(super) fn count_pages_recursive(&self, node_ref: ObjectRef, depth: u32) -> Result<usize> {
295 if depth >= MAX_PAGE_TREE_DEPTH {
296 tracing::warn!(target: LOG_TARGET,
297 object_id = node_ref.id,
298 max_depth = MAX_PAGE_TREE_DEPTH,
299 "page tree depth limit exceeded while counting pages"
300 );
301 return Err(Error::RecursionLimitExceeded(MAX_PAGE_TREE_DEPTH));
302 }
303
304 let node = match self.load_object(node_ref) {
305 Ok(n) => n,
306 Err(error) => {
307 tracing::warn!(target: LOG_TARGET,
308 object_id = node_ref.id,
309 generation = node_ref.generation,
310 error_code = error.telemetry_code(),
311 error_offset = ?error.telemetry_offset(),
312 "failed to load page tree node"
313 );
314 return Ok(0);
315 }
316 };
317
318 let node_dict = match node.as_dict() {
319 Some(d) => d,
320 None => {
321 tracing::warn!(target: LOG_TARGET, "Page tree node {} is not a dictionary", node_ref);
322 return Ok(0);
323 }
324 };
325
326 let node_type = node_dict.get("Type").and_then(|obj| obj.as_name());
327
328 match node_type {
329 Some("Page") => Ok(1),
330 Some("Pages") => {
331 let kids = match node_dict.get("Kids").and_then(|obj| obj.as_array()) {
332 Some(k) => k,
333 None => {
334 tracing::warn!(target: LOG_TARGET, "Pages node {} missing /Kids array", node_ref);
335 return Ok(0);
336 }
337 };
338
339 let mut count = 0;
340 for kid in kids {
341 if let Some(kid_ref) = kid.as_reference() {
342 match self.count_pages_recursive(kid_ref, depth + 1) {
343 Ok(page_count) => count += page_count,
344 Err(Error::CircularReference(obj_ref)) => {
345 tracing::warn!(target: LOG_TARGET, "Circular reference in page tree at object {}, skipping", obj_ref);
346 continue;
347 }
348 Err(error) => {
353 tracing::warn!(target: LOG_TARGET,
354 error_code = error.telemetry_code(),
355 error_offset = ?error.telemetry_offset(),
356 "error counting pages in branch; skipping"
357 );
358 continue;
359 }
360 }
361 }
362 }
363 Ok(count)
364 }
365 _ => {
366 tracing::warn!(
367 target: crate::LOG_TARGET_ROOT,
368 operation = "traverse_page_tree",
369 error_code = "unknown_node_type",
370 "skipping unknown page tree node"
371 );
372 Ok(0)
373 }
374 }
375 }
376
377 #[deprecated(since = "0.1.0", note = "Use page_count() instead, which returns Result")]
383 pub fn page_count_u32(&self) -> u32 {
384 self.page_count().unwrap_or(0) as u32
385 }
386
387 pub fn page_indices(&self) -> std::ops::Range<usize> {
406 0..self.page_count().unwrap_or(0)
407 }
408
409 pub fn get_page(&self, page_index: usize) -> Result<Object> {
424 if let Some(cached) = self.page_cache.lock_or_recover().get(&page_index).cloned() {
426 return Ok(cached);
427 }
428
429 const LAZY_THRESHOLD: usize = 64;
431 let cache_misses = self.page_cache.lock_or_recover().len();
432
433 if !self.page_cache_populated.load(Ordering::Acquire) && cache_misses >= LAZY_THRESHOLD {
434 self.page_cache_populated.store(true, Ordering::Release);
435 if let Err(e) = self.populate_page_cache() {
436 trace_recoverable_pdf_error("populate_page_cache", &e);
437 }
438 if let Some(cached) = self.page_cache.lock_or_recover().get(&page_index).cloned() {
439 return Ok(cached);
440 }
441 }
442
443 let catalog = self.catalog()?;
444 let catalog_dict = catalog.as_dict().ok_or_else(|| Error::InvalidObjectType {
445 expected: "Dictionary".to_string(),
446 found: catalog.type_name().to_string(),
447 })?;
448
449 let pages_ref = catalog_dict
450 .get("Pages")
451 .ok_or_else(|| Error::InvalidPdf("Catalog missing /Pages entry".to_string()))?
452 .as_reference()
453 .ok_or_else(|| Error::InvalidPdf("/Pages is not a reference".to_string()))?;
454
455 let mut inherited = HashMap::new();
456
457 let page = match self.get_page_from_tree(pages_ref, page_index, &mut 0, &mut inherited) {
458 Ok(page) => {
459 if let Some(dict) = page.as_dict() {
460 tracing::debug!(target: LOG_TARGET, "Collected page {}, keys: {:?}", page_index, dict.keys());
461 if let Some(contents) = dict.get("Contents") {
462 tracing::debug!(target: LOG_TARGET, " -> /Contents: {:?}", contents);
463 }
464 if let Some(rotate) = dict.get("Rotate") {
465 tracing::debug!(target: LOG_TARGET, " -> /Rotate: {:?}", rotate);
466 }
467 }
468 Ok(page)
469 }
470 Err(error) => {
471 if matches!(
472 error,
473 Error::InvalidPdf(_)
474 | Error::InvalidObjectType { .. }
475 | Error::CircularReference(_)
476 | Error::ObjectNotFound(_, _)
477 ) {
478 trace_recoverable_pdf_error("traverse_page_tree", &error);
479 self.get_page_by_scanning(page_index)
480 } else {
481 Err(error)
482 }
483 }
484 }?;
485
486 self.page_cache.lock_or_recover().insert(page_index, page.clone());
487 Ok(page)
488 }
489
490 fn populate_page_cache(&self) -> Result<()> {
493 let catalog = self.catalog()?;
494 let catalog_dict = catalog.as_dict().ok_or_else(|| Error::InvalidObjectType {
495 expected: "Dictionary".to_string(),
496 found: catalog.type_name().to_string(),
497 })?;
498
499 let pages_ref = catalog_dict
500 .get("Pages")
501 .ok_or_else(|| Error::InvalidPdf("Catalog missing /Pages entry".to_string()))?
502 .as_reference()
503 .ok_or_else(|| Error::InvalidPdf("/Pages is not a reference".to_string()))?;
504
505 let mut page_index = 0usize;
506 let mut inherited = HashMap::new();
507 self.collect_all_pages(pages_ref, &mut page_index, &mut inherited, &mut HashSet::new(), 0)?;
508 tracing::debug!(target: LOG_TARGET, "Populated page cache with {} pages", page_index);
509 Ok(())
510 }
511
512 #[allow(dead_code)]
516 fn prefetch_xobject_subtypes(&self) {
517 let mut xobj_refs: Vec<ObjectRef> = Vec::new();
518 let page_dicts: Vec<Object> = self.page_cache.lock_or_recover().values().cloned().collect();
519
520 for page_obj in &page_dicts {
521 let page_dict = match page_obj.as_dict() {
522 Some(d) => d,
523 None => continue,
524 };
525 let resources = match page_dict.get("Resources") {
526 Some(r) => {
527 if let Some(ref_obj) = r.as_reference() {
528 match self.load_object(ref_obj) {
529 Ok(obj) => obj,
530 Err(_) => continue,
531 }
532 } else {
533 r.clone()
534 }
535 }
536 None => continue,
537 };
538 let res_dict = match resources.as_dict() {
539 Some(d) => d,
540 None => continue,
541 };
542 let xobj_obj = match res_dict.get("XObject") {
543 Some(x) => {
544 if let Some(ref_obj) = x.as_reference() {
545 match self.load_object(ref_obj) {
546 Ok(obj) => obj,
547 Err(_) => continue,
548 }
549 } else {
550 x.clone()
551 }
552 }
553 None => continue,
554 };
555 if let Some(xobj_dict) = xobj_obj.as_dict() {
556 for val in xobj_dict.values() {
557 if let Some(obj_ref) = val.as_reference()
558 && !self.image_xobject_cache.lock_or_recover().contains(&obj_ref)
559 {
560 xobj_refs.push(obj_ref);
561 }
562 }
563 }
564 }
565
566 xobj_refs.sort_unstable_by_key(|r| (r.id, r.generation));
567 xobj_refs.dedup();
568
569 xobj_refs.sort_by_key(|r| self.xref.get(r.id).map(|e| e.offset).unwrap_or(u64::MAX));
570
571 tracing::debug!(target: LOG_TARGET, "Prefetching XObject subtypes for {} unique refs", xobj_refs.len());
572
573 for obj_ref in xobj_refs {
575 self.is_form_xobject(obj_ref);
576 }
577 }
578
579 fn collect_all_pages(
581 &self,
582 node_ref: ObjectRef,
583 page_index: &mut usize,
584 inherited: &mut HashMap<String, Object>,
585 visited: &mut HashSet<ObjectRef>,
586 depth: u32,
587 ) -> Result<()> {
588 if depth >= MAX_PAGE_TREE_DEPTH {
589 tracing::warn!(target: LOG_TARGET,
590 object_id = node_ref.id,
591 max_depth = MAX_PAGE_TREE_DEPTH,
592 "page tree depth limit exceeded while collecting pages"
593 );
594 return Err(Error::RecursionLimitExceeded(MAX_PAGE_TREE_DEPTH));
595 }
596 if !visited.insert(node_ref) {
597 return Err(Error::CircularReference(node_ref));
598 }
599
600 let node = self.load_object(node_ref)?;
601 let node_dict = match node.as_dict() {
602 Some(d) => d,
603 None => return Ok(()),
604 };
605
606 let node_type = node_dict.get("Type").and_then(|obj| obj.as_name()).unwrap_or("");
607
608 match node_type {
609 "Page" => {
610 let mut page_dict = node_dict.clone();
611 for attr_name in &["Resources", "MediaBox", "CropBox", "Rotate"] {
612 if !page_dict
613 .get(*attr_name)
614 .is_some_and(|value| self.inheritable_value_is_resolvable(value))
615 && let Some(inherited_value) = inherited.get(*attr_name)
616 {
617 tracing::debug!(target: LOG_TARGET, "Page {} inheriting {}: {:?}", *page_index, attr_name, inherited_value);
618 page_dict.insert(attr_name.to_string(), inherited_value.clone());
619 }
620 }
621 tracing::debug!(target: LOG_TARGET, "Collected page {}, keys: {:?}", *page_index, page_dict.keys());
622 if let Some(contents) = page_dict.get("Contents") {
623 tracing::debug!(target: LOG_TARGET, " -> /Contents: {:?}", contents);
624 }
625 if let Some(rotate) = page_dict.get("Rotate") {
626 tracing::debug!(target: LOG_TARGET, " -> /Rotate: {:?}", rotate);
627 }
628 self.page_cache
629 .lock_or_recover()
630 .insert(*page_index, Object::Dictionary(page_dict));
631 *page_index += 1;
632 }
633 "Pages" => {
634 let saved = inherited.clone();
636
637 for attr_name in &["Resources", "MediaBox", "CropBox", "Rotate"] {
641 if let Some(attr_value) = node_dict
642 .get(*attr_name)
643 .filter(|value| self.inheritable_value_is_resolvable(value))
644 {
645 tracing::debug!(target: LOG_TARGET,
646 "Pages node at {:?} providing inheritable {}: {:?}",
647 node_ref,
648 attr_name,
649 attr_value
650 );
651 inherited.insert(attr_name.to_string(), attr_value.clone());
652 }
653 }
654
655 if let Some(kids) = node_dict.get("Kids").and_then(|obj| obj.as_array()) {
656 for kid in kids {
657 if let Some(kid_ref) = kid.as_reference()
658 && let Err(error) =
659 self.collect_all_pages(kid_ref, page_index, inherited, visited, depth + 1)
660 {
661 tracing::warn!(target: LOG_TARGET,
662 error_code = error.telemetry_code(),
663 error_offset = ?error.telemetry_offset(),
664 "error collecting page from tree; skipping branch"
665 );
666 }
667 }
668 }
669
670 *inherited = saved;
671 }
672 _ => {}
673 }
674
675 Ok(())
676 }
677
678 fn get_page_by_scanning(&self, target_index: usize) -> Result<Object> {
681 let mut current_index = 0;
682
683 self.recover_from_object_streams();
690
691 let mut obj_nums: Vec<u32> = self.xref.all_object_numbers().collect();
697 for r in self.object_cache.lock_or_recover().keys() {
698 obj_nums.push(r.id);
699 }
700 obj_nums.sort_unstable();
701 obj_nums.dedup();
702
703 for &obj_num in &obj_nums {
704 if let Ok(obj) = self.load_object(ObjectRef {
705 id: obj_num,
706 generation: 0,
707 }) && let Some(dict) = obj.as_dict()
708 && let Some(type_obj) = dict.get("Type")
709 && let Some(type_name) = type_obj.as_name()
710 && type_name == "Page"
711 {
712 if current_index == target_index {
713 return Ok(obj);
714 }
715 current_index += 1;
716 }
717 }
718
719 let mut heuristic_index = current_index;
727 for &obj_num in &obj_nums {
728 if let Ok(obj) = self.load_object(ObjectRef {
729 id: obj_num,
730 generation: 0,
731 }) && let Some(dict) = obj.as_dict()
732 {
733 let has_no_type = dict.get("Type").is_none();
734 let type_is_null = dict.get("Type").is_some_and(|t| matches!(t, Object::Null));
736 if (has_no_type || type_is_null)
737 && (dict.contains_key("MediaBox")
738 || dict.contains_key("Contents")
739 || (dict.contains_key("Resources") && dict.contains_key("Parent")))
740 {
741 tracing::warn!(target: LOG_TARGET,
742 "Heuristic page candidate: object {} (page-like keys without valid /Type)",
743 obj_num
744 );
745 if heuristic_index == target_index {
746 return Ok(obj);
747 }
748 heuristic_index += 1;
749 }
750 }
751 }
752 current_index = heuristic_index;
753
754 if current_index == 0
755 && let Ok(catalog) = self.catalog()
756 && let Some(catalog_dict) = catalog.as_dict()
757 && let Some(pages_ref) = catalog_dict.get("Pages").and_then(|p| p.as_reference())
758 && let Ok(pages_obj) = self.load_object(pages_ref)
759 && let Some(pages_dict) = pages_obj.as_dict()
760 && let Some(kids) = pages_dict.get("Kids").and_then(|k| k.as_array())
761 {
762 let mut kids_index = 0;
763 for kid in kids {
764 if let Some(kid_ref) = kid.as_reference() {
765 if kid_ref == pages_ref {
767 continue;
768 }
769 if let Ok(kid_obj) = self.load_object(kid_ref)
770 && let Some(kid_dict) = kid_obj.as_dict()
771 {
772 let is_pages_node = kid_dict
773 .get("Type")
774 .and_then(|t| t.as_name())
775 .is_some_and(|n| n == "Pages");
776 if is_pages_node {
777 continue;
778 }
779 if kids_index == target_index {
780 tracing::warn!(target: LOG_TARGET,
781 "Found page {} via direct /Kids resolution of object {}",
782 target_index,
783 kid_ref.id
784 );
785 return Ok(kid_obj);
786 }
787 kids_index += 1;
788 }
789 }
790 }
791 }
792
793 Err(Error::InvalidPdf(format!(
794 "Page index {} not found by scanning",
795 target_index
796 )))
797 }
798
799 fn get_page_from_tree(
808 &self,
809 node_ref: ObjectRef,
810 target_index: usize,
811 current_index: &mut usize,
812 inherited: &mut HashMap<String, Object>,
813 ) -> Result<Object> {
814 self.get_page_from_tree_inner(node_ref, target_index, current_index, inherited, &mut HashSet::new(), 0)?
815 .ok_or_else(|| Error::InvalidPdf(format!("Page index {} not found in tree", target_index)))
816 }
817
818 fn get_page_from_tree_inner(
828 &self,
829 node_ref: ObjectRef,
830 target_index: usize,
831 current_index: &mut usize,
832 inherited: &mut HashMap<String, Object>,
833 visited: &mut HashSet<ObjectRef>,
834 depth: u32,
835 ) -> Result<Option<Object>> {
836 if depth >= MAX_PAGE_TREE_DEPTH {
837 tracing::warn!(target: LOG_TARGET,
838 object_id = node_ref.id,
839 max_depth = MAX_PAGE_TREE_DEPTH,
840 "page tree depth limit exceeded while resolving a page"
841 );
842 return Err(Error::RecursionLimitExceeded(MAX_PAGE_TREE_DEPTH));
843 }
844 if !visited.insert(node_ref) {
845 return Err(Error::CircularReference(node_ref));
846 }
847 let node = self.load_object(node_ref)?;
848 let node_dict = match node.as_dict() {
849 Some(d) => d,
850 None => {
851 tracing::warn!(target: LOG_TARGET,
852 "Page tree node {} is {} (expected Dictionary), skipping",
853 node_ref.id,
854 node.type_name()
855 );
856 return Err(Error::InvalidPdf(format!(
857 "Page tree node {} is not a dictionary",
858 node_ref.id
859 )));
860 }
861 };
862
863 let node_type = node_dict
864 .get("Type")
865 .and_then(|obj| obj.as_name())
866 .ok_or_else(|| Error::InvalidPdf("Page tree node missing /Type".to_string()))?;
867
868 match node_type {
869 "Pages" if *current_index < target_index => {
870 if let Some(count) = node_dict.get("Count").and_then(|c| c.as_integer()).filter(|&c| c > 0) {
873 let count = count as usize;
874 if *current_index + count <= target_index {
875 *current_index += count;
876 return Ok(None);
877 }
878 }
879 }
880 _ => {}
881 }
882
883 match node_type {
884 "Page" => {
885 if *current_index == target_index {
886 let mut page_dict = node_dict.clone();
890
891 let inheritable_attrs = ["Resources", "MediaBox", "CropBox", "Rotate"];
897
898 for attr_name in &inheritable_attrs {
899 if !page_dict
900 .get(*attr_name)
901 .is_some_and(|value| self.inheritable_value_is_resolvable(value))
902 && let Some(inherited_value) = inherited.get(*attr_name)
903 {
904 tracing::debug!(target: LOG_TARGET,
905 "Page {} inheriting /{} from ancestor Pages node",
906 target_index,
907 attr_name
908 );
909 page_dict.insert(attr_name.to_string(), inherited_value.clone());
910 }
911 }
912
913 Ok(Some(Object::Dictionary(page_dict)))
914 } else {
915 *current_index += 1;
916 Ok(None)
917 }
918 }
919 "Pages" => {
920 let inheritable_attrs = ["Resources", "MediaBox", "CropBox", "Rotate"];
921
922 for attr_name in &inheritable_attrs {
923 if let Some(attr_value) = node_dict
924 .get(*attr_name)
925 .filter(|value| self.inheritable_value_is_resolvable(value))
926 {
927 inherited.insert(attr_name.to_string(), attr_value.clone());
928 }
929 }
930
931 let kids = match node_dict.get("Kids").and_then(|obj| obj.as_array()) {
932 Some(k) => k,
933 None => {
934 tracing::warn!(target: LOG_TARGET, "Malformed PDF: Pages node missing /Kids array");
935 return Err(Error::InvalidPdf(
939 "Pages node missing /Kids array - try fallback method".to_string(),
940 ));
941 }
942 };
943
944 for kid in kids {
945 let kid_ref = kid
946 .as_reference()
947 .ok_or_else(|| Error::InvalidPdf("Kid in /Kids array is not a reference".to_string()))?;
948
949 let mut child_inherited = inherited.clone();
951 match self.get_page_from_tree_inner(
952 kid_ref,
953 target_index,
954 current_index,
955 &mut child_inherited,
956 visited,
957 depth + 1,
958 ) {
959 Ok(Some(page)) => return Ok(Some(page)),
960 Ok(None) => continue,
964 Err(Error::CircularReference(obj_ref)) => {
965 tracing::warn!(target: LOG_TARGET, "Circular reference in page tree at object {}, skipping", obj_ref);
966 continue;
967 }
968 Err(error) => {
974 tracing::warn!(target: LOG_TARGET,
975 error_code = error.telemetry_code(),
976 error_offset = ?error.telemetry_offset(),
977 "error walking to page in tree; skipping branch"
978 );
979 continue;
980 }
981 }
982 }
983
984 Ok(None)
985 }
986 _ => Err(Error::InvalidPdf(format!("Unknown page tree node type: {}", node_type))),
987 }
988 }
989
990 pub(crate) fn get_page_ref(&self, page_index: usize) -> Result<ObjectRef> {
994 let catalog = self.catalog()?;
995 let catalog_dict = catalog.as_dict().ok_or_else(|| Error::InvalidObjectType {
996 expected: "Dictionary".to_string(),
997 found: catalog.type_name().to_string(),
998 })?;
999
1000 let pages_ref = catalog_dict
1001 .get("Pages")
1002 .ok_or_else(|| Error::InvalidPdf("Catalog missing /Pages entry".to_string()))?
1003 .as_reference()
1004 .ok_or_else(|| Error::InvalidPdf("/Pages is not a reference".to_string()))?;
1005
1006 self.get_page_ref_recursive(pages_ref, page_index, &mut 0, &mut HashSet::new(), 0)?
1007 .ok_or_else(|| Error::InvalidPdf(format!("Page {} not found", page_index)))
1008 }
1009
1010 pub(crate) fn get_page_ref_recursive(
1016 &self,
1017 node_ref: ObjectRef,
1018 target_index: usize,
1019 current_index: &mut usize,
1020 visited: &mut HashSet<ObjectRef>,
1021 depth: u32,
1022 ) -> Result<Option<ObjectRef>> {
1023 if depth >= MAX_PAGE_TREE_DEPTH {
1024 tracing::warn!(target: LOG_TARGET,
1025 object_id = node_ref.id,
1026 max_depth = MAX_PAGE_TREE_DEPTH,
1027 "page tree depth limit exceeded while resolving a page reference"
1028 );
1029 return Err(Error::RecursionLimitExceeded(MAX_PAGE_TREE_DEPTH));
1030 }
1031 if !visited.insert(node_ref) {
1032 return Err(Error::CircularReference(node_ref));
1033 }
1034 let node = self.load_object(node_ref)?;
1035 let node_dict = match node.as_dict() {
1036 Some(d) => d,
1037 None => {
1038 tracing::warn!(target: LOG_TARGET,
1039 "Page tree node {} is {} (expected Dictionary), skipping",
1040 node_ref.id,
1041 node.type_name()
1042 );
1043 return Err(Error::InvalidPdf(format!(
1044 "Page tree node {} is not a dictionary",
1045 node_ref.id
1046 )));
1047 }
1048 };
1049
1050 let node_type = node_dict
1051 .get("Type")
1052 .and_then(|t| t.as_name())
1053 .ok_or_else(|| Error::InvalidPdf("Node missing Type".to_string()))?;
1054
1055 match node_type {
1056 "Page" => {
1057 if *current_index == target_index {
1058 Ok(Some(node_ref))
1059 } else {
1060 *current_index += 1;
1061 Ok(None)
1062 }
1063 }
1064 "Pages" => {
1065 let kids = node_dict
1066 .get("Kids")
1067 .and_then(|k| k.as_array())
1068 .ok_or_else(|| Error::InvalidPdf("Pages node missing Kids".to_string()))?;
1069
1070 for kid_obj in kids {
1071 if let Some(kid_ref) = kid_obj.as_reference() {
1072 match self.get_page_ref_recursive(kid_ref, target_index, current_index, visited, depth + 1) {
1073 Ok(Some(page_ref)) => return Ok(Some(page_ref)),
1074 Ok(None) => continue,
1075 Err(_) => continue,
1076 }
1077 }
1078 }
1079
1080 Ok(None)
1081 }
1082 _ => Err(Error::InvalidPdf(format!("Unknown node type: {}", node_type))),
1083 }
1084 }
1085
1086 pub(crate) fn page_index_of_ref(&self, page_ref: ObjectRef) -> Result<usize> {
1095 self.all_page_refs()?
1096 .iter()
1097 .position(|candidate| *candidate == page_ref)
1098 .ok_or_else(|| Error::InvalidPdf(format!("object {} is not a page in the page tree", page_ref.id)))
1099 }
1100}