oxidize_pdf/parser/page_tree.rs
1//! PDF Page Tree Parser
2//!
3//! This module handles navigation and extraction of pages from the PDF page tree structure.
4//! The page tree is a hierarchical structure that organizes pages in a PDF document,
5//! allowing for efficient access and inheritance of properties from parent nodes.
6//!
7//! # Overview
8//!
9//! The PDF page tree consists of:
10//! - **Page Tree Nodes**: Internal nodes that can contain other nodes or pages
11//! - **Page Objects**: Leaf nodes representing individual pages
12//! - **Inherited Properties**: Resources, MediaBox, CropBox, and Rotate can be inherited from parent nodes
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use oxidize_pdf::parser::{PdfDocument, PdfReader};
18//!
19//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! // Open a PDF document
21//! let reader = PdfReader::open("document.pdf")?;
22//! let document = PdfDocument::new(reader);
23//!
24//! // Get a specific page
25//! let page = document.get_page(0)?;
26//!
27//! // Access page properties
28//! println!("Page size: {}x{} points", page.width(), page.height());
29//! println!("Rotation: {}°", page.rotation);
30//!
31//! // Get page resources
32//! if let Some(resources) = page.get_resources() {
33//! println!("Page has resources");
34//! }
35//! # Ok(())
36//! # }
37//! ```
38
39use super::document::PdfDocument;
40use super::objects::{PdfArray, PdfDictionary, PdfObject, PdfStream};
41use super::reader::PdfReader;
42use super::{ParseError, ParseResult};
43use std::collections::{HashMap, HashSet};
44use std::io::{Read, Seek};
45
46/// Represents a single page in the PDF with all its properties and resources.
47///
48/// A `ParsedPage` contains all the information needed to render or analyze a PDF page,
49/// including its dimensions, content streams, resources, and inherited properties from
50/// parent page tree nodes.
51///
52/// # Fields
53///
54/// * `obj_ref` - Object reference (object number, generation number) pointing to this page in the PDF
55/// * `dict` - Complete page dictionary containing all page-specific entries
56/// * `inherited_resources` - Resources inherited from parent page tree nodes
57/// * `media_box` - Page dimensions in PDF units [llx, lly, urx, ury]
58/// * `crop_box` - Optional visible area of the page
59/// * `rotation` - Page rotation in degrees (0, 90, 180, or 270)
60///
61/// # Example
62///
63/// ```rust,no_run
64/// use oxidize_pdf::parser::{PdfDocument, PdfReader};
65///
66/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
67/// let reader = PdfReader::open("document.pdf")?;
68/// let document = PdfDocument::new(reader);
69/// let page = document.get_page(0)?;
70///
71/// // Access page properties
72/// let (obj_num, gen_num) = page.obj_ref;
73/// println!("Page object: {} {} R", obj_num, gen_num);
74///
75/// // Get page dimensions
76/// let [llx, lly, urx, ury] = page.media_box;
77/// println!("MediaBox: ({}, {}) to ({}, {})", llx, lly, urx, ury);
78///
79/// // Check for content
80/// if let Some(contents) = page.dict.get("Contents") {
81/// println!("Page has content streams");
82/// }
83/// # Ok(())
84/// # }
85/// ```
86#[derive(Debug, Clone)]
87pub struct ParsedPage {
88 /// Object reference to this page in the form (object_number, generation_number).
89 /// This uniquely identifies the page object in the PDF file.
90 pub obj_ref: (u32, u16),
91
92 /// Page dictionary containing all page-specific entries like Contents, Resources, etc.
93 /// This is the raw PDF dictionary for the page object.
94 pub dict: PdfDictionary,
95
96 /// Resources inherited from parent page tree nodes.
97 /// These are automatically merged during page tree traversal.
98 pub inherited_resources: Option<PdfDictionary>,
99
100 /// MediaBox defining the page dimensions in PDF units (typically points).
101 /// Format: [lower_left_x, lower_left_y, upper_right_x, upper_right_y]
102 pub media_box: [f64; 4],
103
104 /// CropBox defining the visible area of the page.
105 /// If None, the entire MediaBox is visible.
106 pub crop_box: Option<[f64; 4]>,
107
108 /// Page rotation in degrees. Valid values are 0, 90, 180, or 270.
109 /// The rotation is applied clockwise.
110 pub rotation: i32,
111
112 /// Annotations array containing references to annotation objects.
113 /// This is parsed from the page's /Annots entry.
114 pub annotations: Option<PdfArray>,
115}
116
117/// Maximum number of pages to allow in a flat index.
118/// Prevents OOM from malicious /Count values (e.g., 9,999,999,999).
119const MAX_PAGES: usize = 100_000;
120
121/// Page tree navigator
122pub struct PageTree {
123 /// Total number of pages
124 page_count: u32,
125 /// Cached pages by index
126 pages: HashMap<u32, ParsedPage>,
127 /// Root pages dictionary (for navigation)
128 #[allow(dead_code)]
129 pages_dict: Option<PdfDictionary>,
130 /// Flat index of page object references, built once during initialization.
131 /// Each entry is (obj_num, gen_num) for a leaf Page node.
132 page_refs: Vec<(u32, u16)>,
133}
134
135/// Resolve a `/Kids` value into its list of page/pages object references.
136///
137/// The value may be a direct array (`/Kids [ ... ]`) or an indirect reference
138/// to an array (`/Kids N G R`), which ISO 32000-1 §7.3.10 permits for any
139/// object and iText 5.5.9 emits in practice. One level of indirection is
140/// resolved; anything else yields an empty list.
141fn resolve_kids<R: Read + Seek>(
142 reader: &mut PdfReader<R>,
143 kids_obj: Option<&PdfObject>,
144) -> Vec<(u32, u16)> {
145 super::reader::resolve_to_array(reader, kids_obj)
146 .map(|a| a.0.iter().filter_map(|k| k.as_reference()).collect())
147 .unwrap_or_default()
148}
149
150impl PageTree {
151 /// Create a new page tree navigator
152 pub fn new(page_count: u32) -> Self {
153 Self {
154 page_count,
155 pages: HashMap::new(),
156 pages_dict: None,
157 page_refs: Vec::new(),
158 }
159 }
160
161 /// Create a new page tree navigator with pages dictionary
162 pub fn new_with_pages_dict(page_count: u32, pages_dict: PdfDictionary) -> Self {
163 Self {
164 page_count,
165 pages: HashMap::new(),
166 pages_dict: Some(pages_dict),
167 page_refs: Vec::new(),
168 }
169 }
170
171 /// Create a new page tree navigator with a pre-built flat index.
172 /// The page_count is derived from the actual number of leaf pages found.
173 pub fn new_with_flat_index(pages_dict: PdfDictionary, page_refs: Vec<(u32, u16)>) -> Self {
174 let page_count = page_refs.len() as u32;
175 Self {
176 page_count,
177 pages: HashMap::new(),
178 pages_dict: Some(pages_dict),
179 page_refs,
180 }
181 }
182
183 /// Get a cached page by index (0-based)
184 pub fn get_cached_page(&self, index: u32) -> Option<&ParsedPage> {
185 self.pages.get(&index)
186 }
187
188 /// Cache a page
189 pub fn cache_page(&mut self, index: u32, page: ParsedPage) {
190 self.pages.insert(index, page);
191 }
192
193 /// Clear all cached pages
194 pub fn clear_cache(&mut self) {
195 self.pages.clear();
196 }
197
198 /// Get the total page count
199 pub fn page_count(&self) -> u32 {
200 self.page_count
201 }
202
203 /// Get a page object reference from the flat index by page index (0-based).
204 pub fn get_page_ref(&self, index: u32) -> Option<(u32, u16)> {
205 self.page_refs.get(index as usize).copied()
206 }
207
208 /// Flatten the page tree into a `Vec<(u32, u16)>` of leaf Page object references.
209 ///
210 /// This walks the tree iteratively using an explicit stack, with:
211 /// - **Cycle detection**: `HashSet<(u32, u16)>` prevents infinite loops from circular refs
212 /// - **Page cap**: Stops at `MAX_PAGES` to prevent OOM from absurd `/Count` values
213 /// - **Type inference**: Handles missing `/Type` keys by checking for `/Kids`, `/Contents`, `/MediaBox`
214 pub fn flatten_page_tree<R: Read + Seek>(
215 reader: &mut PdfReader<R>,
216 pages_dict: &PdfDictionary,
217 ) -> ParseResult<Vec<(u32, u16)>> {
218 let mut page_refs: Vec<(u32, u16)> = Vec::new();
219 let mut visited: HashSet<(u32, u16)> = HashSet::new();
220
221 // Work stack: each entry is an object reference to process
222 let mut stack: Vec<(u32, u16)> = Vec::new();
223
224 // Seed from root Kids array.
225 // Push in reverse so first kid is processed first (LIFO stack).
226 for kid_ref in resolve_kids(reader, pages_dict.get("Kids"))
227 .into_iter()
228 .rev()
229 {
230 stack.push(kid_ref);
231 }
232
233 while let Some(obj_ref) = stack.pop() {
234 if page_refs.len() >= MAX_PAGES {
235 tracing::warn!("Page tree exceeds {} leaves, truncating", MAX_PAGES);
236 break;
237 }
238
239 // Cycle detection
240 if !visited.insert(obj_ref) {
241 tracing::warn!(
242 "Cycle detected at {} {} R in page tree, skipping",
243 obj_ref.0,
244 obj_ref.1
245 );
246 continue;
247 }
248
249 // Resolve the object. Own it (clone) so the reader borrow ends
250 // here — the `Pages` arm below resolves indirect `/Kids` via the
251 // reader while this node is still in scope.
252 let obj = match reader.get_object(obj_ref.0, obj_ref.1).cloned() {
253 Ok(o) => o,
254 Err(e) => {
255 tracing::warn!(
256 "Failed to resolve page tree node {} {} R: {}",
257 obj_ref.0,
258 obj_ref.1,
259 e
260 );
261 continue;
262 }
263 };
264
265 let dict = match obj.as_dict() {
266 Some(d) => d,
267 None => {
268 // Check if it's a stream with a dict (some PDFs embed page data in streams)
269 if let Some(stream) = obj.as_stream() {
270 &stream.dict
271 } else {
272 continue; // Skip non-dict/non-stream nodes
273 }
274 }
275 };
276
277 // Determine node type
278 let node_type = dict.get_type().or_else(|| {
279 if dict.contains_key("Kids") {
280 Some("Pages")
281 } else if dict.contains_key("Contents") || dict.contains_key("MediaBox") {
282 Some("Page")
283 } else {
284 None
285 }
286 });
287
288 match node_type {
289 Some("Page") => {
290 page_refs.push(obj_ref);
291 }
292 Some("Pages") => {
293 // Push in reverse for correct order. `/Kids` may be an
294 // indirect reference to the array (ISO 32000-1 §7.3.10).
295 for kid_ref in resolve_kids(reader, dict.get("Kids")).into_iter().rev() {
296 stack.push(kid_ref);
297 }
298 }
299 _ => {
300 // Unknown type — treat as Page if it has page-like attributes
301 if dict.contains_key("MediaBox") || dict.contains_key("Contents") {
302 page_refs.push(obj_ref);
303 }
304 // Otherwise silently skip
305 }
306 }
307 }
308
309 Ok(page_refs)
310 }
311
312 /// Load a specific page by traversing the page tree
313 ///
314 /// Note: This method is currently not fully implemented due to architectural constraints
315 /// with recursive page tree traversal and borrow checker issues.
316 #[allow(dead_code)]
317 fn load_page_at_index<R: Read + Seek>(
318 &self,
319 reader: &mut PdfReader<R>,
320 node: &PdfDictionary,
321 node_ref: (u32, u16),
322 target_index: u32,
323 inherited: Option<&PdfDictionary>,
324 ) -> ParseResult<ParsedPage> {
325 let node_type = node
326 .get_type()
327 .or_else(|| {
328 // If Type is missing, try to infer from content
329 if node.contains_key("Kids") && node.contains_key("Count") {
330 Some("Pages")
331 } else if node.contains_key("Contents") || node.contains_key("MediaBox") {
332 Some("Page")
333 } else {
334 None
335 }
336 })
337 .or_else(|| {
338 // If Type is missing and we have lenient parsing, try to infer
339 let lenient_syntax = reader.options().lenient_syntax;
340 let collect_warnings = reader.options().collect_warnings;
341
342 if lenient_syntax || collect_warnings {
343 // If it has Kids, it's likely a Pages node
344 if node.contains_key("Kids") {
345 if collect_warnings {
346 tracing::debug!(
347 "Warning: Inferred Type=Pages for object {} {} R (missing Type field, has Kids)",
348 node_ref.0, node_ref.1
349 );
350 }
351 Some("Pages")
352 }
353 // If it has Contents or MediaBox but no Kids, it's likely a Page
354 else if node.contains_key("Contents")
355 || (node.contains_key("MediaBox") && !node.contains_key("Kids"))
356 {
357 if collect_warnings {
358 tracing::debug!(
359 "Warning: Inferred Type=Page for object {} {} R (missing Type field, has Contents/MediaBox)",
360 node_ref.0, node_ref.1
361 );
362 }
363 Some("Page")
364 } else {
365 None
366 }
367 } else {
368 None
369 }
370 })
371 .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
372
373 match node_type {
374 "Pages" => {
375 // This is a page tree node
376 let kids = node
377 .get("Kids")
378 .and_then(|obj| obj.as_array())
379 .or_else(|| {
380 // If Kids is missing and we have lenient parsing, use empty array
381 if reader.options().lenient_syntax {
382 if reader.options().collect_warnings {
383 tracing::debug!(
384 "Warning: Missing Kids array in Pages node, using empty array"
385 );
386 }
387 Some(&super::objects::EMPTY_PDF_ARRAY)
388 } else {
389 None
390 }
391 })
392 .ok_or_else(|| ParseError::MissingKey("Kids".to_string()))?;
393
394 // Merge inherited attributes
395 let mut merged_inherited = inherited.cloned().unwrap_or_else(PdfDictionary::new);
396
397 // Inheritable attributes: Resources, MediaBox, CropBox, Rotate
398 if let Some(resources) = node.get("Resources") {
399 if !merged_inherited.contains_key("Resources") {
400 merged_inherited.insert("Resources".to_string(), resources.clone());
401 }
402 }
403 if let Some(media_box) = node.get("MediaBox") {
404 if !merged_inherited.contains_key("MediaBox") {
405 merged_inherited.insert("MediaBox".to_string(), media_box.clone());
406 }
407 }
408 if let Some(crop_box) = node.get("CropBox") {
409 if !merged_inherited.contains_key("CropBox") {
410 merged_inherited.insert("CropBox".to_string(), crop_box.clone());
411 }
412 }
413 if let Some(rotate) = node.get("Rotate") {
414 if !merged_inherited.contains_key("Rotate") {
415 merged_inherited.insert("Rotate".to_string(), rotate.clone());
416 }
417 }
418
419 // Find which kid contains our target page
420 let mut current_index = 0;
421 for kid_ref in &kids.0 {
422 let kid_ref =
423 kid_ref
424 .as_reference()
425 .ok_or_else(|| ParseError::SyntaxError {
426 position: 0,
427 message: "Kids array must contain references".to_string(),
428 })?;
429
430 // Get the kid object info first
431 let (_kid_type, count, is_target) = {
432 // Cache parse options to avoid borrow checker issues
433 let lenient_syntax = reader.options().lenient_syntax;
434 let collect_warnings = reader.options().collect_warnings;
435
436 let kid_obj = reader.get_object(kid_ref.0, kid_ref.1)?;
437 let kid_dict =
438 kid_obj.as_dict().ok_or_else(|| ParseError::SyntaxError {
439 position: 0,
440 message: "Page tree node must be a dictionary".to_string(),
441 })?;
442
443 let kid_type = kid_dict
444 .get_type()
445 .or_else(|| {
446 // If Type is missing, try to infer from content
447 if kid_dict.contains_key("Kids") && kid_dict.contains_key("Count") {
448 Some("Pages")
449 } else if kid_dict.contains_key("Contents")
450 || kid_dict.contains_key("MediaBox")
451 {
452 Some("Page")
453 } else {
454 None
455 }
456 })
457 .or_else(|| {
458 // Additional inference for reconstructed/corrupted objects
459 if lenient_syntax || collect_warnings {
460 // If it has Kids, it's likely a Pages node
461 if kid_dict.contains_key("Kids") {
462 if collect_warnings {
463 tracing::debug!(
464 "Warning: Inferred Type=Pages for object {} 0 R (missing Type field, has Kids)",
465 kid_ref.0
466 );
467 }
468 Some("Pages")
469 }
470 // If it has Contents or MediaBox but no Kids, it's likely a Page
471 else if kid_dict.contains_key("Contents")
472 || (kid_dict.contains_key("MediaBox") && !kid_dict.contains_key("Kids"))
473 {
474 if collect_warnings {
475 tracing::debug!(
476 "Warning: Inferred Type=Page for object {} 0 R (missing Type field, has Contents/MediaBox)",
477 kid_ref.0
478 );
479 }
480 Some("Page")
481 } else {
482 None
483 }
484 } else {
485 None
486 }
487 })
488 .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
489
490 let count = if kid_type == "Pages" {
491 // This is another page tree node
492 if let Some(count_obj) = kid_dict.get("Count") {
493 count_obj.as_integer().unwrap_or(0) as u32
494 } else {
495 // Missing Count - use size of Kids array as approximation
496 if let Some(nested_kids_obj) = kid_dict.get("Kids") {
497 if let Some(nested_kids_array) = nested_kids_obj.as_array() {
498 // Use array length as page count approximation
499 nested_kids_array.0.len() as u32
500 } else {
501 1 // Default if Kids is not an array
502 }
503 } else {
504 1 // Default if no Kids array
505 }
506 }
507 } else {
508 // This is a page
509 1
510 };
511
512 let is_target = target_index < current_index + count;
513 (kid_type.to_string(), count, is_target)
514 };
515
516 if is_target {
517 // Found the right subtree/page
518 // Due to borrow checker constraints with recursive calls,
519 // we return a placeholder page for now.
520 // A proper implementation would require refactoring the page tree
521 // traversal to use an iterative approach instead of recursion.
522
523 return Ok(ParsedPage {
524 obj_ref: kid_ref,
525 dict: PdfDictionary::new(),
526 inherited_resources: Some(merged_inherited.clone()),
527 media_box: [0.0, 0.0, 612.0, 792.0],
528 crop_box: None,
529 rotation: 0,
530 annotations: None,
531 });
532 }
533
534 current_index += count;
535 }
536
537 Err(ParseError::SyntaxError {
538 position: 0,
539 message: "Page not found in tree".to_string(),
540 })
541 }
542 "Page" => {
543 // This is a page object
544 if target_index != 0 {
545 return Err(ParseError::SyntaxError {
546 position: 0,
547 message: "Page index mismatch".to_string(),
548 });
549 }
550
551 // Use the object reference passed as parameter
552 let obj_ref = node_ref;
553
554 // Extract page attributes
555 let media_box =
556 Self::get_rectangle(node, inherited, "MediaBox")?.unwrap_or_else(|| {
557 // Use default Letter size if MediaBox is missing
558 #[cfg(debug_assertions)]
559 tracing::debug!(
560 "Warning: Page {} {} R missing MediaBox, using default Letter size",
561 obj_ref.0,
562 obj_ref.1
563 );
564 [0.0, 0.0, 612.0, 792.0]
565 });
566
567 let crop_box = Self::get_rectangle(node, inherited, "CropBox")?;
568
569 let rotation = Self::get_integer(node, inherited, "Rotate")?.unwrap_or(0) as i32;
570
571 // Get resources
572 let inherited_resources = if let Some(inherited) = inherited {
573 inherited
574 .get("Resources")
575 .and_then(|r| r.as_dict())
576 .cloned()
577 } else {
578 None
579 };
580
581 // Get annotations if present
582 let annotations = node.get("Annots").and_then(|obj| obj.as_array()).cloned();
583
584 Ok(ParsedPage {
585 obj_ref,
586 dict: node.clone(),
587 inherited_resources,
588 media_box,
589 crop_box,
590 rotation,
591 annotations,
592 })
593 }
594 _ => Err(ParseError::SyntaxError {
595 position: 0,
596 message: format!("Invalid page tree node type: {node_type}"),
597 }),
598 }
599 }
600
601 /// Get a rectangle value, checking both node and inherited dictionaries
602 #[allow(dead_code)]
603 fn get_rectangle(
604 node: &PdfDictionary,
605 inherited: Option<&PdfDictionary>,
606 key: &str,
607 ) -> ParseResult<Option<[f64; 4]>> {
608 let array = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
609
610 if let Some(array) = array.and_then(|obj| obj.as_array()) {
611 if array.len() != 4 {
612 return Err(ParseError::SyntaxError {
613 position: 0,
614 message: format!("{key} must have 4 elements"),
615 });
616 }
617
618 // Safe: array length is guaranteed to be 4 after validation above
619 let rect = [
620 array.0[0].as_real().unwrap_or(0.0),
621 array.0[1].as_real().unwrap_or(0.0),
622 array.0[2].as_real().unwrap_or(0.0),
623 array.0[3].as_real().unwrap_or(0.0),
624 ];
625
626 Ok(Some(rect))
627 } else {
628 Ok(None)
629 }
630 }
631
632 /// Get an integer value, checking both node and inherited dictionaries
633 #[allow(dead_code)]
634 fn get_integer(
635 node: &PdfDictionary,
636 inherited: Option<&PdfDictionary>,
637 key: &str,
638 ) -> ParseResult<Option<i64>> {
639 let value = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
640
641 Ok(value.and_then(|obj| obj.as_integer()))
642 }
643}
644
645impl ParsedPage {
646 /// Get the effective page width accounting for rotation.
647 ///
648 /// The width is calculated from the MediaBox and adjusted based on the page rotation.
649 /// For 90° or 270° rotations, the width and height are swapped.
650 ///
651 /// # Returns
652 ///
653 /// The page width in PDF units (typically points, where 1 point = 1/72 inch)
654 ///
655 /// # Example
656 ///
657 /// ```rust,no_run
658 /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
659 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
660 /// # let reader = PdfReader::open("document.pdf")?;
661 /// # let document = PdfDocument::new(reader);
662 /// let page = document.get_page(0)?;
663 /// let width_pts = page.width();
664 /// let width_inches = width_pts / 72.0;
665 /// let width_mm = width_pts * 25.4 / 72.0;
666 /// println!("Page width: {} points ({:.2} inches, {:.2} mm)", width_pts, width_inches, width_mm);
667 /// # Ok(())
668 /// # }
669 /// ```
670 pub fn width(&self) -> f64 {
671 match self.rotation {
672 90 | 270 => self.media_box[3] - self.media_box[1],
673 _ => self.media_box[2] - self.media_box[0],
674 }
675 }
676
677 /// Get the effective page height accounting for rotation.
678 ///
679 /// The height is calculated from the MediaBox and adjusted based on the page rotation.
680 /// For 90° or 270° rotations, the width and height are swapped.
681 ///
682 /// # Returns
683 ///
684 /// The page height in PDF units (typically points, where 1 point = 1/72 inch)
685 ///
686 /// # Example
687 ///
688 /// ```rust,no_run
689 /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
690 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
691 /// # let reader = PdfReader::open("document.pdf")?;
692 /// # let document = PdfDocument::new(reader);
693 /// let page = document.get_page(0)?;
694 /// println!("Page dimensions: {}x{} points", page.width(), page.height());
695 /// if page.rotation != 0 {
696 /// println!("Page is rotated {} degrees", page.rotation);
697 /// }
698 /// # Ok(())
699 /// # }
700 /// ```
701 pub fn height(&self) -> f64 {
702 match self.rotation {
703 90 | 270 => self.media_box[2] - self.media_box[0],
704 _ => self.media_box[3] - self.media_box[1],
705 }
706 }
707
708 /// Get the content streams for this page using a PdfReader.
709 ///
710 /// Content streams contain the actual drawing instructions (operators) that render
711 /// text, graphics, and images on the page. A page may have multiple content streams
712 /// which are concatenated during rendering.
713 ///
714 /// # Arguments
715 ///
716 /// * `reader` - Mutable reference to the PDF reader
717 ///
718 /// # Returns
719 ///
720 /// A vector of decompressed content stream data. Each vector contains the raw bytes
721 /// of a content stream ready for parsing.
722 ///
723 /// # Errors
724 ///
725 /// Returns an error if:
726 /// - The Contents entry is malformed
727 /// - Stream decompression fails
728 /// - Referenced objects cannot be resolved
729 ///
730 /// # Example
731 ///
732 /// ```rust,no_run
733 /// # use oxidize_pdf::parser::{PdfReader, ParsedPage};
734 /// # fn example(page: &ParsedPage, reader: &mut PdfReader<std::fs::File>) -> Result<(), Box<dyn std::error::Error>> {
735 /// let streams = page.content_streams(reader)?;
736 /// for (i, stream) in streams.iter().enumerate() {
737 /// println!("Content stream {}: {} bytes", i, stream.len());
738 /// }
739 /// # Ok(())
740 /// # }
741 /// ```
742 pub fn content_streams<R: Read + Seek>(
743 &self,
744 reader: &mut PdfReader<R>,
745 ) -> ParseResult<Vec<Vec<u8>>> {
746 let mut streams = Vec::new();
747
748 if let Some(contents) = self.dict.get("Contents") {
749 // First resolve contents to check its type
750 let contents_type = match contents {
751 PdfObject::Reference(obj_num, gen_num) => {
752 let resolved = reader.get_object(*obj_num, *gen_num)?;
753 match resolved {
754 PdfObject::Stream(_) => "stream",
755 PdfObject::Array(_) => "array",
756 _ => "other",
757 }
758 }
759 PdfObject::Stream(_) => "stream",
760 PdfObject::Array(_) => "array",
761 _ => "other",
762 };
763
764 let options = reader.options().clone();
765 match contents_type {
766 "stream" => {
767 let resolved = reader.resolve(contents)?;
768 if let PdfObject::Stream(stream) = resolved {
769 streams.push(stream.decode(&options)?);
770 }
771 }
772 "array" => {
773 // Get array references first
774 let refs: Vec<(u32, u16)> = {
775 let resolved = reader.resolve(contents)?;
776 if let PdfObject::Array(array) = resolved {
777 array
778 .0
779 .iter()
780 .filter_map(|obj| {
781 if let PdfObject::Reference(num, gen) = obj {
782 Some((*num, *gen))
783 } else {
784 None
785 }
786 })
787 .collect()
788 } else {
789 Vec::new()
790 }
791 };
792
793 // Now resolve each reference
794 for (obj_num, gen_num) in refs {
795 let obj = reader.get_object(obj_num, gen_num)?;
796 if let PdfObject::Stream(stream) = obj {
797 streams.push(stream.decode(&options)?);
798 }
799 }
800 }
801 _ => {
802 return Err(ParseError::SyntaxError {
803 position: 0,
804 message: "Contents must be a stream or array of streams".to_string(),
805 })
806 }
807 }
808 }
809
810 Ok(streams)
811 }
812
813 /// Get content streams using PdfDocument (recommended method).
814 ///
815 /// This is the preferred method for accessing content streams as it uses the
816 /// document's caching and resource management capabilities.
817 ///
818 /// # Arguments
819 ///
820 /// * `document` - Reference to the PDF document
821 ///
822 /// # Returns
823 ///
824 /// A vector of decompressed content stream data ready for parsing with `ContentParser`.
825 ///
826 /// # Example
827 ///
828 /// ```rust,no_run
829 /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
830 /// # use oxidize_pdf::parser::content::ContentParser;
831 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
832 /// let reader = PdfReader::open("document.pdf")?;
833 /// let document = PdfDocument::new(reader);
834 /// let page = document.get_page(0)?;
835 ///
836 /// // Get content streams
837 /// let streams = page.content_streams_with_document(&document)?;
838 ///
839 /// // Parse each stream
840 /// for stream_data in streams {
841 /// let operations = ContentParser::parse_content(&stream_data)?;
842 /// println!("Stream has {} operations", operations.len());
843 /// }
844 /// # Ok(())
845 /// # }
846 /// ```
847 pub fn content_streams_with_document<R: Read + Seek>(
848 &self,
849 document: &PdfDocument<R>,
850 ) -> ParseResult<Vec<Vec<u8>>> {
851 document.get_page_content_streams(self)
852 }
853
854 /// Get the effective resources for this page (including inherited).
855 ///
856 /// Resources include fonts, images (XObjects), color spaces, patterns, and other
857 /// assets needed to render the page. This method returns page-specific resources
858 /// if present, otherwise falls back to inherited resources from parent nodes.
859 ///
860 /// # Returns
861 ///
862 /// The Resources dictionary if available, or None if the page has no resources.
863 ///
864 /// # Resource Categories
865 ///
866 /// The Resources dictionary may contain:
867 /// - `Font` - Font definitions used by text operators
868 /// - `XObject` - External objects (images, form XObjects)
869 /// - `ColorSpace` - Color space definitions
870 /// - `Pattern` - Pattern definitions for fills
871 /// - `Shading` - Shading dictionaries
872 /// - `ExtGState` - Graphics state parameter dictionaries
873 /// - `Properties` - Property list dictionaries
874 ///
875 /// # Example
876 ///
877 /// ```rust,no_run
878 /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
879 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
880 /// # let reader = PdfReader::open("document.pdf")?;
881 /// # let document = PdfDocument::new(reader);
882 /// # let page = document.get_page(0)?;
883 /// if let Some(resources) = page.get_resources() {
884 /// // Check for fonts
885 /// if let Some(fonts) = resources.get("Font").and_then(|f| f.as_dict()) {
886 /// println!("Page uses {} fonts", fonts.0.len());
887 /// }
888 ///
889 /// // Check for images
890 /// if let Some(xobjects) = resources.get("XObject").and_then(|x| x.as_dict()) {
891 /// println!("Page has {} XObjects", xobjects.0.len());
892 /// }
893 /// }
894 /// # Ok(())
895 /// # }
896 /// ```
897 pub fn get_contents(&self) -> Option<&PdfObject> {
898 self.dict.get("Contents")
899 }
900
901 pub fn get_resources(&self) -> Option<&PdfDictionary> {
902 self.dict
903 .get("Resources")
904 .and_then(|r| r.as_dict())
905 .or(self.inherited_resources.as_ref())
906 }
907
908 /// Clone this page with all inherited resources merged into the page dictionary.
909 ///
910 /// This is useful when extracting a page for separate processing or when you need
911 /// a self-contained page object with all resources explicitly included.
912 ///
913 /// # Returns
914 ///
915 /// A cloned page with inherited resources merged into the Resources entry
916 /// of the page dictionary.
917 ///
918 /// # Example
919 ///
920 /// ```rust,no_run
921 /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
922 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
923 /// # let reader = PdfReader::open("document.pdf")?;
924 /// # let document = PdfDocument::new(reader);
925 /// # let page = document.get_page(0)?;
926 /// // Get a self-contained page with all resources
927 /// let standalone_page = page.clone_with_resources();
928 ///
929 /// // The cloned page now has all resources in its dictionary
930 /// assert!(standalone_page.dict.contains_key("Resources"));
931 /// # Ok(())
932 /// # }
933 /// ```
934 pub fn clone_with_resources(&self) -> Self {
935 let mut cloned = self.clone();
936
937 // Merge inherited resources into the page dictionary if needed
938 if let Some(inherited) = &self.inherited_resources {
939 if !cloned.dict.contains_key("Resources") {
940 cloned.dict.insert(
941 "Resources".to_string(),
942 PdfObject::Dictionary(inherited.clone()),
943 );
944 }
945 }
946
947 cloned
948 }
949
950 /// Get the annotations array for this page.
951 ///
952 /// Returns a reference to the annotations array if present.
953 /// Each element in the array is typically a reference to an annotation dictionary.
954 ///
955 /// # Example
956 ///
957 /// ```rust,no_run
958 /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
959 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
960 /// # let reader = PdfReader::open("document.pdf")?;
961 /// # let document = PdfDocument::new(reader);
962 /// # let page = document.get_page(0)?;
963 /// if let Some(annots) = page.get_annotations() {
964 /// println!("Page has {} annotations", annots.len());
965 /// }
966 /// # Ok(())
967 /// # }
968 /// ```
969 pub fn get_annotations(&self) -> Option<&PdfArray> {
970 self.annotations.as_ref()
971 }
972
973 /// Check if the page has annotations.
974 ///
975 /// # Returns
976 ///
977 /// `true` if the page has an annotations array with at least one annotation,
978 /// `false` otherwise.
979 ///
980 /// # Example
981 ///
982 /// ```rust,no_run
983 /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
984 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
985 /// # let reader = PdfReader::open("document.pdf")?;
986 /// # let document = PdfDocument::new(reader);
987 /// # let page = document.get_page(0)?;
988 /// if page.has_annotations() {
989 /// println!("This page contains annotations");
990 /// }
991 /// # Ok(())
992 /// # }
993 /// ```
994 pub fn has_annotations(&self) -> bool {
995 self.annotations
996 .as_ref()
997 .map(|arr| !arr.is_empty())
998 .unwrap_or(false)
999 }
1000
1001 /// Get all objects referenced by this page (for extraction or analysis).
1002 ///
1003 /// This method recursively collects all objects referenced by the page, including:
1004 /// - Content streams
1005 /// - Resources (fonts, images, etc.)
1006 /// - Nested objects within resources
1007 ///
1008 /// This is useful for extracting a complete page with all its dependencies or
1009 /// for analyzing the object graph of a page.
1010 ///
1011 /// # Arguments
1012 ///
1013 /// * `reader` - Mutable reference to the PDF reader
1014 ///
1015 /// # Returns
1016 ///
1017 /// A HashMap mapping object references (obj_num, gen_num) to their resolved objects.
1018 ///
1019 /// # Example
1020 ///
1021 /// ```rust,no_run
1022 /// # use oxidize_pdf::parser::{PdfReader, ParsedPage};
1023 /// # fn example(page: &ParsedPage, reader: &mut PdfReader<std::fs::File>) -> Result<(), Box<dyn std::error::Error>> {
1024 /// let referenced_objects = page.get_referenced_objects(reader)?;
1025 ///
1026 /// println!("Page references {} objects", referenced_objects.len());
1027 /// for ((obj_num, gen_num), obj) in &referenced_objects {
1028 /// println!(" {} {} R: {:?}", obj_num, gen_num, obj);
1029 /// }
1030 /// # Ok(())
1031 /// # }
1032 /// ```
1033 pub fn get_referenced_objects<R: Read + Seek>(
1034 &self,
1035 reader: &mut PdfReader<R>,
1036 ) -> ParseResult<HashMap<(u32, u16), PdfObject>> {
1037 let mut objects = HashMap::new();
1038 let mut to_process = Vec::new();
1039
1040 // Start with Contents
1041 if let Some(contents) = self.dict.get("Contents") {
1042 Self::collect_references(contents, &mut to_process);
1043 }
1044
1045 // Add Resources
1046 if let Some(resources) = self.get_resources() {
1047 for value in resources.0.values() {
1048 Self::collect_references(value, &mut to_process);
1049 }
1050 }
1051
1052 // Process all references
1053 while let Some((obj_num, gen_num)) = to_process.pop() {
1054 if let std::collections::hash_map::Entry::Vacant(e) = objects.entry((obj_num, gen_num))
1055 {
1056 let obj = reader.get_object(obj_num, gen_num)?;
1057
1058 // Collect nested references
1059 Self::collect_references_from_object(obj, &mut to_process);
1060
1061 e.insert(obj.clone());
1062 }
1063 }
1064
1065 Ok(objects)
1066 }
1067
1068 /// Collect object references from a PDF object
1069 fn collect_references(obj: &PdfObject, refs: &mut Vec<(u32, u16)>) {
1070 match obj {
1071 PdfObject::Reference(obj_num, gen_num) => {
1072 refs.push((*obj_num, *gen_num));
1073 }
1074 PdfObject::Array(array) => {
1075 for item in &array.0 {
1076 Self::collect_references(item, refs);
1077 }
1078 }
1079 PdfObject::Dictionary(dict) => {
1080 for value in dict.0.values() {
1081 Self::collect_references(value, refs);
1082 }
1083 }
1084 _ => {}
1085 }
1086 }
1087
1088 /// Collect references from an object (after resolution)
1089 fn collect_references_from_object(obj: &PdfObject, refs: &mut Vec<(u32, u16)>) {
1090 match obj {
1091 PdfObject::Array(array) => {
1092 for item in &array.0 {
1093 Self::collect_references(item, refs);
1094 }
1095 }
1096 PdfObject::Dictionary(dict) | PdfObject::Stream(PdfStream { dict, .. }) => {
1097 for value in dict.0.values() {
1098 Self::collect_references(value, refs);
1099 }
1100 }
1101 _ => {}
1102 }
1103 }
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::super::objects::{PdfArray, PdfDictionary, PdfName, PdfObject};
1109 use super::*;
1110 use std::collections::HashMap;
1111
1112 fn create_test_page() -> ParsedPage {
1113 let mut dict = PdfDictionary(HashMap::new());
1114 dict.0.insert(
1115 PdfName("Type".to_string()),
1116 PdfObject::Name(PdfName("Page".to_string())),
1117 );
1118 dict.0
1119 .insert(PdfName("Parent".to_string()), PdfObject::Reference(2, 0));
1120
1121 ParsedPage {
1122 obj_ref: (3, 0),
1123 dict,
1124 inherited_resources: None,
1125 media_box: [0.0, 0.0, 595.0, 842.0],
1126 crop_box: None,
1127 rotation: 0,
1128 annotations: None,
1129 }
1130 }
1131
1132 fn create_test_page_with_resources() -> ParsedPage {
1133 let mut dict = PdfDictionary(HashMap::new());
1134 dict.0.insert(
1135 PdfName("Type".to_string()),
1136 PdfObject::Name(PdfName("Page".to_string())),
1137 );
1138
1139 let mut resources = PdfDictionary(HashMap::new());
1140 resources.0.insert(
1141 PdfName("Font".to_string()),
1142 PdfObject::Dictionary(PdfDictionary(HashMap::new())),
1143 );
1144
1145 ParsedPage {
1146 obj_ref: (4, 0),
1147 dict,
1148 inherited_resources: Some(resources),
1149 media_box: [0.0, 0.0, 595.0, 842.0],
1150 crop_box: Some([10.0, 10.0, 585.0, 832.0]),
1151 rotation: 90,
1152 annotations: Some(PdfArray(vec![])),
1153 }
1154 }
1155
1156 #[test]
1157 fn test_page_tree_new() {
1158 let tree = PageTree::new(10);
1159 assert_eq!(tree.page_count, 10);
1160 assert_eq!(tree.pages.len(), 0);
1161 assert!(tree.pages_dict.is_none());
1162 }
1163
1164 #[test]
1165 fn test_page_tree_new_with_pages_dict() {
1166 let pages_dict = PdfDictionary(HashMap::new());
1167 let tree = PageTree::new_with_pages_dict(5, pages_dict);
1168 assert_eq!(tree.page_count, 5);
1169 assert_eq!(tree.pages.len(), 0);
1170 assert!(tree.pages_dict.is_some());
1171 }
1172
1173 #[test]
1174 fn test_get_cached_page_empty() {
1175 let tree = PageTree::new(10);
1176 assert!(tree.get_cached_page(0).is_none());
1177 assert!(tree.get_cached_page(5).is_none());
1178 }
1179
1180 #[test]
1181 fn test_cache_and_get_page() {
1182 let mut tree = PageTree::new(10);
1183 let page = create_test_page();
1184
1185 tree.cache_page(0, page);
1186
1187 let cached = tree.get_cached_page(0);
1188 assert!(cached.is_some());
1189 let cached_page = cached.unwrap();
1190 assert_eq!(cached_page.obj_ref, (3, 0));
1191 assert_eq!(cached_page.media_box, [0.0, 0.0, 595.0, 842.0]);
1192 }
1193
1194 #[test]
1195 fn test_cache_multiple_pages() {
1196 let mut tree = PageTree::new(10);
1197 let page1 = create_test_page();
1198 let page2 = create_test_page_with_resources();
1199
1200 tree.cache_page(0, page1);
1201 tree.cache_page(1, page2);
1202
1203 assert!(tree.get_cached_page(0).is_some());
1204 assert!(tree.get_cached_page(1).is_some());
1205 assert!(tree.get_cached_page(2).is_none());
1206
1207 let cached1 = tree.get_cached_page(0).unwrap();
1208 assert_eq!(cached1.rotation, 0);
1209
1210 let cached2 = tree.get_cached_page(1).unwrap();
1211 assert_eq!(cached2.rotation, 90);
1212 }
1213
1214 #[test]
1215 fn test_get_page_count() {
1216 let tree = PageTree::new(25);
1217 assert_eq!(tree.page_count, 25);
1218 }
1219
1220 #[test]
1221 fn test_clear_cache() {
1222 let mut tree = PageTree::new(10);
1223 let page = create_test_page();
1224
1225 tree.cache_page(0, page.clone());
1226 tree.cache_page(1, page);
1227 assert_eq!(tree.pages.len(), 2);
1228
1229 tree.clear_cache();
1230 assert_eq!(tree.pages.len(), 0);
1231 assert!(tree.get_cached_page(0).is_none());
1232 assert!(tree.get_cached_page(1).is_none());
1233 }
1234
1235 #[test]
1236 fn test_parsed_page_properties() {
1237 let page = create_test_page_with_resources();
1238
1239 assert_eq!(page.obj_ref, (4, 0));
1240 assert_eq!(page.rotation, 90);
1241 assert!(page.inherited_resources.is_some());
1242 assert!(page.crop_box.is_some());
1243 assert!(page.annotations.is_some());
1244
1245 let crop_box = page.crop_box.unwrap();
1246 assert_eq!(crop_box, [10.0, 10.0, 585.0, 832.0]);
1247 }
1248
1249 #[test]
1250 fn test_parsed_page_creation() {
1251 let dict = PdfDictionary::new();
1252 let page = ParsedPage {
1253 obj_ref: (1, 0),
1254 dict: dict.clone(),
1255 inherited_resources: None,
1256 media_box: [0.0, 0.0, 612.0, 792.0],
1257 crop_box: None,
1258 rotation: 0,
1259 annotations: None,
1260 };
1261
1262 assert_eq!(page.obj_ref, (1, 0));
1263 assert_eq!(page.dict, dict);
1264 assert!(page.inherited_resources.is_none());
1265 assert_eq!(page.media_box, [0.0, 0.0, 612.0, 792.0]); // Default US Letter
1266 assert!(page.crop_box.is_none());
1267 assert_eq!(page.rotation, 0);
1268 assert!(page.annotations.is_none());
1269 }
1270
1271 #[test]
1272 fn test_parsed_page_width_height() {
1273 let mut page = create_test_page();
1274
1275 // A4 size
1276 assert_eq!(page.width(), 595.0);
1277 assert_eq!(page.height(), 842.0);
1278
1279 // Test with rotation
1280 page.rotation = 90;
1281 // Width and height should swap when rotated
1282 assert_eq!(page.width(), 842.0);
1283 assert_eq!(page.height(), 595.0);
1284
1285 page.rotation = 270;
1286 assert_eq!(page.width(), 842.0);
1287 assert_eq!(page.height(), 595.0);
1288
1289 page.rotation = 180;
1290 assert_eq!(page.width(), 595.0);
1291 assert_eq!(page.height(), 842.0);
1292 }
1293
1294 #[test]
1295 fn test_parsed_page_get_resources() {
1296 let page = create_test_page_with_resources();
1297 let resources = page.get_resources();
1298
1299 assert!(resources.is_some());
1300 let res = resources.unwrap();
1301 assert!(res.contains_key("Font"));
1302 }
1303
1304 #[test]
1305 fn test_parsed_page_get_contents() {
1306 let mut page = create_test_page();
1307
1308 // Add contents to page
1309 page.dict
1310 .insert("Contents".to_string(), PdfObject::Reference(10, 0));
1311
1312 let contents = page.get_contents();
1313 assert!(contents.is_some());
1314 assert_eq!(contents, Some(&PdfObject::Reference(10, 0)));
1315 }
1316
1317 #[test]
1318 fn test_parsed_page_get_annotations() {
1319 let page = create_test_page_with_resources();
1320 let annotations = page.get_annotations();
1321
1322 assert!(annotations.is_some());
1323 if let Some(arr) = annotations {
1324 assert_eq!(arr.0.len(), 0);
1325 }
1326 }
1327
1328 #[test]
1329 fn test_parsed_page_inherited_resources() {
1330 let mut page = create_test_page();
1331 let mut parent_resources = PdfDictionary::new();
1332 parent_resources.insert(
1333 "Font".to_string(),
1334 PdfObject::Dictionary(PdfDictionary::new()),
1335 );
1336
1337 // Directly set inherited resources
1338 page.inherited_resources = Some(parent_resources.clone());
1339
1340 assert!(page.inherited_resources.is_some());
1341 assert_eq!(page.inherited_resources, Some(parent_resources));
1342 }
1343
1344 #[test]
1345 fn test_parsed_page_with_crop_box() {
1346 let mut page = create_test_page();
1347 page.crop_box = Some([50.0, 50.0, 545.0, 792.0]);
1348
1349 // CropBox affects visible area
1350 let crop = page.crop_box.unwrap();
1351 assert_eq!(crop[0], 50.0);
1352 assert_eq!(crop[1], 50.0);
1353 assert_eq!(crop[2], 545.0);
1354 assert_eq!(crop[3], 792.0);
1355 }
1356
1357 #[test]
1358 fn test_page_tree_cache_overflow() {
1359 let mut tree = PageTree::new(100);
1360
1361 // Cache more pages than typical cache size
1362 for i in 0..50 {
1363 let page = create_test_page();
1364 tree.cache_page(i, page);
1365 }
1366
1367 // All pages should be cached
1368 for i in 0..50 {
1369 assert!(tree.get_cached_page(i).is_some());
1370 }
1371 }
1372
1373 #[test]
1374 fn test_page_tree_update_cached_page() {
1375 let mut tree = PageTree::new(10);
1376 let page1 = create_test_page();
1377 let mut page2 = create_test_page();
1378 page2.rotation = 180;
1379
1380 tree.cache_page(0, page1);
1381 let cached = tree.get_cached_page(0).unwrap();
1382 assert_eq!(cached.rotation, 0);
1383
1384 // Update the same page
1385 tree.cache_page(0, page2);
1386 let cached = tree.get_cached_page(0).unwrap();
1387 assert_eq!(cached.rotation, 180);
1388 }
1389
1390 #[test]
1391 fn test_parsed_page_clone() {
1392 let page = create_test_page_with_resources();
1393 let cloned = page.clone();
1394
1395 assert_eq!(page.obj_ref, cloned.obj_ref);
1396 assert_eq!(page.dict, cloned.dict);
1397 assert_eq!(page.inherited_resources, cloned.inherited_resources);
1398 assert_eq!(page.media_box, cloned.media_box);
1399 assert_eq!(page.crop_box, cloned.crop_box);
1400 assert_eq!(page.rotation, cloned.rotation);
1401 assert_eq!(page.annotations, cloned.annotations);
1402 }
1403
1404 #[test]
1405 fn test_page_tree_get_page_bounds() {
1406 let tree = PageTree::new(100);
1407
1408 // Test bounds checking
1409 assert!(tree.get_cached_page(0).is_none()); // Not cached yet
1410 assert!(tree.get_cached_page(99).is_none()); // Within bounds but not cached
1411 assert!(tree.get_cached_page(100).is_none()); // Out of bounds
1412 assert!(tree.get_cached_page(u32::MAX).is_none()); // Way out of bounds
1413 }
1414}
1415
1416#[cfg(test)]
1417#[path = "page_tree_tests.rs"]
1418mod page_tree_tests;