Skip to main content

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;