Skip to main content

oxidize_pdf/parser/
document.rs

1//! PDF Document wrapper - High-level interface for PDF parsing and manipulation
2//!
3//! This module provides a robust, high-level interface for working with PDF documents.
4//! It solves Rust's borrow checker challenges through careful use of interior mutability
5//! (RefCell) and separation of concerns between parsing, caching, and page access.
6//!
7//! # Architecture
8//!
9//! The module uses a layered architecture:
10//! - **PdfDocument**: Main entry point with RefCell-based state management
11//! - **ResourceManager**: Centralized object caching with interior mutability
12//! - **PdfReader**: Low-level file access (wrapped in RefCell)
13//! - **PageTree**: Lazy-loaded page navigation
14//!
15//! # Key Features
16//!
17//! - **Automatic caching**: Objects are cached after first access
18//! - **Resource management**: Shared resources are handled efficiently
19//! - **Page navigation**: Fast access to any page in the document
20//! - **Reference resolution**: Automatic resolution of indirect references
21//! - **Text extraction**: Built-in support for extracting text from pages
22//!
23//! # Example
24//!
25//! ```rust,no_run
26//! use oxidize_pdf::parser::{PdfDocument, PdfReader};
27//!
28//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
29//! // Open a PDF document
30//! let reader = PdfReader::open("document.pdf")?;
31//! let document = PdfDocument::new(reader);
32//!
33//! // Get document information
34//! let page_count = document.page_count()?;
35//! let metadata = document.metadata()?;
36//! println!("Title: {:?}", metadata.title);
37//! println!("Pages: {}", page_count);
38//!
39//! // Access a specific page
40//! let page = document.get_page(0)?;
41//! println!("Page size: {}x{}", page.width(), page.height());
42//!
43//! // Extract text from all pages
44//! let extracted_text = document.extract_text()?;
45//! for (i, page_text) in extracted_text.iter().enumerate() {
46//!     println!("Page {}: {}", i + 1, page_text.text);
47//! }
48//! # Ok(())
49//! # }
50//! ```
51
52#[cfg(test)]
53use super::objects::{PdfArray, PdfName};
54use super::objects::{PdfDictionary, PdfObject};
55use super::page_tree::{PageTree, ParsedPage};
56use super::reader::PdfReader;
57use super::{ParseError, ParseOptions, ParseResult};
58use std::cell::RefCell;
59use std::collections::HashMap;
60use std::io::{Read, Seek};
61use std::rc::Rc;
62
63/// Resource manager for efficient PDF object caching.
64///
65/// The ResourceManager provides centralized caching of PDF objects to avoid
66/// repeated parsing and to share resources between different parts of the document.
67/// It uses RefCell for interior mutability, allowing multiple immutable references
68/// to the document while still being able to update the cache.
69///
70/// # Caching Strategy
71///
72/// - Objects are cached on first access
73/// - Cache persists for the lifetime of the document
74/// - Manual cache clearing is supported for memory management
75///
76/// # Example
77///
78/// ```rust,no_run
79/// use oxidize_pdf::parser::document::ResourceManager;
80///
81/// let resources = ResourceManager::new();
82///
83/// // Objects are cached automatically when accessed through PdfDocument
84/// // Manual cache management:
85/// resources.clear_cache(); // Free memory when needed
86/// ```
87pub struct ResourceManager {
88    /// Cached objects indexed by (object_number, generation_number)
89    object_cache: RefCell<HashMap<(u32, u16), PdfObject>>,
90}
91
92impl Default for ResourceManager {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl ResourceManager {
99    /// Create a new resource manager
100    pub fn new() -> Self {
101        Self {
102            object_cache: RefCell::new(HashMap::new()),
103        }
104    }
105
106    /// Get an object from cache if available.
107    ///
108    /// # Arguments
109    ///
110    /// * `obj_ref` - Object reference (object_number, generation_number)
111    ///
112    /// # Returns
113    ///
114    /// Cloned object if cached, None otherwise.
115    ///
116    /// # Example
117    ///
118    /// ```rust,no_run
119    /// # use oxidize_pdf::parser::document::ResourceManager;
120    /// # let resources = ResourceManager::new();
121    /// if let Some(obj) = resources.get_cached((10, 0)) {
122    ///     println!("Object 10 0 R found in cache");
123    /// }
124    /// ```
125    pub fn get_cached(&self, obj_ref: (u32, u16)) -> Option<PdfObject> {
126        self.object_cache.borrow().get(&obj_ref).cloned()
127    }
128
129    /// Cache an object for future access.
130    ///
131    /// # Arguments
132    ///
133    /// * `obj_ref` - Object reference (object_number, generation_number)
134    /// * `obj` - The PDF object to cache
135    ///
136    /// # Example
137    ///
138    /// ```rust,no_run
139    /// # use oxidize_pdf::parser::document::ResourceManager;
140    /// # use oxidize_pdf::parser::objects::PdfObject;
141    /// # let resources = ResourceManager::new();
142    /// resources.cache_object((10, 0), PdfObject::Integer(42));
143    /// ```
144    pub fn cache_object(&self, obj_ref: (u32, u16), obj: PdfObject) {
145        self.object_cache.borrow_mut().insert(obj_ref, obj);
146    }
147
148    /// Clear all cached objects to free memory.
149    ///
150    /// Use this when processing large documents to manage memory usage.
151    ///
152    /// # Example
153    ///
154    /// ```rust,no_run
155    /// # use oxidize_pdf::parser::document::ResourceManager;
156    /// # let resources = ResourceManager::new();
157    /// // After processing many pages
158    /// resources.clear_cache();
159    /// println!("Cache cleared to free memory");
160    /// ```
161    pub fn clear_cache(&self) {
162        self.object_cache.borrow_mut().clear();
163    }
164}
165
166/// High-level PDF document interface for parsing and manipulation.
167///
168/// `PdfDocument` provides a clean, safe API for working with PDF files.
169/// It handles the complexity of PDF structure, object references, and resource
170/// management behind a simple interface.
171///
172/// # Type Parameter
173///
174/// * `R` - The reader type (must implement Read + Seek)
175///
176/// # Architecture Benefits
177///
178/// - **RefCell Usage**: Allows multiple parts of the API to access the document
179/// - **Lazy Loading**: Pages and resources are loaded on demand
180/// - **Automatic Caching**: Frequently accessed objects are cached
181/// - **Safe API**: Borrow checker issues are handled internally
182///
183/// # Example
184///
185/// ```rust,no_run
186/// use oxidize_pdf::parser::{PdfDocument, PdfReader};
187/// use std::fs::File;
188///
189/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
190/// // From a file
191/// let reader = PdfReader::open("document.pdf")?;
192/// let document = PdfDocument::new(reader);
193///
194/// // From any Read + Seek source
195/// let file = File::open("document.pdf")?;
196/// let reader = PdfReader::new(file)?;
197/// let document = PdfDocument::new(reader);
198///
199/// // Use the document
200/// let page_count = document.page_count()?;
201/// for i in 0..page_count {
202///     let page = document.get_page(i)?;
203///     // Process page...
204/// }
205/// # Ok(())
206/// # }
207/// ```
208pub struct PdfDocument<R: Read + Seek> {
209    /// The underlying PDF reader wrapped for interior mutability
210    reader: RefCell<PdfReader<R>>,
211    /// Page tree navigator (lazily initialized)
212    page_tree: RefCell<Option<PageTree>>,
213    /// Shared resource manager for object caching
214    resources: Rc<ResourceManager>,
215    /// Cached document metadata to avoid repeated parsing
216    metadata_cache: RefCell<Option<super::reader::DocumentMetadata>>,
217}
218
219impl<R: Read + Seek> PdfDocument<R> {
220    /// Create a new PDF document from a reader
221    pub fn new(reader: PdfReader<R>) -> Self {
222        Self {
223            reader: RefCell::new(reader),
224            page_tree: RefCell::new(None),
225            resources: Rc::new(ResourceManager::new()),
226            metadata_cache: RefCell::new(None),
227        }
228    }
229
230    /// Get the PDF version of the document.
231    ///
232    /// # Returns
233    ///
234    /// PDF version string (e.g., "1.4", "1.7", "2.0")
235    ///
236    /// # Example
237    ///
238    /// ```rust,no_run
239    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
240    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
241    /// # let reader = PdfReader::open("document.pdf")?;
242    /// # let document = PdfDocument::new(reader);
243    /// let version = document.version()?;
244    /// println!("PDF version: {}", version);
245    /// # Ok(())
246    /// # }
247    /// ```
248    pub fn version(&self) -> ParseResult<String> {
249        Ok(self.reader.borrow().version().to_string())
250    }
251
252    /// Get the parse options
253    pub fn options(&self) -> ParseOptions {
254        self.reader.borrow().options().clone()
255    }
256
257    /// Get the total number of pages in the document.
258    ///
259    /// # Returns
260    ///
261    /// The page count as an unsigned 32-bit integer.
262    ///
263    /// # Errors
264    ///
265    /// Returns an error if the page tree is malformed or missing.
266    ///
267    /// # Example
268    ///
269    /// ```rust,no_run
270    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
271    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
272    /// # let reader = PdfReader::open("document.pdf")?;
273    /// # let document = PdfDocument::new(reader);
274    /// let count = document.page_count()?;
275    /// println!("Document has {} pages", count);
276    ///
277    /// // Iterate through all pages
278    /// for i in 0..count {
279    ///     let page = document.get_page(i)?;
280    ///     // Process page...
281    /// }
282    /// # Ok(())
283    /// # }
284    /// ```
285    pub fn page_count(&self) -> ParseResult<u32> {
286        self.reader.borrow_mut().page_count()
287    }
288
289    /// Get document metadata including title, author, creation date, etc.
290    ///
291    /// Metadata is cached after first access for performance.
292    ///
293    /// # Returns
294    ///
295    /// A `DocumentMetadata` struct containing all available metadata fields.
296    ///
297    /// # Example
298    ///
299    /// ```rust,no_run
300    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
301    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
302    /// # let reader = PdfReader::open("document.pdf")?;
303    /// # let document = PdfDocument::new(reader);
304    /// let metadata = document.metadata()?;
305    ///
306    /// if let Some(title) = &metadata.title {
307    ///     println!("Title: {}", title);
308    /// }
309    /// if let Some(author) = &metadata.author {
310    ///     println!("Author: {}", author);
311    /// }
312    /// if let Some(creation_date) = &metadata.creation_date {
313    ///     println!("Created: {}", creation_date);
314    /// }
315    /// println!("PDF Version: {}", metadata.version);
316    /// # Ok(())
317    /// # }
318    /// ```
319    pub fn metadata(&self) -> ParseResult<super::reader::DocumentMetadata> {
320        // Check cache first
321        if let Some(metadata) = self.metadata_cache.borrow().as_ref() {
322            return Ok(metadata.clone());
323        }
324
325        // Load metadata
326        let metadata = self.reader.borrow_mut().metadata()?;
327        self.metadata_cache.borrow_mut().replace(metadata.clone());
328        Ok(metadata)
329    }
330
331    /// Initialize the page tree if not already done
332    fn ensure_page_tree(&self) -> ParseResult<()> {
333        if self.page_tree.borrow().is_none() {
334            let page_count = self.page_count()?;
335            let pages_dict = self.load_pages_dict()?;
336            let page_tree = PageTree::new_with_pages_dict(page_count, pages_dict);
337            self.page_tree.borrow_mut().replace(page_tree);
338        }
339        Ok(())
340    }
341
342    /// Load the pages dictionary
343    fn load_pages_dict(&self) -> ParseResult<PdfDictionary> {
344        let mut reader = self.reader.borrow_mut();
345        let pages = reader.pages()?;
346        Ok(pages.clone())
347    }
348
349    /// Get a page by index (0-based).
350    ///
351    /// Pages are cached after first access. This method handles page tree
352    /// traversal and property inheritance automatically.
353    ///
354    /// # Arguments
355    ///
356    /// * `index` - Zero-based page index (0 to page_count-1)
357    ///
358    /// # Returns
359    ///
360    /// A complete `ParsedPage` with all properties and inherited resources.
361    ///
362    /// # Errors
363    ///
364    /// Returns an error if:
365    /// - Index is out of bounds
366    /// - Page tree is malformed
367    /// - Required page properties are missing
368    ///
369    /// # Example
370    ///
371    /// ```rust,no_run
372    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
373    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
374    /// # let reader = PdfReader::open("document.pdf")?;
375    /// # let document = PdfDocument::new(reader);
376    /// // Get the first page
377    /// let page = document.get_page(0)?;
378    ///
379    /// // Access page properties
380    /// println!("Page size: {}x{} points", page.width(), page.height());
381    /// println!("Rotation: {}°", page.rotation);
382    ///
383    /// // Get content streams
384    /// let streams = page.content_streams_with_document(&document)?;
385    /// println!("Page has {} content streams", streams.len());
386    /// # Ok(())
387    /// # }
388    /// ```
389    pub fn get_page(&self, index: u32) -> ParseResult<ParsedPage> {
390        self.ensure_page_tree()?;
391
392        // First check if page is already loaded
393        if let Some(page_tree) = self.page_tree.borrow().as_ref() {
394            if let Some(page) = page_tree.get_cached_page(index) {
395                return Ok(page.clone());
396            }
397        }
398
399        // Load the page (reference stack will handle circular detection automatically)
400        let page = self.load_page_at_index(index)?;
401
402        // Cache it
403        if let Some(page_tree) = self.page_tree.borrow_mut().as_mut() {
404            page_tree.cache_page(index, page.clone());
405        }
406
407        Ok(page)
408    }
409
410    /// Load a specific page by index
411    fn load_page_at_index(&self, index: u32) -> ParseResult<ParsedPage> {
412        // Get the pages root
413        let pages_dict = self.load_pages_dict()?;
414
415        // Navigate to the specific page
416        let page_info = self.find_page_in_tree(&pages_dict, index, 0, None)?;
417
418        Ok(page_info)
419    }
420
421    /// Find a page in the page tree (iterative implementation for stack safety)
422    fn find_page_in_tree(
423        &self,
424        root_node: &PdfDictionary,
425        target_index: u32,
426        initial_current_index: u32,
427        initial_inherited: Option<&PdfDictionary>,
428    ) -> ParseResult<ParsedPage> {
429        // Work item for the traversal queue
430        #[derive(Debug)]
431        struct WorkItem {
432            node_dict: PdfDictionary,
433            node_ref: Option<(u32, u16)>,
434            current_index: u32,
435            inherited: Option<PdfDictionary>,
436        }
437
438        // Initialize work queue with root node
439        let mut work_queue = Vec::new();
440        work_queue.push(WorkItem {
441            node_dict: root_node.clone(),
442            node_ref: None,
443            current_index: initial_current_index,
444            inherited: initial_inherited.cloned(),
445        });
446
447        // Iterative traversal
448        while let Some(work_item) = work_queue.pop() {
449            let WorkItem {
450                node_dict,
451                node_ref,
452                current_index,
453                inherited,
454            } = work_item;
455
456            let node_type = node_dict
457                .get_type()
458                .or_else(|| {
459                    // If Type is missing, try to infer from content
460                    if node_dict.contains_key("Kids") && node_dict.contains_key("Count") {
461                        Some("Pages")
462                    } else if node_dict.contains_key("Contents")
463                        || node_dict.contains_key("MediaBox")
464                    {
465                        Some("Page")
466                    } else {
467                        None
468                    }
469                })
470                .or_else(|| {
471                    // If Type is missing, try to infer from structure
472                    if node_dict.contains_key("Kids") {
473                        Some("Pages")
474                    } else if node_dict.contains_key("Contents")
475                        || (node_dict.contains_key("MediaBox") && !node_dict.contains_key("Kids"))
476                    {
477                        Some("Page")
478                    } else {
479                        None
480                    }
481                })
482                .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
483
484            match node_type {
485                "Pages" => {
486                    // This is a page tree node
487                    let kids = node_dict
488                        .get("Kids")
489                        .and_then(|obj| obj.as_array())
490                        .or_else(|| {
491                            // If Kids is missing, use empty array
492                            tracing::debug!(
493                                "Warning: Missing Kids array in Pages node, using empty array"
494                            );
495                            Some(&super::objects::EMPTY_PDF_ARRAY)
496                        })
497                        .ok_or_else(|| ParseError::MissingKey("Kids".to_string()))?;
498
499                    // Merge inherited attributes
500                    let mut merged_inherited = inherited.unwrap_or_else(PdfDictionary::new);
501
502                    // Inheritable attributes
503                    for key in ["Resources", "MediaBox", "CropBox", "Rotate"] {
504                        if let Some(value) = node_dict.get(key) {
505                            if !merged_inherited.contains_key(key) {
506                                merged_inherited.insert(key.to_string(), value.clone());
507                            }
508                        }
509                    }
510
511                    // Process kids in reverse order (since we're using a stack/Vec::pop())
512                    // This ensures we process them in the correct order
513                    let mut current_idx = current_index;
514                    let mut pending_kids = Vec::new();
515
516                    for kid_ref in &kids.0 {
517                        let kid_ref =
518                            kid_ref
519                                .as_reference()
520                                .ok_or_else(|| ParseError::SyntaxError {
521                                    position: 0,
522                                    message: "Kids array must contain references".to_string(),
523                                })?;
524
525                        // Get the kid object
526                        let kid_obj = self.get_object(kid_ref.0, kid_ref.1)?;
527                        let kid_dict = match kid_obj.as_dict() {
528                            Some(dict) => dict,
529                            None => {
530                                // Skip invalid page tree nodes in lenient mode
531                                tracing::debug!(
532                                    "Warning: Page tree node {} {} R is not a dictionary, skipping",
533                                    kid_ref.0,
534                                    kid_ref.1
535                                );
536                                current_idx += 1; // Count as processed but skip
537                                continue;
538                            }
539                        };
540
541                        let kid_type = kid_dict
542                            .get_type()
543                            .or_else(|| {
544                                // If Type is missing, try to infer from content
545                                if kid_dict.contains_key("Kids") && kid_dict.contains_key("Count") {
546                                    Some("Pages")
547                                } else if kid_dict.contains_key("Contents")
548                                    || kid_dict.contains_key("MediaBox")
549                                {
550                                    Some("Page")
551                                } else {
552                                    None
553                                }
554                            })
555                            .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
556
557                        let count = if kid_type == "Pages" {
558                            kid_dict
559                                .get("Count")
560                                .and_then(|obj| obj.as_integer())
561                                .unwrap_or(1) // Fallback to 1 if Count is missing (defensive)
562                                as u32
563                        } else {
564                            1
565                        };
566
567                        if target_index < current_idx + count {
568                            // Found the right subtree/page
569                            if kid_type == "Page" {
570                                // This is the page we want
571                                return self.create_parsed_page(
572                                    kid_ref,
573                                    kid_dict,
574                                    Some(&merged_inherited),
575                                );
576                            } else {
577                                // Need to traverse this subtree - add to queue
578                                pending_kids.push(WorkItem {
579                                    node_dict: kid_dict.clone(),
580                                    node_ref: Some(kid_ref),
581                                    current_index: current_idx,
582                                    inherited: Some(merged_inherited.clone()),
583                                });
584                                break; // Found our target subtree, no need to continue
585                            }
586                        }
587
588                        current_idx += count;
589                    }
590
591                    // Add pending kids to work queue in reverse order for correct processing
592                    work_queue.extend(pending_kids.into_iter().rev());
593                }
594                "Page" => {
595                    // This is a page object
596                    if target_index != current_index {
597                        return Err(ParseError::SyntaxError {
598                            position: 0,
599                            message: "Page index mismatch".to_string(),
600                        });
601                    }
602
603                    // We need the reference for creating the parsed page
604                    if let Some(page_ref) = node_ref {
605                        return self.create_parsed_page(page_ref, &node_dict, inherited.as_ref());
606                    } else {
607                        return Err(ParseError::SyntaxError {
608                            position: 0,
609                            message: "Direct page object without reference".to_string(),
610                        });
611                    }
612                }
613                _ => {
614                    return Err(ParseError::SyntaxError {
615                        position: 0,
616                        message: format!("Invalid page tree node type: {node_type}"),
617                    });
618                }
619            }
620        }
621
622        // Try fallback: search for the page by direct object scanning
623        tracing::debug!(
624            "Warning: Page {} not found in tree, attempting direct lookup",
625            target_index
626        );
627
628        // Scan for Page objects directly (try first few hundred objects)
629        for obj_num in 1..500 {
630            if let Ok(obj) = self.reader.borrow_mut().get_object(obj_num, 0) {
631                if let Some(dict) = obj.as_dict() {
632                    if let Some(obj_type) = dict.get("Type").and_then(|t| t.as_name()) {
633                        if obj_type.0 == "Page" {
634                            // Found a page, check if it's the right index (approximate)
635                            return self.create_parsed_page((obj_num, 0), dict, None);
636                        }
637                    }
638                }
639            }
640        }
641
642        Err(ParseError::SyntaxError {
643            position: 0,
644            message: format!("Page {} not found in tree or document", target_index),
645        })
646    }
647
648    /// Create a ParsedPage from a page dictionary
649    fn create_parsed_page(
650        &self,
651        obj_ref: (u32, u16),
652        page_dict: &PdfDictionary,
653        inherited: Option<&PdfDictionary>,
654    ) -> ParseResult<ParsedPage> {
655        // Extract page attributes with fallback for missing MediaBox
656        let media_box = match self.get_rectangle(page_dict, inherited, "MediaBox")? {
657            Some(mb) => mb,
658            None => {
659                // Use default Letter size if MediaBox is missing
660                #[cfg(debug_assertions)]
661                tracing::debug!(
662                    "Warning: Page {} {} R missing MediaBox, using default Letter size",
663                    obj_ref.0,
664                    obj_ref.1
665                );
666                [0.0, 0.0, 612.0, 792.0]
667            }
668        };
669
670        let crop_box = self.get_rectangle(page_dict, inherited, "CropBox")?;
671
672        let rotation = self
673            .get_integer(page_dict, inherited, "Rotate")?
674            .unwrap_or(0) as i32;
675
676        // Get inherited resources
677        let inherited_resources = if let Some(inherited) = inherited {
678            inherited
679                .get("Resources")
680                .and_then(|r| r.as_dict())
681                .cloned()
682        } else {
683            None
684        };
685
686        // Get annotations if present
687        let annotations = page_dict
688            .get("Annots")
689            .and_then(|obj| obj.as_array())
690            .cloned();
691
692        Ok(ParsedPage {
693            obj_ref,
694            dict: page_dict.clone(),
695            inherited_resources,
696            media_box,
697            crop_box,
698            rotation,
699            annotations,
700        })
701    }
702
703    /// Get a rectangle value
704    fn get_rectangle(
705        &self,
706        node: &PdfDictionary,
707        inherited: Option<&PdfDictionary>,
708        key: &str,
709    ) -> ParseResult<Option<[f64; 4]>> {
710        let array = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
711
712        if let Some(array) = array.and_then(|obj| obj.as_array()) {
713            if array.len() != 4 {
714                return Err(ParseError::SyntaxError {
715                    position: 0,
716                    message: format!("{key} must have 4 elements"),
717                });
718            }
719
720            // After length check, we know array has exactly 4 elements
721            // Safe to index directly without unwrap
722            let rect = [
723                array.0[0].as_real().unwrap_or(0.0),
724                array.0[1].as_real().unwrap_or(0.0),
725                array.0[2].as_real().unwrap_or(0.0),
726                array.0[3].as_real().unwrap_or(0.0),
727            ];
728
729            Ok(Some(rect))
730        } else {
731            Ok(None)
732        }
733    }
734
735    /// Get an integer value
736    fn get_integer(
737        &self,
738        node: &PdfDictionary,
739        inherited: Option<&PdfDictionary>,
740        key: &str,
741    ) -> ParseResult<Option<i64>> {
742        let value = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
743
744        Ok(value.and_then(|obj| obj.as_integer()))
745    }
746
747    /// Get an object by its reference numbers.
748    ///
749    /// This method first checks the cache, then loads from the file if needed.
750    /// Objects are automatically cached after loading.
751    ///
752    /// # Arguments
753    ///
754    /// * `obj_num` - Object number
755    /// * `gen_num` - Generation number
756    ///
757    /// # Returns
758    ///
759    /// The resolved PDF object.
760    ///
761    /// # Errors
762    ///
763    /// Returns an error if:
764    /// - Object doesn't exist
765    /// - Object is part of an encrypted object stream
766    /// - File is corrupted
767    ///
768    /// # Example
769    ///
770    /// ```rust,no_run
771    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
772    /// # use oxidize_pdf::parser::objects::PdfObject;
773    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
774    /// # let reader = PdfReader::open("document.pdf")?;
775    /// # let document = PdfDocument::new(reader);
776    /// // Get object 10 0 R
777    /// let obj = document.get_object(10, 0)?;
778    ///
779    /// // Check object type
780    /// match obj {
781    ///     PdfObject::Dictionary(dict) => {
782    ///         println!("Object is a dictionary with {} entries", dict.0.len());
783    ///     }
784    ///     PdfObject::Stream(stream) => {
785    ///         println!("Object is a stream");
786    ///     }
787    ///     _ => {}
788    /// }
789    /// # Ok(())
790    /// # }
791    /// ```
792    pub fn get_object(&self, obj_num: u32, gen_num: u16) -> ParseResult<PdfObject> {
793        // Check resource cache first
794        if let Some(obj) = self.resources.get_cached((obj_num, gen_num)) {
795            return Ok(obj);
796        }
797
798        // Load from reader
799        let obj = {
800            let mut reader = self.reader.borrow_mut();
801            reader.get_object(obj_num, gen_num)?.clone()
802        };
803
804        // Cache it
805        self.resources.cache_object((obj_num, gen_num), obj.clone());
806
807        Ok(obj)
808    }
809
810    /// Resolve a reference to get the actual object.
811    ///
812    /// If the input is a Reference, fetches the referenced object.
813    /// Otherwise returns a clone of the input object.
814    ///
815    /// # Arguments
816    ///
817    /// * `obj` - The object to resolve (may be a Reference or direct object)
818    ///
819    /// # Returns
820    ///
821    /// The resolved object (never a Reference).
822    ///
823    /// # Example
824    ///
825    /// ```rust,no_run
826    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
827    /// # use oxidize_pdf::parser::objects::PdfObject;
828    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
829    /// # let reader = PdfReader::open("document.pdf")?;
830    /// # let document = PdfDocument::new(reader);
831    /// # let page = document.get_page(0)?;
832    /// // Contents might be a reference or direct object
833    /// if let Some(contents) = page.dict.get("Contents") {
834    ///     let resolved = document.resolve(contents)?;
835    ///     match resolved {
836    ///         PdfObject::Stream(_) => println!("Single content stream"),
837    ///         PdfObject::Array(_) => println!("Multiple content streams"),
838    ///         _ => println!("Unexpected content type"),
839    ///     }
840    /// }
841    /// # Ok(())
842    /// # }
843    /// ```
844    pub fn resolve(&self, obj: &PdfObject) -> ParseResult<PdfObject> {
845        match obj {
846            PdfObject::Reference(obj_num, gen_num) => self.get_object(*obj_num, *gen_num),
847            _ => Ok(obj.clone()),
848        }
849    }
850
851    /// Get content streams for a specific page.
852    ///
853    /// This method handles both single streams and arrays of streams,
854    /// automatically decompressing them according to their filters.
855    ///
856    /// # Arguments
857    ///
858    /// * `page` - The page to get content streams from
859    ///
860    /// # Returns
861    ///
862    /// Vector of decompressed content stream data ready for parsing.
863    ///
864    /// # Example
865    ///
866    /// ```rust,no_run
867    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
868    /// # use oxidize_pdf::parser::content::ContentParser;
869    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
870    /// # let reader = PdfReader::open("document.pdf")?;
871    /// # let document = PdfDocument::new(reader);
872    /// let page = document.get_page(0)?;
873    /// let streams = document.get_page_content_streams(&page)?;
874    ///
875    /// // Parse content streams
876    /// for stream_data in streams {
877    ///     let operations = ContentParser::parse(&stream_data)?;
878    ///     println!("Stream has {} operations", operations.len());
879    /// }
880    /// # Ok(())
881    /// # }
882    /// ```
883    /// Get page resources dictionary.
884    ///
885    /// This method returns the resources dictionary for a page, which may include
886    /// fonts, images (XObjects), patterns, color spaces, and other resources.
887    ///
888    /// # Arguments
889    ///
890    /// * `page` - The page to get resources from
891    ///
892    /// # Returns
893    ///
894    /// Optional resources dictionary if the page has resources.
895    ///
896    /// # Example
897    ///
898    /// ```rust,no_run
899    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader, PdfObject, PdfName};
900    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
901    /// # let reader = PdfReader::open("document.pdf")?;
902    /// # let document = PdfDocument::new(reader);
903    /// let page = document.get_page(0)?;
904    /// if let Some(resources) = document.get_page_resources(&page)? {
905    ///     // Check for images (XObjects)
906    ///     if let Some(PdfObject::Dictionary(xobjects)) = resources.0.get(&PdfName("XObject".to_string())) {
907    ///         for (name, _) in xobjects.0.iter() {
908    ///             println!("Found XObject: {}", name.0);
909    ///         }
910    ///     }
911    /// }
912    /// # Ok(())
913    /// # }
914    /// ```
915    pub fn get_page_resources<'a>(
916        &self,
917        page: &'a ParsedPage,
918    ) -> ParseResult<Option<&'a PdfDictionary>> {
919        Ok(page.get_resources())
920    }
921
922    pub fn get_page_content_streams(&self, page: &ParsedPage) -> ParseResult<Vec<Vec<u8>>> {
923        let mut streams = Vec::new();
924        let options = self.options();
925
926        if let Some(contents) = page.dict.get("Contents") {
927            let resolved_contents = self.resolve(contents)?;
928
929            match &resolved_contents {
930                PdfObject::Stream(stream) => {
931                    streams.push(stream.decode(&options)?);
932                }
933                PdfObject::Array(array) => {
934                    for item in &array.0 {
935                        let resolved = self.resolve(item)?;
936                        if let PdfObject::Stream(stream) = resolved {
937                            streams.push(stream.decode(&options)?);
938                        }
939                    }
940                }
941                _ => {
942                    return Err(ParseError::SyntaxError {
943                        position: 0,
944                        message: "Contents must be a stream or array of streams".to_string(),
945                    })
946                }
947            }
948        }
949
950        Ok(streams)
951    }
952
953    /// Extract text from all pages in the document.
954    ///
955    /// Uses the default text extraction settings. For custom settings,
956    /// use `extract_text_with_options`.
957    ///
958    /// # Returns
959    ///
960    /// A vector of `ExtractedText`, one for each page in the document.
961    ///
962    /// # Example
963    ///
964    /// ```rust,no_run
965    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
966    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
967    /// # let reader = PdfReader::open("document.pdf")?;
968    /// # let document = PdfDocument::new(reader);
969    /// let extracted_pages = document.extract_text()?;
970    ///
971    /// for (page_num, page_text) in extracted_pages.iter().enumerate() {
972    ///     println!("=== Page {} ===", page_num + 1);
973    ///     println!("{}", page_text.text);
974    ///     println!();
975    /// }
976    /// # Ok(())
977    /// # }
978    /// ```
979    pub fn extract_text(&self) -> ParseResult<Vec<crate::text::ExtractedText>> {
980        let mut extractor = crate::text::TextExtractor::new();
981        extractor.extract_from_document(self)
982    }
983
984    /// Extract text from a specific page.
985    ///
986    /// # Arguments
987    ///
988    /// * `page_index` - Zero-based page index
989    ///
990    /// # Returns
991    ///
992    /// Extracted text with optional position information.
993    ///
994    /// # Example
995    ///
996    /// ```rust,no_run
997    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
998    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
999    /// # let reader = PdfReader::open("document.pdf")?;
1000    /// # let document = PdfDocument::new(reader);
1001    /// // Extract text from first page only
1002    /// let page_text = document.extract_text_from_page(0)?;
1003    /// println!("First page text: {}", page_text.text);
1004    ///
1005    /// // Access text fragments with positions (if preserved)
1006    /// for fragment in &page_text.fragments {
1007    ///     println!("'{}' at ({}, {})", fragment.text, fragment.x, fragment.y);
1008    /// }
1009    /// # Ok(())
1010    /// # }
1011    /// ```
1012    pub fn extract_text_from_page(
1013        &self,
1014        page_index: u32,
1015    ) -> ParseResult<crate::text::ExtractedText> {
1016        let mut extractor = crate::text::TextExtractor::new();
1017        extractor.extract_from_page(self, page_index)
1018    }
1019
1020    /// Extract text from a specific page with custom options.
1021    ///
1022    /// This method combines the functionality of [`extract_text_from_page`] and
1023    /// [`extract_text_with_options`], allowing fine control over extraction
1024    /// behavior for a single page.
1025    ///
1026    /// # Arguments
1027    ///
1028    /// * `page_index` - Zero-based page index
1029    /// * `options` - Text extraction configuration
1030    ///
1031    /// # Returns
1032    ///
1033    /// Extracted text with optional position information.
1034    ///
1035    /// # Example
1036    ///
1037    /// ```rust,no_run
1038    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1039    /// # use oxidize_pdf::text::ExtractionOptions;
1040    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1041    /// # let reader = PdfReader::open("document.pdf")?;
1042    /// # let document = PdfDocument::new(reader);
1043    /// // Use higher space threshold for PDFs with micro-adjustments
1044    /// let options = ExtractionOptions {
1045    ///     space_threshold: 0.4,
1046    ///     ..Default::default()
1047    /// };
1048    ///
1049    /// let page_text = document.extract_text_from_page_with_options(0, options)?;
1050    /// println!("Text: {}", page_text.text);
1051    /// # Ok(())
1052    /// # }
1053    /// ```
1054    pub fn extract_text_from_page_with_options(
1055        &self,
1056        page_index: u32,
1057        options: crate::text::ExtractionOptions,
1058    ) -> ParseResult<crate::text::ExtractedText> {
1059        let mut extractor = crate::text::TextExtractor::with_options(options);
1060        extractor.extract_from_page(self, page_index)
1061    }
1062
1063    /// Extract text with custom extraction options.
1064    ///
1065    /// Allows fine control over text extraction behavior including
1066    /// layout preservation, spacing thresholds, and more.
1067    ///
1068    /// # Arguments
1069    ///
1070    /// * `options` - Text extraction configuration
1071    ///
1072    /// # Returns
1073    ///
1074    /// A vector of `ExtractedText`, one for each page.
1075    ///
1076    /// # Example
1077    ///
1078    /// ```rust,no_run
1079    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1080    /// # use oxidize_pdf::text::ExtractionOptions;
1081    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1082    /// # let reader = PdfReader::open("document.pdf")?;
1083    /// # let document = PdfDocument::new(reader);
1084    /// // Configure extraction to preserve layout
1085    /// let options = ExtractionOptions {
1086    ///     preserve_layout: true,
1087    ///     space_threshold: 0.3,
1088    ///     newline_threshold: 10.0,
1089    ///     ..Default::default()
1090    /// };
1091    ///
1092    /// let extracted_pages = document.extract_text_with_options(options)?;
1093    ///
1094    /// // Text fragments will include position information
1095    /// for page_text in extracted_pages {
1096    ///     for fragment in &page_text.fragments {
1097    ///         println!("{:?}", fragment);
1098    ///     }
1099    /// }
1100    /// # Ok(())
1101    /// # }
1102    /// ```
1103    pub fn extract_text_with_options(
1104        &self,
1105        options: crate::text::ExtractionOptions,
1106    ) -> ParseResult<Vec<crate::text::ExtractedText>> {
1107        let mut extractor = crate::text::TextExtractor::with_options(options);
1108        extractor.extract_from_document(self)
1109    }
1110
1111    /// Get annotations from a specific page.
1112    ///
1113    /// Returns a vector of annotation dictionaries for the specified page.
1114    /// Each annotation dictionary contains properties like Type, Rect, Contents, etc.
1115    ///
1116    /// # Arguments
1117    ///
1118    /// * `page_index` - Zero-based page index
1119    ///
1120    /// # Returns
1121    ///
1122    /// A vector of PdfDictionary objects representing annotations, or an empty vector
1123    /// if the page has no annotations.
1124    ///
1125    /// # Example
1126    ///
1127    /// ```rust,no_run
1128    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1129    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1130    /// # let reader = PdfReader::open("document.pdf")?;
1131    /// # let document = PdfDocument::new(reader);
1132    /// let annotations = document.get_page_annotations(0)?;
1133    /// for annot in &annotations {
1134    ///     if let Some(contents) = annot.get("Contents").and_then(|c| c.as_string()) {
1135    ///         println!("Annotation: {:?}", contents);
1136    ///     }
1137    /// }
1138    /// # Ok(())
1139    /// # }
1140    /// ```
1141    pub fn get_page_annotations(&self, page_index: u32) -> ParseResult<Vec<PdfDictionary>> {
1142        let page = self.get_page(page_index)?;
1143
1144        if let Some(annots_array) = page.get_annotations() {
1145            let mut annotations = Vec::new();
1146            let mut reader = self.reader.borrow_mut();
1147
1148            for annot_ref in &annots_array.0 {
1149                if let Some(ref_nums) = annot_ref.as_reference() {
1150                    match reader.get_object(ref_nums.0, ref_nums.1) {
1151                        Ok(obj) => {
1152                            if let Some(dict) = obj.as_dict() {
1153                                annotations.push(dict.clone());
1154                            }
1155                        }
1156                        Err(_) => {
1157                            // Skip annotations that can't be loaded
1158                            continue;
1159                        }
1160                    }
1161                }
1162            }
1163
1164            Ok(annotations)
1165        } else {
1166            Ok(Vec::new())
1167        }
1168    }
1169
1170    /// Get all annotations from all pages in the document.
1171    ///
1172    /// Returns a vector of tuples containing (page_index, annotations) for each page
1173    /// that has annotations.
1174    ///
1175    /// # Returns
1176    ///
1177    /// A vector of tuples where the first element is the page index and the second
1178    /// is a vector of annotation dictionaries for that page.
1179    ///
1180    /// # Example
1181    ///
1182    /// ```rust,no_run
1183    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1184    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1185    /// # let reader = PdfReader::open("document.pdf")?;
1186    /// # let document = PdfDocument::new(reader);
1187    /// let all_annotations = document.get_all_annotations()?;
1188    /// for (page_idx, annotations) in all_annotations {
1189    ///     println!("Page {} has {} annotations", page_idx, annotations.len());
1190    /// }
1191    /// # Ok(())
1192    /// # }
1193    /// ```
1194    pub fn get_all_annotations(&self) -> ParseResult<Vec<(u32, Vec<PdfDictionary>)>> {
1195        let page_count = self.page_count()?;
1196        let mut all_annotations = Vec::new();
1197
1198        for i in 0..page_count {
1199            let annotations = self.get_page_annotations(i)?;
1200            if !annotations.is_empty() {
1201                all_annotations.push((i, annotations));
1202            }
1203        }
1204
1205        Ok(all_annotations)
1206    }
1207}
1208
1209#[cfg(test)]
1210mod tests {
1211    use super::*;
1212    use crate::parser::objects::{PdfObject, PdfString};
1213    use std::io::Cursor;
1214
1215    // Helper function to create a minimal PDF in memory
1216    fn create_minimal_pdf() -> Vec<u8> {
1217        let mut pdf = Vec::new();
1218
1219        // PDF header
1220        pdf.extend_from_slice(b"%PDF-1.4\n");
1221
1222        // Catalog object
1223        pdf.extend_from_slice(b"1 0 obj\n");
1224        pdf.extend_from_slice(b"<< /Type /Catalog /Pages 2 0 R >>\n");
1225        pdf.extend_from_slice(b"endobj\n");
1226
1227        // Pages object
1228        pdf.extend_from_slice(b"2 0 obj\n");
1229        pdf.extend_from_slice(b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>\n");
1230        pdf.extend_from_slice(b"endobj\n");
1231
1232        // Page object
1233        pdf.extend_from_slice(b"3 0 obj\n");
1234        pdf.extend_from_slice(
1235            b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> >>\n",
1236        );
1237        pdf.extend_from_slice(b"endobj\n");
1238
1239        // Cross-reference table
1240        let xref_pos = pdf.len();
1241        pdf.extend_from_slice(b"xref\n");
1242        pdf.extend_from_slice(b"0 4\n");
1243        pdf.extend_from_slice(b"0000000000 65535 f \n");
1244        pdf.extend_from_slice(b"0000000009 00000 n \n");
1245        pdf.extend_from_slice(b"0000000058 00000 n \n");
1246        pdf.extend_from_slice(b"0000000115 00000 n \n");
1247
1248        // Trailer
1249        pdf.extend_from_slice(b"trailer\n");
1250        pdf.extend_from_slice(b"<< /Size 4 /Root 1 0 R >>\n");
1251        pdf.extend_from_slice(b"startxref\n");
1252        pdf.extend_from_slice(format!("{xref_pos}\n").as_bytes());
1253        pdf.extend_from_slice(b"%%EOF\n");
1254
1255        pdf
1256    }
1257
1258    // Helper to create a PDF with metadata
1259    fn create_pdf_with_metadata() -> Vec<u8> {
1260        let mut pdf = Vec::new();
1261
1262        // PDF header
1263        pdf.extend_from_slice(b"%PDF-1.5\n");
1264
1265        // Record positions for xref
1266        let obj1_pos = pdf.len();
1267
1268        // Catalog object
1269        pdf.extend_from_slice(b"1 0 obj\n");
1270        pdf.extend_from_slice(b"<< /Type /Catalog /Pages 2 0 R >>\n");
1271        pdf.extend_from_slice(b"endobj\n");
1272
1273        let obj2_pos = pdf.len();
1274
1275        // Pages object
1276        pdf.extend_from_slice(b"2 0 obj\n");
1277        pdf.extend_from_slice(b"<< /Type /Pages /Kids [] /Count 0 >>\n");
1278        pdf.extend_from_slice(b"endobj\n");
1279
1280        let obj3_pos = pdf.len();
1281
1282        // Info object
1283        pdf.extend_from_slice(b"3 0 obj\n");
1284        pdf.extend_from_slice(
1285            b"<< /Title (Test Document) /Author (Test Author) /Subject (Test Subject) >>\n",
1286        );
1287        pdf.extend_from_slice(b"endobj\n");
1288
1289        // Cross-reference table
1290        let xref_pos = pdf.len();
1291        pdf.extend_from_slice(b"xref\n");
1292        pdf.extend_from_slice(b"0 4\n");
1293        pdf.extend_from_slice(b"0000000000 65535 f \n");
1294        pdf.extend_from_slice(format!("{obj1_pos:010} 00000 n \n").as_bytes());
1295        pdf.extend_from_slice(format!("{obj2_pos:010} 00000 n \n").as_bytes());
1296        pdf.extend_from_slice(format!("{obj3_pos:010} 00000 n \n").as_bytes());
1297
1298        // Trailer
1299        pdf.extend_from_slice(b"trailer\n");
1300        pdf.extend_from_slice(b"<< /Size 4 /Root 1 0 R /Info 3 0 R >>\n");
1301        pdf.extend_from_slice(b"startxref\n");
1302        pdf.extend_from_slice(format!("{xref_pos}\n").as_bytes());
1303        pdf.extend_from_slice(b"%%EOF\n");
1304
1305        pdf
1306    }
1307
1308    #[test]
1309    fn test_pdf_document_new() {
1310        let pdf_data = create_minimal_pdf();
1311        let cursor = Cursor::new(pdf_data);
1312        let reader = PdfReader::new(cursor).unwrap();
1313        let document = PdfDocument::new(reader);
1314
1315        // Verify document is created with empty caches
1316        assert!(document.page_tree.borrow().is_none());
1317        assert!(document.metadata_cache.borrow().is_none());
1318    }
1319
1320    #[test]
1321    fn test_version() {
1322        let pdf_data = create_minimal_pdf();
1323        let cursor = Cursor::new(pdf_data);
1324        let reader = PdfReader::new(cursor).unwrap();
1325        let document = PdfDocument::new(reader);
1326
1327        let version = document.version().unwrap();
1328        assert_eq!(version, "1.4");
1329    }
1330
1331    #[test]
1332    fn test_page_count() {
1333        let pdf_data = create_minimal_pdf();
1334        let cursor = Cursor::new(pdf_data);
1335        let reader = PdfReader::new(cursor).unwrap();
1336        let document = PdfDocument::new(reader);
1337
1338        let count = document.page_count().unwrap();
1339        assert_eq!(count, 1);
1340    }
1341
1342    #[test]
1343    fn test_metadata() {
1344        let pdf_data = create_pdf_with_metadata();
1345        let cursor = Cursor::new(pdf_data);
1346        let reader = PdfReader::new(cursor).unwrap();
1347        let document = PdfDocument::new(reader);
1348
1349        let metadata = document.metadata().unwrap();
1350        assert_eq!(metadata.title, Some("Test Document".to_string()));
1351        assert_eq!(metadata.author, Some("Test Author".to_string()));
1352        assert_eq!(metadata.subject, Some("Test Subject".to_string()));
1353
1354        // Verify caching works
1355        let metadata2 = document.metadata().unwrap();
1356        assert_eq!(metadata.title, metadata2.title);
1357    }
1358
1359    #[test]
1360    fn test_get_page() {
1361        let pdf_data = create_minimal_pdf();
1362        let cursor = Cursor::new(pdf_data);
1363        let reader = PdfReader::new(cursor).unwrap();
1364        let document = PdfDocument::new(reader);
1365
1366        // Get first page
1367        let page = document.get_page(0).unwrap();
1368        assert_eq!(page.media_box, [0.0, 0.0, 612.0, 792.0]);
1369
1370        // Verify caching works
1371        let page2 = document.get_page(0).unwrap();
1372        assert_eq!(page.media_box, page2.media_box);
1373    }
1374
1375    #[test]
1376    fn test_get_page_out_of_bounds() {
1377        let pdf_data = create_minimal_pdf();
1378        let cursor = Cursor::new(pdf_data);
1379        let reader = PdfReader::new(cursor).unwrap();
1380        let document = PdfDocument::new(reader);
1381
1382        // Try to get page that doesn't exist
1383        let result = document.get_page(10);
1384        // With fallback lookup, this might succeed or fail gracefully
1385        if result.is_err() {
1386            assert!(result.unwrap_err().to_string().contains("Page"));
1387        } else {
1388            // If succeeds, should return a valid page
1389            let _page = result.unwrap();
1390        }
1391    }
1392
1393    #[test]
1394    fn test_resource_manager_caching() {
1395        let resources = ResourceManager::new();
1396
1397        // Test caching an object
1398        let obj_ref = (1, 0);
1399        let obj = PdfObject::String(PdfString("Test".as_bytes().to_vec()));
1400
1401        assert!(resources.get_cached(obj_ref).is_none());
1402
1403        resources.cache_object(obj_ref, obj.clone());
1404
1405        let cached = resources.get_cached(obj_ref).unwrap();
1406        assert_eq!(cached, obj);
1407
1408        // Test clearing cache
1409        resources.clear_cache();
1410        assert!(resources.get_cached(obj_ref).is_none());
1411    }
1412
1413    #[test]
1414    fn test_get_object() {
1415        let pdf_data = create_minimal_pdf();
1416        let cursor = Cursor::new(pdf_data);
1417        let reader = PdfReader::new(cursor).unwrap();
1418        let document = PdfDocument::new(reader);
1419
1420        // Get catalog object
1421        let catalog = document.get_object(1, 0).unwrap();
1422        if let PdfObject::Dictionary(dict) = catalog {
1423            if let Some(PdfObject::Name(name)) = dict.get("Type") {
1424                assert_eq!(name.0, "Catalog");
1425            } else {
1426                panic!("Expected /Type name");
1427            }
1428        } else {
1429            panic!("Expected dictionary object");
1430        }
1431    }
1432
1433    #[test]
1434    fn test_resolve_reference() {
1435        let pdf_data = create_minimal_pdf();
1436        let cursor = Cursor::new(pdf_data);
1437        let reader = PdfReader::new(cursor).unwrap();
1438        let document = PdfDocument::new(reader);
1439
1440        // Create a reference to the catalog
1441        let ref_obj = PdfObject::Reference(1, 0);
1442
1443        // Resolve it
1444        let resolved = document.resolve(&ref_obj).unwrap();
1445        if let PdfObject::Dictionary(dict) = resolved {
1446            if let Some(PdfObject::Name(name)) = dict.get("Type") {
1447                assert_eq!(name.0, "Catalog");
1448            } else {
1449                panic!("Expected /Type name");
1450            }
1451        } else {
1452            panic!("Expected dictionary object");
1453        }
1454    }
1455
1456    #[test]
1457    fn test_resolve_non_reference() {
1458        let pdf_data = create_minimal_pdf();
1459        let cursor = Cursor::new(pdf_data);
1460        let reader = PdfReader::new(cursor).unwrap();
1461        let document = PdfDocument::new(reader);
1462
1463        // Try to resolve a non-reference object
1464        let obj = PdfObject::String(PdfString("Test".as_bytes().to_vec()));
1465        let resolved = document.resolve(&obj).unwrap();
1466
1467        // Should return the same object
1468        assert_eq!(resolved, obj);
1469    }
1470
1471    #[test]
1472    fn test_invalid_pdf_data() {
1473        let invalid_data = b"This is not a PDF";
1474        let cursor = Cursor::new(invalid_data.to_vec());
1475        let result = PdfReader::new(cursor);
1476
1477        assert!(result.is_err());
1478    }
1479
1480    #[test]
1481    fn test_empty_page_tree() {
1482        // Create PDF with empty page tree
1483        let pdf_data = create_pdf_with_metadata(); // This has 0 pages
1484        let cursor = Cursor::new(pdf_data);
1485        let reader = PdfReader::new(cursor).unwrap();
1486        let document = PdfDocument::new(reader);
1487
1488        let count = document.page_count().unwrap();
1489        assert_eq!(count, 0);
1490
1491        // Try to get a page from empty document
1492        let result = document.get_page(0);
1493        assert!(result.is_err());
1494    }
1495
1496    #[test]
1497    fn test_extract_text_empty_document() {
1498        let pdf_data = create_pdf_with_metadata();
1499        let cursor = Cursor::new(pdf_data);
1500        let reader = PdfReader::new(cursor).unwrap();
1501        let document = PdfDocument::new(reader);
1502
1503        let text = document.extract_text().unwrap();
1504        assert!(text.is_empty());
1505    }
1506
1507    #[test]
1508    fn test_concurrent_access() {
1509        let pdf_data = create_minimal_pdf();
1510        let cursor = Cursor::new(pdf_data);
1511        let reader = PdfReader::new(cursor).unwrap();
1512        let document = PdfDocument::new(reader);
1513
1514        // Access multiple things concurrently
1515        let version = document.version().unwrap();
1516        let count = document.page_count().unwrap();
1517        let page = document.get_page(0).unwrap();
1518
1519        assert_eq!(version, "1.4");
1520        assert_eq!(count, 1);
1521        assert_eq!(page.media_box[2], 612.0);
1522    }
1523
1524    // Additional comprehensive tests
1525    mod comprehensive_tests {
1526        use super::*;
1527
1528        #[test]
1529        fn test_resource_manager_default() {
1530            let resources = ResourceManager::default();
1531            assert!(resources.get_cached((1, 0)).is_none());
1532        }
1533
1534        #[test]
1535        fn test_resource_manager_multiple_objects() {
1536            let resources = ResourceManager::new();
1537
1538            // Cache multiple objects
1539            resources.cache_object((1, 0), PdfObject::Integer(42));
1540            resources.cache_object((2, 0), PdfObject::Boolean(true));
1541            resources.cache_object(
1542                (3, 0),
1543                PdfObject::String(PdfString("test".as_bytes().to_vec())),
1544            );
1545
1546            // Verify all are cached
1547            assert!(resources.get_cached((1, 0)).is_some());
1548            assert!(resources.get_cached((2, 0)).is_some());
1549            assert!(resources.get_cached((3, 0)).is_some());
1550
1551            // Clear and verify empty
1552            resources.clear_cache();
1553            assert!(resources.get_cached((1, 0)).is_none());
1554            assert!(resources.get_cached((2, 0)).is_none());
1555            assert!(resources.get_cached((3, 0)).is_none());
1556        }
1557
1558        #[test]
1559        fn test_resource_manager_object_overwrite() {
1560            let resources = ResourceManager::new();
1561
1562            // Cache an object
1563            resources.cache_object((1, 0), PdfObject::Integer(42));
1564            assert_eq!(resources.get_cached((1, 0)), Some(PdfObject::Integer(42)));
1565
1566            // Overwrite with different object
1567            resources.cache_object((1, 0), PdfObject::Boolean(true));
1568            assert_eq!(resources.get_cached((1, 0)), Some(PdfObject::Boolean(true)));
1569        }
1570
1571        #[test]
1572        fn test_get_object_caching() {
1573            let pdf_data = create_minimal_pdf();
1574            let cursor = Cursor::new(pdf_data);
1575            let reader = PdfReader::new(cursor).unwrap();
1576            let document = PdfDocument::new(reader);
1577
1578            // Get object first time (should cache)
1579            let obj1 = document.get_object(1, 0).unwrap();
1580
1581            // Get same object again (should use cache)
1582            let obj2 = document.get_object(1, 0).unwrap();
1583
1584            // Objects should be identical
1585            assert_eq!(obj1, obj2);
1586
1587            // Verify it's cached
1588            assert!(document.resources.get_cached((1, 0)).is_some());
1589        }
1590
1591        #[test]
1592        fn test_get_object_different_generations() {
1593            let pdf_data = create_minimal_pdf();
1594            let cursor = Cursor::new(pdf_data);
1595            let reader = PdfReader::new(cursor).unwrap();
1596            let document = PdfDocument::new(reader);
1597
1598            // Get object with generation 0
1599            let _obj1 = document.get_object(1, 0).unwrap();
1600
1601            // Try to get same object with different generation (should fail)
1602            let result = document.get_object(1, 1);
1603            assert!(result.is_err());
1604
1605            // Original should still be cached
1606            assert!(document.resources.get_cached((1, 0)).is_some());
1607        }
1608
1609        #[test]
1610        fn test_get_object_nonexistent() {
1611            let pdf_data = create_minimal_pdf();
1612            let cursor = Cursor::new(pdf_data);
1613            let reader = PdfReader::new(cursor).unwrap();
1614            let document = PdfDocument::new(reader);
1615
1616            // Try to get non-existent object
1617            let result = document.get_object(999, 0);
1618            assert!(result.is_err());
1619        }
1620
1621        #[test]
1622        fn test_resolve_nested_references() {
1623            let pdf_data = create_minimal_pdf();
1624            let cursor = Cursor::new(pdf_data);
1625            let reader = PdfReader::new(cursor).unwrap();
1626            let document = PdfDocument::new(reader);
1627
1628            // Test resolving a reference
1629            let ref_obj = PdfObject::Reference(2, 0);
1630            let resolved = document.resolve(&ref_obj).unwrap();
1631
1632            // Should resolve to the pages object
1633            if let PdfObject::Dictionary(dict) = resolved {
1634                if let Some(PdfObject::Name(name)) = dict.get("Type") {
1635                    assert_eq!(name.0, "Pages");
1636                }
1637            }
1638        }
1639
1640        #[test]
1641        fn test_resolve_various_object_types() {
1642            let pdf_data = create_minimal_pdf();
1643            let cursor = Cursor::new(pdf_data);
1644            let reader = PdfReader::new(cursor).unwrap();
1645            let document = PdfDocument::new(reader);
1646
1647            // Test resolving different object types
1648            let test_objects = vec![
1649                PdfObject::Integer(42),
1650                PdfObject::Boolean(true),
1651                PdfObject::String(PdfString("test".as_bytes().to_vec())),
1652                PdfObject::Real(3.14),
1653                PdfObject::Null,
1654            ];
1655
1656            for obj in test_objects {
1657                let resolved = document.resolve(&obj).unwrap();
1658                assert_eq!(resolved, obj);
1659            }
1660        }
1661
1662        #[test]
1663        fn test_get_page_cached() {
1664            let pdf_data = create_minimal_pdf();
1665            let cursor = Cursor::new(pdf_data);
1666            let reader = PdfReader::new(cursor).unwrap();
1667            let document = PdfDocument::new(reader);
1668
1669            // Get page first time
1670            let page1 = document.get_page(0).unwrap();
1671
1672            // Get same page again
1673            let page2 = document.get_page(0).unwrap();
1674
1675            // Should be identical
1676            assert_eq!(page1.media_box, page2.media_box);
1677            assert_eq!(page1.rotation, page2.rotation);
1678            assert_eq!(page1.obj_ref, page2.obj_ref);
1679        }
1680
1681        #[test]
1682        fn test_metadata_caching() {
1683            let pdf_data = create_pdf_with_metadata();
1684            let cursor = Cursor::new(pdf_data);
1685            let reader = PdfReader::new(cursor).unwrap();
1686            let document = PdfDocument::new(reader);
1687
1688            // Get metadata first time
1689            let meta1 = document.metadata().unwrap();
1690
1691            // Get metadata again
1692            let meta2 = document.metadata().unwrap();
1693
1694            // Should be identical
1695            assert_eq!(meta1.title, meta2.title);
1696            assert_eq!(meta1.author, meta2.author);
1697            assert_eq!(meta1.subject, meta2.subject);
1698            assert_eq!(meta1.version, meta2.version);
1699        }
1700
1701        #[test]
1702        fn test_page_tree_initialization() {
1703            let pdf_data = create_minimal_pdf();
1704            let cursor = Cursor::new(pdf_data);
1705            let reader = PdfReader::new(cursor).unwrap();
1706            let document = PdfDocument::new(reader);
1707
1708            // Initially page tree should be None
1709            assert!(document.page_tree.borrow().is_none());
1710
1711            // After getting page count, page tree should be initialized
1712            let _count = document.page_count().unwrap();
1713            // Note: page_tree is private, so we can't directly check it
1714            // But we can verify it works by getting a page
1715            let _page = document.get_page(0).unwrap();
1716        }
1717
1718        #[test]
1719        fn test_get_page_resources() {
1720            let pdf_data = create_minimal_pdf();
1721            let cursor = Cursor::new(pdf_data);
1722            let reader = PdfReader::new(cursor).unwrap();
1723            let document = PdfDocument::new(reader);
1724
1725            let page = document.get_page(0).unwrap();
1726            let resources = document.get_page_resources(&page).unwrap();
1727
1728            // The minimal PDF has empty resources
1729            assert!(resources.is_some());
1730        }
1731
1732        #[test]
1733        fn test_get_page_content_streams_empty() {
1734            let pdf_data = create_minimal_pdf();
1735            let cursor = Cursor::new(pdf_data);
1736            let reader = PdfReader::new(cursor).unwrap();
1737            let document = PdfDocument::new(reader);
1738
1739            let page = document.get_page(0).unwrap();
1740            let streams = document.get_page_content_streams(&page).unwrap();
1741
1742            // Minimal PDF has no content streams
1743            assert!(streams.is_empty());
1744        }
1745
1746        #[test]
1747        fn test_extract_text_from_page() {
1748            let pdf_data = create_minimal_pdf();
1749            let cursor = Cursor::new(pdf_data);
1750            let reader = PdfReader::new(cursor).unwrap();
1751            let document = PdfDocument::new(reader);
1752
1753            let result = document.extract_text_from_page(0);
1754            // Should succeed even with empty page
1755            assert!(result.is_ok());
1756        }
1757
1758        #[test]
1759        fn test_extract_text_from_page_out_of_bounds() {
1760            let pdf_data = create_minimal_pdf();
1761            let cursor = Cursor::new(pdf_data);
1762            let reader = PdfReader::new(cursor).unwrap();
1763            let document = PdfDocument::new(reader);
1764
1765            let result = document.extract_text_from_page(999);
1766            // With fallback lookup, this might succeed or fail gracefully
1767            if result.is_err() {
1768                assert!(result.unwrap_err().to_string().contains("Page"));
1769            } else {
1770                // If succeeds, should return empty or valid text
1771                let _text = result.unwrap();
1772            }
1773        }
1774
1775        #[test]
1776        fn test_extract_text_with_options() {
1777            let pdf_data = create_minimal_pdf();
1778            let cursor = Cursor::new(pdf_data);
1779            let reader = PdfReader::new(cursor).unwrap();
1780            let document = PdfDocument::new(reader);
1781
1782            let options = crate::text::ExtractionOptions {
1783                preserve_layout: true,
1784                space_threshold: 0.5,
1785                newline_threshold: 15.0,
1786                ..Default::default()
1787            };
1788
1789            let result = document.extract_text_with_options(options);
1790            assert!(result.is_ok());
1791        }
1792
1793        #[test]
1794        fn test_version_different_pdf_versions() {
1795            // Test with different PDF versions
1796            let versions = vec!["1.3", "1.4", "1.5", "1.6", "1.7"];
1797
1798            for version in versions {
1799                let mut pdf_data = Vec::new();
1800
1801                // PDF header
1802                pdf_data.extend_from_slice(format!("%PDF-{version}\n").as_bytes());
1803
1804                // Track positions for xref
1805                let obj1_pos = pdf_data.len();
1806
1807                // Catalog object
1808                pdf_data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
1809
1810                let obj2_pos = pdf_data.len();
1811
1812                // Pages object
1813                pdf_data
1814                    .extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
1815
1816                // Cross-reference table
1817                let xref_pos = pdf_data.len();
1818                pdf_data.extend_from_slice(b"xref\n");
1819                pdf_data.extend_from_slice(b"0 3\n");
1820                pdf_data.extend_from_slice(b"0000000000 65535 f \n");
1821                pdf_data.extend_from_slice(format!("{obj1_pos:010} 00000 n \n").as_bytes());
1822                pdf_data.extend_from_slice(format!("{obj2_pos:010} 00000 n \n").as_bytes());
1823
1824                // Trailer
1825                pdf_data.extend_from_slice(b"trailer\n");
1826                pdf_data.extend_from_slice(b"<< /Size 3 /Root 1 0 R >>\n");
1827                pdf_data.extend_from_slice(b"startxref\n");
1828                pdf_data.extend_from_slice(format!("{xref_pos}\n").as_bytes());
1829                pdf_data.extend_from_slice(b"%%EOF\n");
1830
1831                let cursor = Cursor::new(pdf_data);
1832                let reader = PdfReader::new(cursor).unwrap();
1833                let document = PdfDocument::new(reader);
1834
1835                let pdf_version = document.version().unwrap();
1836                assert_eq!(pdf_version, version);
1837            }
1838        }
1839
1840        #[test]
1841        fn test_page_count_zero() {
1842            let pdf_data = create_pdf_with_metadata(); // Has 0 pages
1843            let cursor = Cursor::new(pdf_data);
1844            let reader = PdfReader::new(cursor).unwrap();
1845            let document = PdfDocument::new(reader);
1846
1847            let count = document.page_count().unwrap();
1848            assert_eq!(count, 0);
1849        }
1850
1851        #[test]
1852        fn test_multiple_object_access() {
1853            let pdf_data = create_minimal_pdf();
1854            let cursor = Cursor::new(pdf_data);
1855            let reader = PdfReader::new(cursor).unwrap();
1856            let document = PdfDocument::new(reader);
1857
1858            // Access multiple objects
1859            let catalog = document.get_object(1, 0).unwrap();
1860            let pages = document.get_object(2, 0).unwrap();
1861            let page = document.get_object(3, 0).unwrap();
1862
1863            // Verify they're all different objects
1864            assert_ne!(catalog, pages);
1865            assert_ne!(pages, page);
1866            assert_ne!(catalog, page);
1867        }
1868
1869        #[test]
1870        fn test_error_handling_invalid_object_reference() {
1871            let pdf_data = create_minimal_pdf();
1872            let cursor = Cursor::new(pdf_data);
1873            let reader = PdfReader::new(cursor).unwrap();
1874            let document = PdfDocument::new(reader);
1875
1876            // Try to resolve an invalid reference
1877            let invalid_ref = PdfObject::Reference(999, 0);
1878            let result = document.resolve(&invalid_ref);
1879            assert!(result.is_err());
1880        }
1881
1882        #[test]
1883        fn test_concurrent_metadata_access() {
1884            let pdf_data = create_pdf_with_metadata();
1885            let cursor = Cursor::new(pdf_data);
1886            let reader = PdfReader::new(cursor).unwrap();
1887            let document = PdfDocument::new(reader);
1888
1889            // Access metadata and other properties concurrently
1890            let metadata = document.metadata().unwrap();
1891            let version = document.version().unwrap();
1892            let count = document.page_count().unwrap();
1893
1894            assert_eq!(metadata.title, Some("Test Document".to_string()));
1895            assert_eq!(version, "1.5");
1896            assert_eq!(count, 0);
1897        }
1898
1899        #[test]
1900        fn test_page_properties_comprehensive() {
1901            let pdf_data = create_minimal_pdf();
1902            let cursor = Cursor::new(pdf_data);
1903            let reader = PdfReader::new(cursor).unwrap();
1904            let document = PdfDocument::new(reader);
1905
1906            let page = document.get_page(0).unwrap();
1907
1908            // Test all page properties
1909            assert_eq!(page.media_box, [0.0, 0.0, 612.0, 792.0]);
1910            assert_eq!(page.crop_box, None);
1911            assert_eq!(page.rotation, 0);
1912            assert_eq!(page.obj_ref, (3, 0));
1913
1914            // Test width/height calculation
1915            assert_eq!(page.width(), 612.0);
1916            assert_eq!(page.height(), 792.0);
1917        }
1918
1919        #[test]
1920        fn test_memory_usage_efficiency() {
1921            let pdf_data = create_minimal_pdf();
1922            let cursor = Cursor::new(pdf_data);
1923            let reader = PdfReader::new(cursor).unwrap();
1924            let document = PdfDocument::new(reader);
1925
1926            // Access same page multiple times
1927            for _ in 0..10 {
1928                let _page = document.get_page(0).unwrap();
1929            }
1930
1931            // Should only have one copy in cache
1932            let page_count = document.page_count().unwrap();
1933            assert_eq!(page_count, 1);
1934        }
1935
1936        #[test]
1937        fn test_reader_borrow_safety() {
1938            let pdf_data = create_minimal_pdf();
1939            let cursor = Cursor::new(pdf_data);
1940            let reader = PdfReader::new(cursor).unwrap();
1941            let document = PdfDocument::new(reader);
1942
1943            // Multiple concurrent borrows should work
1944            let version = document.version().unwrap();
1945            let count = document.page_count().unwrap();
1946            let metadata = document.metadata().unwrap();
1947
1948            assert_eq!(version, "1.4");
1949            assert_eq!(count, 1);
1950            assert!(metadata.title.is_none());
1951        }
1952
1953        #[test]
1954        fn test_cache_consistency() {
1955            let pdf_data = create_minimal_pdf();
1956            let cursor = Cursor::new(pdf_data);
1957            let reader = PdfReader::new(cursor).unwrap();
1958            let document = PdfDocument::new(reader);
1959
1960            // Get object and verify caching
1961            let obj1 = document.get_object(1, 0).unwrap();
1962            let cached = document.resources.get_cached((1, 0)).unwrap();
1963
1964            assert_eq!(obj1, cached);
1965
1966            // Clear cache and get object again
1967            document.resources.clear_cache();
1968            let obj2 = document.get_object(1, 0).unwrap();
1969
1970            // Should be same content but loaded fresh
1971            assert_eq!(obj1, obj2);
1972        }
1973    }
1974
1975    #[test]
1976    fn test_resource_manager_new() {
1977        let resources = ResourceManager::new();
1978        assert!(resources.get_cached((1, 0)).is_none());
1979    }
1980
1981    #[test]
1982    fn test_resource_manager_cache_and_get() {
1983        let resources = ResourceManager::new();
1984
1985        // Cache an object
1986        let obj = PdfObject::Integer(42);
1987        resources.cache_object((10, 0), obj.clone());
1988
1989        // Should be retrievable
1990        let cached = resources.get_cached((10, 0));
1991        assert!(cached.is_some());
1992        assert_eq!(cached.unwrap(), obj);
1993
1994        // Non-existent object
1995        assert!(resources.get_cached((11, 0)).is_none());
1996    }
1997
1998    #[test]
1999    fn test_resource_manager_clear_cache() {
2000        let resources = ResourceManager::new();
2001
2002        // Cache multiple objects
2003        resources.cache_object((1, 0), PdfObject::Integer(1));
2004        resources.cache_object((2, 0), PdfObject::Integer(2));
2005        resources.cache_object((3, 0), PdfObject::Integer(3));
2006
2007        // Verify they're cached
2008        assert!(resources.get_cached((1, 0)).is_some());
2009        assert!(resources.get_cached((2, 0)).is_some());
2010        assert!(resources.get_cached((3, 0)).is_some());
2011
2012        // Clear cache
2013        resources.clear_cache();
2014
2015        // Should all be gone
2016        assert!(resources.get_cached((1, 0)).is_none());
2017        assert!(resources.get_cached((2, 0)).is_none());
2018        assert!(resources.get_cached((3, 0)).is_none());
2019    }
2020
2021    #[test]
2022    fn test_resource_manager_overwrite_cached() {
2023        let resources = ResourceManager::new();
2024
2025        // Cache initial object
2026        resources.cache_object((1, 0), PdfObject::Integer(42));
2027        assert_eq!(
2028            resources.get_cached((1, 0)).unwrap(),
2029            PdfObject::Integer(42)
2030        );
2031
2032        // Overwrite with new object
2033        resources.cache_object((1, 0), PdfObject::Integer(100));
2034        assert_eq!(
2035            resources.get_cached((1, 0)).unwrap(),
2036            PdfObject::Integer(100)
2037        );
2038    }
2039
2040    #[test]
2041    fn test_resource_manager_multiple_generations() {
2042        let resources = ResourceManager::new();
2043
2044        // Cache objects with different generations
2045        resources.cache_object((1, 0), PdfObject::Integer(10));
2046        resources.cache_object((1, 1), PdfObject::Integer(11));
2047        resources.cache_object((1, 2), PdfObject::Integer(12));
2048
2049        // Each should be distinct
2050        assert_eq!(
2051            resources.get_cached((1, 0)).unwrap(),
2052            PdfObject::Integer(10)
2053        );
2054        assert_eq!(
2055            resources.get_cached((1, 1)).unwrap(),
2056            PdfObject::Integer(11)
2057        );
2058        assert_eq!(
2059            resources.get_cached((1, 2)).unwrap(),
2060            PdfObject::Integer(12)
2061        );
2062    }
2063
2064    #[test]
2065    fn test_resource_manager_cache_complex_objects() {
2066        let resources = ResourceManager::new();
2067
2068        // Cache different object types
2069        resources.cache_object((1, 0), PdfObject::Boolean(true));
2070        resources.cache_object((2, 0), PdfObject::Real(3.14159));
2071        resources.cache_object(
2072            (3, 0),
2073            PdfObject::String(PdfString::new(b"Hello PDF".to_vec())),
2074        );
2075        resources.cache_object((4, 0), PdfObject::Name(PdfName::new("Type".to_string())));
2076
2077        let mut dict = PdfDictionary::new();
2078        dict.insert(
2079            "Key".to_string(),
2080            PdfObject::String(PdfString::new(b"Value".to_vec())),
2081        );
2082        resources.cache_object((5, 0), PdfObject::Dictionary(dict));
2083
2084        let array = vec![PdfObject::Integer(1), PdfObject::Integer(2)];
2085        resources.cache_object((6, 0), PdfObject::Array(PdfArray(array)));
2086
2087        // Verify all cached correctly
2088        assert_eq!(
2089            resources.get_cached((1, 0)).unwrap(),
2090            PdfObject::Boolean(true)
2091        );
2092        assert_eq!(
2093            resources.get_cached((2, 0)).unwrap(),
2094            PdfObject::Real(3.14159)
2095        );
2096        assert_eq!(
2097            resources.get_cached((3, 0)).unwrap(),
2098            PdfObject::String(PdfString::new(b"Hello PDF".to_vec()))
2099        );
2100        assert_eq!(
2101            resources.get_cached((4, 0)).unwrap(),
2102            PdfObject::Name(PdfName::new("Type".to_string()))
2103        );
2104        assert!(matches!(
2105            resources.get_cached((5, 0)).unwrap(),
2106            PdfObject::Dictionary(_)
2107        ));
2108        assert!(matches!(
2109            resources.get_cached((6, 0)).unwrap(),
2110            PdfObject::Array(_)
2111        ));
2112    }
2113
2114    // Tests for PdfDocument removed due to API incompatibilities
2115    // The methods tested don't exist in the current implementation
2116
2117    /*
2118        #[test]
2119        fn test_pdf_document_new_initialization() {
2120            // Create a minimal PDF for testing
2121            let data = b"%PDF-1.4
2122    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2123    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2124    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2125    xref
2126    0 4
2127    0000000000 65535 f
2128    0000000009 00000 n
2129    0000000052 00000 n
2130    0000000101 00000 n
2131    trailer<</Size 4/Root 1 0 R>>
2132    startxref
2133    164
2134    %%EOF";
2135            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2136            let document = PdfDocument::new(reader);
2137
2138            // Document should be created successfully
2139            // Initially no page tree loaded
2140            assert!(document.page_tree.borrow().is_none());
2141            assert!(document.metadata_cache.borrow().is_none());
2142        }
2143
2144        #[test]
2145        fn test_pdf_document_version() {
2146            // Create a minimal PDF for testing
2147            let data = b"%PDF-1.4
2148    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2149    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2150    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2151    xref
2152    0 4
2153    0000000000 65535 f
2154    0000000009 00000 n
2155    0000000052 00000 n
2156    0000000101 00000 n
2157    trailer<</Size 4/Root 1 0 R>>
2158    startxref
2159    164
2160    %%EOF";
2161            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2162            let document = PdfDocument::new(reader);
2163
2164            let version = document.version().unwrap();
2165            assert!(!version.is_empty());
2166            // Most PDFs are version 1.4 to 1.7
2167            assert!(version.starts_with("1.") || version.starts_with("2."));
2168        }
2169
2170        #[test]
2171        fn test_pdf_document_page_count() {
2172            // Create a minimal PDF for testing
2173            let data = b"%PDF-1.4
2174    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2175    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2176    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2177    xref
2178    0 4
2179    0000000000 65535 f
2180    0000000009 00000 n
2181    0000000052 00000 n
2182    0000000101 00000 n
2183    trailer<</Size 4/Root 1 0 R>>
2184    startxref
2185    164
2186    %%EOF";
2187            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2188            let document = PdfDocument::new(reader);
2189
2190            let count = document.page_count().unwrap();
2191            assert!(count > 0);
2192        }
2193
2194        #[test]
2195        fn test_pdf_document_metadata() {
2196            // Create a minimal PDF for testing
2197            let data = b"%PDF-1.4
2198    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2199    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2200    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2201    xref
2202    0 4
2203    0000000000 65535 f
2204    0000000009 00000 n
2205    0000000052 00000 n
2206    0000000101 00000 n
2207    trailer<</Size 4/Root 1 0 R>>
2208    startxref
2209    164
2210    %%EOF";
2211            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2212            let document = PdfDocument::new(reader);
2213
2214            let metadata = document.metadata().unwrap();
2215            // Metadata should be cached after first access
2216            assert!(document.metadata_cache.borrow().is_some());
2217
2218            // Second call should use cache
2219            let metadata2 = document.metadata().unwrap();
2220            assert_eq!(metadata.title, metadata2.title);
2221        }
2222
2223        #[test]
2224        fn test_pdf_document_get_page() {
2225            // Create a minimal PDF for testing
2226            let data = b"%PDF-1.4
2227    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2228    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2229    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2230    xref
2231    0 4
2232    0000000000 65535 f
2233    0000000009 00000 n
2234    0000000052 00000 n
2235    0000000101 00000 n
2236    trailer<</Size 4/Root 1 0 R>>
2237    startxref
2238    164
2239    %%EOF";
2240            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2241            let document = PdfDocument::new(reader);
2242
2243            // Get first page
2244            let page = document.get_page(0).unwrap();
2245            assert!(page.width() > 0.0);
2246            assert!(page.height() > 0.0);
2247
2248            // Page tree should be loaded now
2249            assert!(document.page_tree.borrow().is_some());
2250        }
2251
2252        #[test]
2253        fn test_pdf_document_get_page_out_of_bounds() {
2254            // Create a minimal PDF for testing
2255            let data = b"%PDF-1.4
2256    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2257    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2258    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2259    xref
2260    0 4
2261    0000000000 65535 f
2262    0000000009 00000 n
2263    0000000052 00000 n
2264    0000000101 00000 n
2265    trailer<</Size 4/Root 1 0 R>>
2266    startxref
2267    164
2268    %%EOF";
2269            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2270            let document = PdfDocument::new(reader);
2271
2272            let page_count = document.page_count().unwrap();
2273
2274            // Try to get page beyond count
2275            let result = document.get_page(page_count + 10);
2276            assert!(result.is_err());
2277        }
2278
2279
2280        #[test]
2281        fn test_pdf_document_get_object() {
2282            // Create a minimal PDF for testing
2283            let data = b"%PDF-1.4
2284    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2285    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2286    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2287    xref
2288    0 4
2289    0000000000 65535 f
2290    0000000009 00000 n
2291    0000000052 00000 n
2292    0000000101 00000 n
2293    trailer<</Size 4/Root 1 0 R>>
2294    startxref
2295    164
2296    %%EOF";
2297            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2298            let document = PdfDocument::new(reader);
2299
2300            // Get an object (catalog is usually object 1 0)
2301            let obj = document.get_object(1, 0);
2302            assert!(obj.is_ok());
2303
2304            // Object should be cached
2305            assert!(document.resources.get_cached((1, 0)).is_some());
2306        }
2307
2308
2309
2310        #[test]
2311        fn test_pdf_document_extract_text_from_page() {
2312            // Create a minimal PDF for testing
2313            let data = b"%PDF-1.4
2314    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2315    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2316    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2317    xref
2318    0 4
2319    0000000000 65535 f
2320    0000000009 00000 n
2321    0000000052 00000 n
2322    0000000101 00000 n
2323    trailer<</Size 4/Root 1 0 R>>
2324    startxref
2325    164
2326    %%EOF";
2327            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2328            let document = PdfDocument::new(reader);
2329
2330            // Try to extract text from first page
2331            let result = document.extract_text_from_page(0);
2332            // Even if no text, should not error
2333            assert!(result.is_ok());
2334        }
2335
2336        #[test]
2337        fn test_pdf_document_extract_all_text() {
2338            // Create a minimal PDF for testing
2339            let data = b"%PDF-1.4
2340    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2341    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2342    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2343    xref
2344    0 4
2345    0000000000 65535 f
2346    0000000009 00000 n
2347    0000000052 00000 n
2348    0000000101 00000 n
2349    trailer<</Size 4/Root 1 0 R>>
2350    startxref
2351    164
2352    %%EOF";
2353            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2354            let document = PdfDocument::new(reader);
2355
2356            let extracted = document.extract_text().unwrap();
2357            let page_count = document.page_count().unwrap();
2358
2359            // Should have text for each page
2360            assert_eq!(extracted.len(), page_count);
2361        }
2362
2363
2364        #[test]
2365        fn test_pdf_document_ensure_page_tree() {
2366            // Create a minimal PDF for testing
2367            let data = b"%PDF-1.4
2368    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2369    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2370    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2371    xref
2372    0 4
2373    0000000000 65535 f
2374    0000000009 00000 n
2375    0000000052 00000 n
2376    0000000101 00000 n
2377    trailer<</Size 4/Root 1 0 R>>
2378    startxref
2379    164
2380    %%EOF";
2381            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2382            let document = PdfDocument::new(reader);
2383
2384            // Initially no page tree
2385            assert!(document.page_tree.borrow().is_none());
2386
2387            // After ensuring, should be loaded
2388            document.ensure_page_tree().unwrap();
2389            assert!(document.page_tree.borrow().is_some());
2390
2391            // Second call should not error
2392            document.ensure_page_tree().unwrap();
2393        }
2394
2395        #[test]
2396        fn test_resource_manager_concurrent_access() {
2397            let resources = ResourceManager::new();
2398
2399            // Simulate concurrent-like access pattern
2400            resources.cache_object((1, 0), PdfObject::Integer(1));
2401            let obj1 = resources.get_cached((1, 0));
2402
2403            resources.cache_object((2, 0), PdfObject::Integer(2));
2404            let obj2 = resources.get_cached((2, 0));
2405
2406            // Both should be accessible
2407            assert_eq!(obj1.unwrap(), PdfObject::Integer(1));
2408            assert_eq!(obj2.unwrap(), PdfObject::Integer(2));
2409        }
2410
2411        #[test]
2412        fn test_resource_manager_large_cache() {
2413            let resources = ResourceManager::new();
2414
2415            // Cache many objects
2416            for i in 0..1000 {
2417                resources.cache_object((i, 0), PdfObject::Integer(i as i64));
2418            }
2419
2420            // Verify random access
2421            assert_eq!(resources.get_cached((500, 0)).unwrap(), PdfObject::Integer(500));
2422            assert_eq!(resources.get_cached((999, 0)).unwrap(), PdfObject::Integer(999));
2423            assert_eq!(resources.get_cached((0, 0)).unwrap(), PdfObject::Integer(0));
2424
2425            // Clear should remove all
2426            resources.clear_cache();
2427            assert!(resources.get_cached((500, 0)).is_none());
2428        }
2429        */
2430}