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**: Objects are cached per-document for efficient reuse
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, PdfStream};
55use super::page_tree::{PageTree, ParsedPage};
56use super::reader::PdfReader;
57use super::{ParseError, ParseOptions, ParseResult};
58use std::cell::RefCell;
59use std::collections::{HashMap, HashSet};
60use std::fs::File;
61use std::io::{Read, Seek};
62use std::path::Path;
63
64/// Resource manager for efficient PDF object caching.
65///
66/// The ResourceManager provides centralized caching of PDF objects to avoid
67/// repeated parsing overhead. It is owned exclusively by a `PdfDocument` and uses
68/// `RefCell` for interior mutability, so cache writes can occur through a shared
69/// borrow of the document.
70///
71/// # Caching Strategy
72///
73/// - Objects are cached on first access
74/// - Cache persists for the lifetime of the document
75/// - Manual cache clearing is supported for memory management
76///
77/// # Example
78///
79/// ```rust,no_run
80/// use oxidize_pdf::parser::document::ResourceManager;
81///
82/// let resources = ResourceManager::new();
83///
84/// // Objects are cached automatically when accessed through PdfDocument
85/// // Manual cache management:
86/// resources.clear_cache(); // Free memory when needed
87/// ```
88pub struct ResourceManager {
89    /// Cached objects indexed by (object_number, generation_number)
90    object_cache: RefCell<HashMap<(u32, u16), PdfObject>>,
91}
92
93impl Default for ResourceManager {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl ResourceManager {
100    /// Create a new resource manager
101    pub fn new() -> Self {
102        Self {
103            object_cache: RefCell::new(HashMap::new()),
104        }
105    }
106
107    /// Get an object from cache if available.
108    ///
109    /// # Arguments
110    ///
111    /// * `obj_ref` - Object reference (object_number, generation_number)
112    ///
113    /// # Returns
114    ///
115    /// Cloned object if cached, None otherwise.
116    ///
117    /// # Example
118    ///
119    /// ```rust,no_run
120    /// # use oxidize_pdf::parser::document::ResourceManager;
121    /// # let resources = ResourceManager::new();
122    /// if let Some(obj) = resources.get_cached((10, 0)) {
123    ///     println!("Object 10 0 R found in cache");
124    /// }
125    /// ```
126    pub fn get_cached(&self, obj_ref: (u32, u16)) -> Option<PdfObject> {
127        self.object_cache.borrow().get(&obj_ref).cloned()
128    }
129
130    /// Cache an object for future access.
131    ///
132    /// # Arguments
133    ///
134    /// * `obj_ref` - Object reference (object_number, generation_number)
135    /// * `obj` - The PDF object to cache
136    ///
137    /// # Example
138    ///
139    /// ```rust,no_run
140    /// # use oxidize_pdf::parser::document::ResourceManager;
141    /// # use oxidize_pdf::parser::objects::PdfObject;
142    /// # let resources = ResourceManager::new();
143    /// resources.cache_object((10, 0), PdfObject::Integer(42));
144    /// ```
145    pub fn cache_object(&self, obj_ref: (u32, u16), obj: PdfObject) {
146        self.object_cache.borrow_mut().insert(obj_ref, obj);
147    }
148
149    /// Clear all cached objects to free memory.
150    ///
151    /// Use this when processing large documents to manage memory usage.
152    ///
153    /// # Example
154    ///
155    /// ```rust,no_run
156    /// # use oxidize_pdf::parser::document::ResourceManager;
157    /// # let resources = ResourceManager::new();
158    /// // After processing many pages
159    /// resources.clear_cache();
160    /// println!("Cache cleared to free memory");
161    /// ```
162    pub fn clear_cache(&self) {
163        self.object_cache.borrow_mut().clear();
164    }
165}
166
167/// High-level PDF document interface for parsing and manipulation.
168///
169/// `PdfDocument` provides a clean, safe API for working with PDF files.
170/// It handles the complexity of PDF structure, object references, and resource
171/// management behind a simple interface.
172///
173/// # Type Parameter
174///
175/// * `R` - The reader type (must implement Read + Seek)
176///
177/// # Architecture Benefits
178///
179/// - **RefCell Usage**: Allows multiple parts of the API to access the document
180/// - **Lazy Loading**: Pages and resources are loaded on demand
181/// - **Automatic Caching**: Frequently accessed objects are cached
182/// - **Safe API**: Borrow checker issues are handled internally
183///
184/// # Thread Safety
185///
186/// `PdfDocument<R>` is `Send` for `R: Send` (issue #369): it can be moved to
187/// another thread or across a Python GIL boundary (e.g. `Python::allow_threads`).
188/// It is intentionally `!Sync` because `RefCell` does not permit shared concurrent
189/// access; callers that need concurrent reads should use one instance per thread.
190///
191/// # Example
192///
193/// ```rust,no_run
194/// use oxidize_pdf::parser::{PdfDocument, PdfReader};
195/// use std::fs::File;
196///
197/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
198/// // From a file
199/// let reader = PdfReader::open("document.pdf")?;
200/// let document = PdfDocument::new(reader);
201///
202/// // From any Read + Seek source
203/// let file = File::open("document.pdf")?;
204/// let reader = PdfReader::new(file)?;
205/// let document = PdfDocument::new(reader);
206///
207/// // Use the document
208/// let page_count = document.page_count()?;
209/// for i in 0..page_count {
210///     let page = document.get_page(i)?;
211///     // Process page...
212/// }
213/// # Ok(())
214/// # }
215/// ```
216pub struct PdfDocument<R: Read + Seek> {
217    /// The underlying PDF reader wrapped for interior mutability
218    reader: RefCell<PdfReader<R>>,
219    /// Page tree navigator (lazily initialized)
220    page_tree: RefCell<Option<PageTree>>,
221    /// Resource manager for object caching (owned; interior mutability via RefCell).
222    /// Not shared/cloned, so a plain owned field keeps `PdfDocument<R>: Send` for
223    /// `R: Send` without any locking overhead (issue #369).
224    resources: ResourceManager,
225    /// Cached document metadata to avoid repeated parsing
226    metadata_cache: RefCell<Option<super::reader::DocumentMetadata>>,
227}
228
229impl<R: Read + Seek> PdfDocument<R> {
230    /// Create a new PDF document from a reader
231    pub fn new(reader: PdfReader<R>) -> Self {
232        Self {
233            reader: RefCell::new(reader),
234            page_tree: RefCell::new(None),
235            resources: ResourceManager::new(),
236            metadata_cache: RefCell::new(None),
237        }
238    }
239
240    /// Get the PDF version of the document.
241    ///
242    /// # Returns
243    ///
244    /// PDF version string (e.g., "1.4", "1.7", "2.0")
245    ///
246    /// # Example
247    ///
248    /// ```rust,no_run
249    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
250    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
251    /// # let reader = PdfReader::open("document.pdf")?;
252    /// # let document = PdfDocument::new(reader);
253    /// let version = document.version()?;
254    /// println!("PDF version: {}", version);
255    /// # Ok(())
256    /// # }
257    /// ```
258    pub fn version(&self) -> ParseResult<String> {
259        Ok(self.reader.borrow().version().to_string())
260    }
261
262    /// Return an owned catalog for crate-internal document-level consumers.
263    pub(crate) fn catalog_dictionary(&self) -> ParseResult<PdfDictionary> {
264        self.reader.borrow_mut().catalog().cloned()
265    }
266
267    /// Get the parse options
268    pub fn options(&self) -> ParseOptions {
269        self.reader.borrow().options().clone()
270    }
271
272    /// Get the total number of pages in the document.
273    ///
274    /// # Returns
275    ///
276    /// The page count as an unsigned 32-bit integer.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error if the page tree is malformed or missing.
281    ///
282    /// # Example
283    ///
284    /// ```rust,no_run
285    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
286    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
287    /// # let reader = PdfReader::open("document.pdf")?;
288    /// # let document = PdfDocument::new(reader);
289    /// let count = document.page_count()?;
290    /// println!("Document has {} pages", count);
291    ///
292    /// // Iterate through all pages
293    /// for i in 0..count {
294    ///     let page = document.get_page(i)?;
295    ///     // Process page...
296    /// }
297    /// # Ok(())
298    /// # }
299    /// ```
300    pub fn page_count(&self) -> ParseResult<u32> {
301        self.ensure_page_tree()?;
302        if let Some(pt) = self.page_tree.borrow().as_ref() {
303            Ok(pt.page_count())
304        } else {
305            // Fallback: should never reach here since ensure_page_tree() just ran
306            self.reader.borrow_mut().page_count()
307        }
308    }
309
310    /// Get document metadata including title, author, creation date, etc.
311    ///
312    /// Metadata is cached after first access for performance.
313    ///
314    /// # Returns
315    ///
316    /// A `DocumentMetadata` struct containing all available metadata fields.
317    ///
318    /// # Example
319    ///
320    /// ```rust,no_run
321    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
322    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
323    /// # let reader = PdfReader::open("document.pdf")?;
324    /// # let document = PdfDocument::new(reader);
325    /// let metadata = document.metadata()?;
326    ///
327    /// if let Some(title) = &metadata.title {
328    ///     println!("Title: {}", title);
329    /// }
330    /// if let Some(author) = &metadata.author {
331    ///     println!("Author: {}", author);
332    /// }
333    /// if let Some(creation_date) = &metadata.creation_date {
334    ///     println!("Created: {}", creation_date);
335    /// }
336    /// println!("PDF Version: {}", metadata.version);
337    /// # Ok(())
338    /// # }
339    /// ```
340    pub fn metadata(&self) -> ParseResult<super::reader::DocumentMetadata> {
341        // Check cache first
342        if let Some(metadata) = self.metadata_cache.borrow().as_ref() {
343            return Ok(metadata.clone());
344        }
345
346        // Load metadata
347        let metadata = self.reader.borrow_mut().metadata()?;
348        self.metadata_cache.borrow_mut().replace(metadata.clone());
349        Ok(metadata)
350    }
351
352    /// Initialize the page tree if not already done.
353    ///
354    /// Builds a flat index of all leaf Page references by walking the tree once.
355    /// This provides O(1) page access and detects cycles and absurd /Count values.
356    fn ensure_page_tree(&self) -> ParseResult<()> {
357        if self.page_tree.borrow().is_none() {
358            let pages_dict = self.load_pages_dict()?;
359            let page_refs = {
360                let mut reader = self.reader.borrow_mut();
361                PageTree::flatten_page_tree(&mut *reader, &pages_dict)?
362            };
363            let page_tree = PageTree::new_with_flat_index(pages_dict, page_refs);
364            self.page_tree.borrow_mut().replace(page_tree);
365        }
366        Ok(())
367    }
368
369    /// Load the pages dictionary
370    fn load_pages_dict(&self) -> ParseResult<PdfDictionary> {
371        let mut reader = self.reader.borrow_mut();
372        let pages = reader.pages()?;
373        Ok(pages.clone())
374    }
375
376    /// Get a page by index (0-based).
377    ///
378    /// Pages are cached after first access. This method handles page tree
379    /// traversal and property inheritance automatically.
380    ///
381    /// # Arguments
382    ///
383    /// * `index` - Zero-based page index (0 to page_count-1)
384    ///
385    /// # Returns
386    ///
387    /// A complete `ParsedPage` with all properties and inherited resources.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if:
392    /// - Index is out of bounds
393    /// - Page tree is malformed
394    /// - Required page properties are missing
395    ///
396    /// # Example
397    ///
398    /// ```rust,no_run
399    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
400    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
401    /// # let reader = PdfReader::open("document.pdf")?;
402    /// # let document = PdfDocument::new(reader);
403    /// // Get the first page
404    /// let page = document.get_page(0)?;
405    ///
406    /// // Access page properties
407    /// println!("Page size: {}x{} points", page.width(), page.height());
408    /// println!("Rotation: {}°", page.rotation);
409    ///
410    /// // Get content streams
411    /// let streams = page.content_streams_with_document(&document)?;
412    /// println!("Page has {} content streams", streams.len());
413    /// # Ok(())
414    /// # }
415    /// ```
416    pub fn get_page(&self, index: u32) -> ParseResult<ParsedPage> {
417        self.ensure_page_tree()?;
418
419        // First check if page is already cached
420        if let Some(page_tree) = self.page_tree.borrow().as_ref() {
421            if let Some(page) = page_tree.get_cached_page(index) {
422                return Ok(page.clone());
423            }
424        }
425
426        // Try flat index O(1) lookup first
427        let (page_ref, has_flat_index) = {
428            let pt_borrow = self.page_tree.borrow();
429            let pt = pt_borrow.as_ref();
430            let ref_val = pt.and_then(|pt| pt.get_page_ref(index));
431            let has_index = pt.map_or(false, |pt| pt.page_count() > 0 || ref_val.is_some());
432            (ref_val, has_index)
433        };
434
435        let page = if let Some(page_ref) = page_ref {
436            self.load_page_by_ref(page_ref)?
437        } else if has_flat_index {
438            // Flat index exists but page not found — index is out of range
439            return Err(ParseError::SyntaxError {
440                position: 0,
441                message: format!(
442                    "Page index {} out of range (document has {} pages)",
443                    index,
444                    self.page_tree
445                        .borrow()
446                        .as_ref()
447                        .map_or(0, |pt| pt.page_count())
448                ),
449            });
450        } else {
451            // No flat index available — fallback to tree traversal
452            self.load_page_at_index(index)?
453        };
454
455        // Cache it
456        if let Some(page_tree) = self.page_tree.borrow_mut().as_mut() {
457            page_tree.cache_page(index, page.clone());
458        }
459
460        Ok(page)
461    }
462
463    /// Load a specific page by index (legacy tree traversal fallback)
464    fn load_page_at_index(&self, index: u32) -> ParseResult<ParsedPage> {
465        // Get the pages root
466        let pages_dict = self.load_pages_dict()?;
467
468        // Navigate to the specific page
469        let page_info = self.find_page_in_tree(&pages_dict, index, 0, None)?;
470
471        Ok(page_info)
472    }
473
474    /// Load a page directly by its object reference (O(1) via flat index).
475    fn load_page_by_ref(&self, page_ref: (u32, u16)) -> ParseResult<ParsedPage> {
476        let obj = self.get_object(page_ref.0, page_ref.1)?;
477        let dict = obj.as_dict().ok_or_else(|| ParseError::SyntaxError {
478            position: 0,
479            message: format!(
480                "Page object {} {} R is not a dictionary",
481                page_ref.0, page_ref.1
482            ),
483        })?;
484
485        let inherited = self.collect_inherited_attributes(dict);
486        self.create_parsed_page(page_ref, dict, Some(&inherited))
487    }
488
489    /// Walk up the /Parent chain to collect inheritable attributes (Resources, MediaBox, CropBox, Rotate).
490    /// Uses cycle detection to prevent infinite loops in malformed PDFs.
491    fn collect_inherited_attributes(&self, page_dict: &PdfDictionary) -> PdfDictionary {
492        let mut inherited = PdfDictionary::new();
493        let inheritable_keys = ["Resources", "MediaBox", "CropBox", "Rotate"];
494
495        // Collect from the page's own parent chain
496        let mut current_parent_ref = page_dict.get("Parent").and_then(|p| p.as_reference());
497        let mut visited: std::collections::HashSet<(u32, u16)> = std::collections::HashSet::new();
498
499        while let Some(parent_ref) = current_parent_ref {
500            if !visited.insert(parent_ref) {
501                break; // Cycle detected
502            }
503
504            match self.get_object(parent_ref.0, parent_ref.1) {
505                Ok(obj) => {
506                    if let Some(parent_dict) = obj.as_dict() {
507                        for key in &inheritable_keys {
508                            // Only inherit if the page itself doesn't have it
509                            // and we haven't already found it in a closer ancestor
510                            if !page_dict.contains_key(key) && !inherited.contains_key(key) {
511                                if let Some(val) = parent_dict.get(key) {
512                                    inherited.insert((*key).to_string(), val.clone());
513                                }
514                            }
515                        }
516                        current_parent_ref =
517                            parent_dict.get("Parent").and_then(|p| p.as_reference());
518                    } else {
519                        break;
520                    }
521                }
522                Err(_) => break,
523            }
524        }
525
526        inherited
527    }
528
529    /// Find a page in the page tree (iterative implementation for stack safety)
530    fn find_page_in_tree(
531        &self,
532        root_node: &PdfDictionary,
533        target_index: u32,
534        initial_current_index: u32,
535        initial_inherited: Option<&PdfDictionary>,
536    ) -> ParseResult<ParsedPage> {
537        // Work item for the traversal queue
538        #[derive(Debug)]
539        struct WorkItem {
540            node_dict: PdfDictionary,
541            node_ref: Option<(u32, u16)>,
542            current_index: u32,
543            inherited: Option<PdfDictionary>,
544        }
545
546        // Initialize work queue with root node
547        let mut work_queue = Vec::new();
548        work_queue.push(WorkItem {
549            node_dict: root_node.clone(),
550            node_ref: None,
551            current_index: initial_current_index,
552            inherited: initial_inherited.cloned(),
553        });
554
555        // Iterative traversal
556        while let Some(work_item) = work_queue.pop() {
557            let WorkItem {
558                node_dict,
559                node_ref,
560                current_index,
561                inherited,
562            } = work_item;
563
564            let node_type = node_dict
565                .get_type()
566                .or_else(|| {
567                    // If Type is missing, try to infer from content
568                    if node_dict.contains_key("Kids") && node_dict.contains_key("Count") {
569                        Some("Pages")
570                    } else if node_dict.contains_key("Contents")
571                        || node_dict.contains_key("MediaBox")
572                    {
573                        Some("Page")
574                    } else {
575                        None
576                    }
577                })
578                .or_else(|| {
579                    // If Type is missing, try to infer from structure
580                    if node_dict.contains_key("Kids") {
581                        Some("Pages")
582                    } else if node_dict.contains_key("Contents")
583                        || (node_dict.contains_key("MediaBox") && !node_dict.contains_key("Kids"))
584                    {
585                        Some("Page")
586                    } else {
587                        None
588                    }
589                })
590                .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
591
592            match node_type {
593                "Pages" => {
594                    // This is a page tree node
595                    let kids = node_dict
596                        .get("Kids")
597                        .and_then(|obj| obj.as_array())
598                        .or_else(|| {
599                            // If Kids is missing, use empty array
600                            tracing::debug!(
601                                "Warning: Missing Kids array in Pages node, using empty array"
602                            );
603                            Some(&super::objects::EMPTY_PDF_ARRAY)
604                        })
605                        .ok_or_else(|| ParseError::MissingKey("Kids".to_string()))?;
606
607                    // Merge inherited attributes
608                    let mut merged_inherited = inherited.unwrap_or_else(PdfDictionary::new);
609
610                    // Inheritable attributes
611                    for key in ["Resources", "MediaBox", "CropBox", "Rotate"] {
612                        if let Some(value) = node_dict.get(key) {
613                            if !merged_inherited.contains_key(key) {
614                                merged_inherited.insert(key.to_string(), value.clone());
615                            }
616                        }
617                    }
618
619                    // Process kids in reverse order (since we're using a stack/Vec::pop())
620                    // This ensures we process them in the correct order
621                    let mut current_idx = current_index;
622                    let mut pending_kids = Vec::new();
623
624                    for kid_ref in &kids.0 {
625                        let kid_ref =
626                            kid_ref
627                                .as_reference()
628                                .ok_or_else(|| ParseError::SyntaxError {
629                                    position: 0,
630                                    message: "Kids array must contain references".to_string(),
631                                })?;
632
633                        // Get the kid object
634                        let kid_obj = self.get_object(kid_ref.0, kid_ref.1)?;
635                        let kid_dict = match kid_obj.as_dict() {
636                            Some(dict) => dict,
637                            None => {
638                                // Skip invalid page tree nodes in lenient mode
639                                tracing::debug!(
640                                    "Warning: Page tree node {} {} R is not a dictionary, skipping",
641                                    kid_ref.0,
642                                    kid_ref.1
643                                );
644                                current_idx += 1; // Count as processed but skip
645                                continue;
646                            }
647                        };
648
649                        let kid_type = kid_dict
650                            .get_type()
651                            .or_else(|| {
652                                // If Type is missing, try to infer from content
653                                if kid_dict.contains_key("Kids") && kid_dict.contains_key("Count") {
654                                    Some("Pages")
655                                } else if kid_dict.contains_key("Contents")
656                                    || kid_dict.contains_key("MediaBox")
657                                {
658                                    Some("Page")
659                                } else {
660                                    None
661                                }
662                            })
663                            .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
664
665                        let count = if kid_type == "Pages" {
666                            kid_dict
667                                .get("Count")
668                                .and_then(|obj| obj.as_integer())
669                                .unwrap_or(1) // Fallback to 1 if Count is missing (defensive)
670                                as u32
671                        } else {
672                            1
673                        };
674
675                        if target_index < current_idx + count {
676                            // Found the right subtree/page
677                            if kid_type == "Page" {
678                                // This is the page we want
679                                return self.create_parsed_page(
680                                    kid_ref,
681                                    kid_dict,
682                                    Some(&merged_inherited),
683                                );
684                            } else {
685                                // Need to traverse this subtree - add to queue
686                                pending_kids.push(WorkItem {
687                                    node_dict: kid_dict.clone(),
688                                    node_ref: Some(kid_ref),
689                                    current_index: current_idx,
690                                    inherited: Some(merged_inherited.clone()),
691                                });
692                                break; // Found our target subtree, no need to continue
693                            }
694                        }
695
696                        current_idx += count;
697                    }
698
699                    // Add pending kids to work queue in reverse order for correct processing
700                    work_queue.extend(pending_kids.into_iter().rev());
701                }
702                "Page" => {
703                    // This is a page object
704                    if target_index != current_index {
705                        return Err(ParseError::SyntaxError {
706                            position: 0,
707                            message: "Page index mismatch".to_string(),
708                        });
709                    }
710
711                    // We need the reference for creating the parsed page
712                    if let Some(page_ref) = node_ref {
713                        return self.create_parsed_page(page_ref, &node_dict, inherited.as_ref());
714                    } else {
715                        return Err(ParseError::SyntaxError {
716                            position: 0,
717                            message: "Direct page object without reference".to_string(),
718                        });
719                    }
720                }
721                _ => {
722                    return Err(ParseError::SyntaxError {
723                        position: 0,
724                        message: format!("Invalid page tree node type: {node_type}"),
725                    });
726                }
727            }
728        }
729
730        // Try fallback: search for the page by direct object scanning
731        tracing::debug!(
732            "Warning: Page {} not found in tree, attempting direct lookup",
733            target_index
734        );
735
736        // Scan for Page objects directly (try first few hundred objects)
737        for obj_num in 1..500 {
738            if let Ok(obj) = self.reader.borrow_mut().get_object(obj_num, 0) {
739                if let Some(dict) = obj.as_dict() {
740                    if let Some(obj_type) = dict.get("Type").and_then(|t| t.as_name()) {
741                        if obj_type.0 == "Page" {
742                            // Found a page, check if it's the right index (approximate)
743                            return self.create_parsed_page((obj_num, 0), dict, None);
744                        }
745                    }
746                }
747            }
748        }
749
750        Err(ParseError::SyntaxError {
751            position: 0,
752            message: format!("Page {} not found in tree or document", target_index),
753        })
754    }
755
756    /// Create a ParsedPage from a page dictionary
757    fn create_parsed_page(
758        &self,
759        obj_ref: (u32, u16),
760        page_dict: &PdfDictionary,
761        inherited: Option<&PdfDictionary>,
762    ) -> ParseResult<ParsedPage> {
763        // Extract page attributes with fallback for missing MediaBox
764        let media_box = match self.get_rectangle(page_dict, inherited, "MediaBox")? {
765            Some(mb) => mb,
766            None => {
767                // Use default Letter size if MediaBox is missing
768                #[cfg(debug_assertions)]
769                tracing::debug!(
770                    "Warning: Page {} {} R missing MediaBox, using default Letter size",
771                    obj_ref.0,
772                    obj_ref.1
773                );
774                [0.0, 0.0, 612.0, 792.0]
775            }
776        };
777
778        let crop_box = self.get_rectangle(page_dict, inherited, "CropBox")?;
779
780        let rotation = self
781            .get_integer(page_dict, inherited, "Rotate")?
782            .unwrap_or(0) as i32;
783
784        // Resolve the effective /Resources into an owned dictionary so that
785        // `ParsedPage::get_resources()` always yields a dictionary, even when
786        // /Resources is given as an indirect reference (issue #286). The page's
787        // own /Resources takes precedence over inherited ones; when it is an
788        // inline dictionary `get_resources()` returns it directly from the page
789        // dict, so we only need a resolved fallback for the reference / inherited
790        // cases.
791        let inherited_resources = {
792            let own_is_inline_dict = page_dict
793                .get("Resources")
794                .map(|o| o.as_dict().is_some())
795                .unwrap_or(false);
796            if own_is_inline_dict {
797                None
798            } else {
799                page_dict
800                    .get("Resources")
801                    .or_else(|| inherited.and_then(|i| i.get("Resources")))
802                    .and_then(|r| self.resolve(r).ok())
803                    .and_then(|r| r.as_dict().cloned())
804            }
805        };
806
807        // Get annotations if present
808        let annotations = page_dict
809            .get("Annots")
810            .and_then(|obj| obj.as_array())
811            .cloned();
812
813        Ok(ParsedPage {
814            obj_ref,
815            dict: page_dict.clone(),
816            inherited_resources,
817            media_box,
818            crop_box,
819            rotation,
820            annotations,
821        })
822    }
823
824    /// Get a rectangle value
825    fn get_rectangle(
826        &self,
827        node: &PdfDictionary,
828        inherited: Option<&PdfDictionary>,
829        key: &str,
830    ) -> ParseResult<Option<[f64; 4]>> {
831        let array = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
832
833        if let Some(array) = array.and_then(|obj| obj.as_array()) {
834            if array.len() != 4 {
835                return Err(ParseError::SyntaxError {
836                    position: 0,
837                    message: format!("{key} must have 4 elements"),
838                });
839            }
840
841            // After length check, we know array has exactly 4 elements
842            // Safe to index directly without unwrap
843            let rect = [
844                array.0[0].as_real().unwrap_or(0.0),
845                array.0[1].as_real().unwrap_or(0.0),
846                array.0[2].as_real().unwrap_or(0.0),
847                array.0[3].as_real().unwrap_or(0.0),
848            ];
849
850            Ok(Some(rect))
851        } else {
852            Ok(None)
853        }
854    }
855
856    /// Get an integer value
857    fn get_integer(
858        &self,
859        node: &PdfDictionary,
860        inherited: Option<&PdfDictionary>,
861        key: &str,
862    ) -> ParseResult<Option<i64>> {
863        let value = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
864
865        Ok(value.and_then(|obj| obj.as_integer()))
866    }
867
868    /// Get an object by its reference numbers.
869    ///
870    /// This method first checks the cache, then loads from the file if needed.
871    /// Objects are automatically cached after loading.
872    ///
873    /// # Arguments
874    ///
875    /// * `obj_num` - Object number
876    /// * `gen_num` - Generation number
877    ///
878    /// # Returns
879    ///
880    /// The resolved PDF object.
881    ///
882    /// # Errors
883    ///
884    /// Returns an error if:
885    /// - Object doesn't exist
886    /// - Object is part of an encrypted object stream
887    /// - File is corrupted
888    ///
889    /// # Example
890    ///
891    /// ```rust,no_run
892    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
893    /// # use oxidize_pdf::parser::objects::PdfObject;
894    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
895    /// # let reader = PdfReader::open("document.pdf")?;
896    /// # let document = PdfDocument::new(reader);
897    /// // Get object 10 0 R
898    /// let obj = document.get_object(10, 0)?;
899    ///
900    /// // Check object type
901    /// match obj {
902    ///     PdfObject::Dictionary(dict) => {
903    ///         println!("Object is a dictionary with {} entries", dict.0.len());
904    ///     }
905    ///     PdfObject::Stream(stream) => {
906    ///         println!("Object is a stream");
907    ///     }
908    ///     _ => {}
909    /// }
910    /// # Ok(())
911    /// # }
912    /// ```
913    pub fn get_object(&self, obj_num: u32, gen_num: u16) -> ParseResult<PdfObject> {
914        // Check resource cache first
915        if let Some(obj) = self.resources.get_cached((obj_num, gen_num)) {
916            return Ok(obj);
917        }
918
919        // Load from reader
920        let obj = {
921            let mut reader = self.reader.borrow_mut();
922            reader.get_object(obj_num, gen_num)?.clone()
923        };
924
925        // Cache it
926        self.resources.cache_object((obj_num, gen_num), obj.clone());
927
928        Ok(obj)
929    }
930
931    /// Resolve a reference to get the actual object.
932    ///
933    /// If the input is a Reference, fetches the referenced object.
934    /// Otherwise returns a clone of the input object.
935    ///
936    /// # Arguments
937    ///
938    /// * `obj` - The object to resolve (may be a Reference or direct object)
939    ///
940    /// # Returns
941    ///
942    /// The resolved object (never a Reference).
943    ///
944    /// # Example
945    ///
946    /// ```rust,no_run
947    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
948    /// # use oxidize_pdf::parser::objects::PdfObject;
949    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
950    /// # let reader = PdfReader::open("document.pdf")?;
951    /// # let document = PdfDocument::new(reader);
952    /// # let page = document.get_page(0)?;
953    /// // Contents might be a reference or direct object
954    /// if let Some(contents) = page.dict.get("Contents") {
955    ///     let resolved = document.resolve(contents)?;
956    ///     match resolved {
957    ///         PdfObject::Stream(_) => println!("Single content stream"),
958    ///         PdfObject::Array(_) => println!("Multiple content streams"),
959    ///         _ => println!("Unexpected content type"),
960    ///     }
961    /// }
962    /// # Ok(())
963    /// # }
964    /// ```
965    pub fn resolve(&self, obj: &PdfObject) -> ParseResult<PdfObject> {
966        match obj {
967            PdfObject::Reference(obj_num, gen_num) => self.get_object(*obj_num, *gen_num),
968            _ => Ok(obj.clone()),
969        }
970    }
971
972    /// Decode a stream after resolving document-owned `/DecodeParms` references.
973    ///
974    /// This is the document-aware counterpart to [`PdfStream::decode`]. Use it
975    /// when a stream dictionary may contain indirect filter parameter objects.
976    ///
977    /// # Errors
978    ///
979    /// Returns an error when a parameter reference is missing, circular, has an
980    /// invalid type, or when the underlying filter decoder fails.
981    pub fn decode_stream(&self, stream: &PdfStream) -> ParseResult<Vec<u8>> {
982        let dict = self.stream_dict_with_resolved_decode_parms(&stream.dict)?;
983        super::filters::decode_stream(&stream.data, &dict, &self.options())
984    }
985
986    /// Decode a stream with resolved `/DecodeParms` and a maximum output size.
987    ///
988    /// # Errors
989    ///
990    /// Returns the same errors as [`Self::decode_stream`], plus an error when a
991    /// filter would exceed `max_bytes` or has no bounded decoder.
992    pub fn decode_stream_with_limit(
993        &self,
994        stream: &PdfStream,
995        max_bytes: usize,
996    ) -> ParseResult<Vec<u8>> {
997        let dict = self.stream_dict_with_resolved_decode_parms(&stream.dict)?;
998        super::filters::decode_stream_with_limit(&stream.data, &dict, &self.options(), max_bytes)
999    }
1000
1001    fn stream_dict_with_resolved_decode_parms(
1002        &self,
1003        dict: &PdfDictionary,
1004    ) -> ParseResult<PdfDictionary> {
1005        let Some(decode_parms) = dict.get("DecodeParms") else {
1006            return Ok(dict.clone());
1007        };
1008        let mut resolved_dict = dict.clone();
1009        let resolved = match decode_parms {
1010            PdfObject::Array(array) => PdfObject::Array(super::objects::PdfArray(
1011                array
1012                    .0
1013                    .iter()
1014                    .map(|entry| self.resolve_decode_parms_entry(entry, true))
1015                    .collect::<ParseResult<Vec<_>>>()?,
1016            )),
1017            other => self.resolve_decode_parms_entry(other, false)?,
1018        };
1019        resolved_dict.insert("DecodeParms".into(), resolved);
1020        Ok(resolved_dict)
1021    }
1022
1023    fn resolve_decode_parms_entry(
1024        &self,
1025        object: &PdfObject,
1026        array_entry: bool,
1027    ) -> ParseResult<PdfObject> {
1028        let mut current = object.clone();
1029        let mut visited = HashSet::new();
1030        let mut resolved_reference = false;
1031        while let PdfObject::Reference(object_number, generation) = current {
1032            resolved_reference = true;
1033            if !visited.insert((object_number, generation)) {
1034                return Err(ParseError::CircularReference);
1035            }
1036            if visited.len() > super::stack_safe::MAX_RECURSION_DEPTH {
1037                return Err(ParseError::SyntaxError {
1038                    position: 0,
1039                    message: "DecodeParms reference chain exceeds maximum depth".into(),
1040                });
1041            }
1042            current = self.get_object(object_number, generation)?;
1043        }
1044        match current {
1045            PdfObject::Dictionary(_) => Ok(current),
1046            PdfObject::Null if !resolved_reference => Ok(current),
1047            PdfObject::Array(array) if !array_entry => {
1048                Ok(PdfObject::Array(super::objects::PdfArray(
1049                    array
1050                        .0
1051                        .iter()
1052                        .map(|entry| self.resolve_decode_parms_entry(entry, true))
1053                        .collect::<ParseResult<Vec<_>>>()?,
1054                )))
1055            }
1056            _ => Err(ParseError::SyntaxError {
1057                position: 0,
1058                message: "DecodeParms must resolve to a dictionary, null, or an array of those"
1059                    .into(),
1060            }),
1061        }
1062    }
1063
1064    /// Get content streams for a specific page.
1065    ///
1066    /// This method handles both single streams and arrays of streams,
1067    /// automatically decompressing them according to their filters.
1068    ///
1069    /// # Arguments
1070    ///
1071    /// * `page` - The page to get content streams from
1072    ///
1073    /// # Returns
1074    ///
1075    /// Vector of decompressed content stream data ready for parsing.
1076    ///
1077    /// # Example
1078    ///
1079    /// ```rust,no_run
1080    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1081    /// # use oxidize_pdf::parser::content::ContentParser;
1082    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1083    /// # let reader = PdfReader::open("document.pdf")?;
1084    /// # let document = PdfDocument::new(reader);
1085    /// let page = document.get_page(0)?;
1086    /// let streams = document.get_page_content_streams(&page)?;
1087    ///
1088    /// // Parse content streams
1089    /// for stream_data in streams {
1090    ///     let operations = ContentParser::parse(&stream_data)?;
1091    ///     println!("Stream has {} operations", operations.len());
1092    /// }
1093    /// # Ok(())
1094    /// # }
1095    /// ```
1096    /// Get page resources dictionary.
1097    ///
1098    /// This method returns the resources dictionary for a page, which may include
1099    /// fonts, images (XObjects), patterns, color spaces, and other resources.
1100    ///
1101    /// # Arguments
1102    ///
1103    /// * `page` - The page to get resources from
1104    ///
1105    /// # Returns
1106    ///
1107    /// Optional resources dictionary if the page has resources.
1108    ///
1109    /// # Example
1110    ///
1111    /// ```rust,no_run
1112    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader, PdfObject, PdfName};
1113    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1114    /// # let reader = PdfReader::open("document.pdf")?;
1115    /// # let document = PdfDocument::new(reader);
1116    /// let page = document.get_page(0)?;
1117    /// if let Some(resources) = document.get_page_resources(&page)? {
1118    ///     // Check for images (XObjects)
1119    ///     if let Some(PdfObject::Dictionary(xobjects)) = resources.0.get(&PdfName("XObject".to_string())) {
1120    ///         for (name, _) in xobjects.0.iter() {
1121    ///             println!("Found XObject: {}", name.0);
1122    ///         }
1123    ///     }
1124    /// }
1125    /// # Ok(())
1126    /// # }
1127    /// ```
1128    pub fn get_page_resources<'a>(
1129        &self,
1130        page: &'a ParsedPage,
1131    ) -> ParseResult<Option<&'a PdfDictionary>> {
1132        Ok(page.get_resources())
1133    }
1134
1135    pub fn get_page_content_streams(&self, page: &ParsedPage) -> ParseResult<Vec<Vec<u8>>> {
1136        let mut streams = Vec::new();
1137
1138        if let Some(contents) = page.dict.get("Contents") {
1139            let resolved_contents = self.resolve(contents)?;
1140
1141            match &resolved_contents {
1142                PdfObject::Stream(stream) => {
1143                    streams.push(self.decode_stream(stream)?);
1144                }
1145                PdfObject::Array(array) => {
1146                    for item in &array.0 {
1147                        let resolved = self.resolve(item)?;
1148                        if let PdfObject::Stream(stream) = resolved {
1149                            streams.push(self.decode_stream(&stream)?);
1150                        }
1151                    }
1152                }
1153                _ => {
1154                    return Err(ParseError::SyntaxError {
1155                        position: 0,
1156                        message: "Contents must be a stream or array of streams".to_string(),
1157                    })
1158                }
1159            }
1160        }
1161
1162        Ok(streams)
1163    }
1164
1165    /// Extract text from all pages in the document.
1166    ///
1167    /// Uses the default text extraction settings. For custom settings,
1168    /// use `extract_text_with_options`.
1169    ///
1170    /// # Returns
1171    ///
1172    /// A vector of `ExtractedText`, one for each page in the document.
1173    ///
1174    /// # Example
1175    ///
1176    /// ```rust,no_run
1177    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1178    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1179    /// # let reader = PdfReader::open("document.pdf")?;
1180    /// # let document = PdfDocument::new(reader);
1181    /// let extracted_pages = document.extract_text()?;
1182    ///
1183    /// for (page_num, page_text) in extracted_pages.iter().enumerate() {
1184    ///     println!("=== Page {} ===", page_num + 1);
1185    ///     println!("{}", page_text.text);
1186    ///     println!();
1187    /// }
1188    /// # Ok(())
1189    /// # }
1190    /// ```
1191    pub fn extract_text(&self) -> ParseResult<Vec<crate::text::ExtractedText>> {
1192        let mut extractor = crate::text::TextExtractor::new();
1193        extractor.extract_from_document(self)
1194    }
1195
1196    /// Extract text from a specific page.
1197    ///
1198    /// # Arguments
1199    ///
1200    /// * `page_index` - Zero-based page index
1201    ///
1202    /// # Returns
1203    ///
1204    /// Extracted text with optional position information.
1205    ///
1206    /// # Example
1207    ///
1208    /// ```rust,no_run
1209    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1210    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1211    /// # let reader = PdfReader::open("document.pdf")?;
1212    /// # let document = PdfDocument::new(reader);
1213    /// // Extract text from first page only
1214    /// let page_text = document.extract_text_from_page(0)?;
1215    /// println!("First page text: {}", page_text.text);
1216    ///
1217    /// // Access text fragments with positions (if preserved)
1218    /// for fragment in &page_text.fragments {
1219    ///     println!("'{}' at ({}, {})", fragment.text, fragment.x, fragment.y);
1220    /// }
1221    /// # Ok(())
1222    /// # }
1223    /// ```
1224    pub fn extract_text_from_page(
1225        &self,
1226        page_index: u32,
1227    ) -> ParseResult<crate::text::ExtractedText> {
1228        let mut extractor = crate::text::TextExtractor::new();
1229        extractor.extract_from_page(self, page_index)
1230    }
1231
1232    /// Extract text from a specific page with custom options.
1233    ///
1234    /// This method combines the functionality of [`extract_text_from_page`] and
1235    /// [`extract_text_with_options`], allowing fine control over extraction
1236    /// behavior for a single page.
1237    ///
1238    /// # Arguments
1239    ///
1240    /// * `page_index` - Zero-based page index
1241    /// * `options` - Text extraction configuration
1242    ///
1243    /// # Returns
1244    ///
1245    /// Extracted text with optional position information.
1246    ///
1247    /// # Example
1248    ///
1249    /// ```rust,no_run
1250    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1251    /// # use oxidize_pdf::text::ExtractionOptions;
1252    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1253    /// # let reader = PdfReader::open("document.pdf")?;
1254    /// # let document = PdfDocument::new(reader);
1255    /// // Use higher space threshold for PDFs with micro-adjustments
1256    /// let options = ExtractionOptions {
1257    ///     space_threshold: 0.4,
1258    ///     ..Default::default()
1259    /// };
1260    ///
1261    /// let page_text = document.extract_text_from_page_with_options(0, options)?;
1262    /// println!("Text: {}", page_text.text);
1263    /// # Ok(())
1264    /// # }
1265    /// ```
1266    pub fn extract_text_from_page_with_options(
1267        &self,
1268        page_index: u32,
1269        options: crate::text::ExtractionOptions,
1270    ) -> ParseResult<crate::text::ExtractedText> {
1271        let mut extractor = crate::text::TextExtractor::with_options(options);
1272        extractor.extract_from_page(self, page_index)
1273    }
1274
1275    /// Extract text with custom extraction options.
1276    ///
1277    /// Allows fine control over text extraction behavior including
1278    /// layout preservation, spacing thresholds, and more.
1279    ///
1280    /// # Arguments
1281    ///
1282    /// * `options` - Text extraction configuration
1283    ///
1284    /// # Returns
1285    ///
1286    /// A vector of `ExtractedText`, one for each page.
1287    ///
1288    /// # Example
1289    ///
1290    /// ```rust,no_run
1291    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1292    /// # use oxidize_pdf::text::ExtractionOptions;
1293    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1294    /// # let reader = PdfReader::open("document.pdf")?;
1295    /// # let document = PdfDocument::new(reader);
1296    /// // Configure extraction to preserve layout
1297    /// let options = ExtractionOptions {
1298    ///     preserve_layout: true,
1299    ///     space_threshold: 0.3,
1300    ///     newline_threshold: 10.0,
1301    ///     ..Default::default()
1302    /// };
1303    ///
1304    /// let extracted_pages = document.extract_text_with_options(options)?;
1305    ///
1306    /// // Text fragments will include position information
1307    /// for page_text in extracted_pages {
1308    ///     for fragment in &page_text.fragments {
1309    ///         println!("{:?}", fragment);
1310    ///     }
1311    /// }
1312    /// # Ok(())
1313    /// # }
1314    /// ```
1315    pub fn extract_text_with_options(
1316        &self,
1317        options: crate::text::ExtractionOptions,
1318    ) -> ParseResult<Vec<crate::text::ExtractedText>> {
1319        let mut extractor = crate::text::TextExtractor::with_options(options);
1320        extractor.extract_from_document(self)
1321    }
1322
1323    /// Get annotations from a specific page.
1324    ///
1325    /// Returns a vector of annotation dictionaries for the specified page.
1326    /// Each annotation dictionary contains properties like Type, Rect, Contents, etc.
1327    ///
1328    /// # Arguments
1329    ///
1330    /// * `page_index` - Zero-based page index
1331    ///
1332    /// # Returns
1333    ///
1334    /// A vector of PdfDictionary objects representing annotations, or an empty vector
1335    /// if the page has no annotations.
1336    ///
1337    /// # Example
1338    ///
1339    /// ```rust,no_run
1340    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1341    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1342    /// # let reader = PdfReader::open("document.pdf")?;
1343    /// # let document = PdfDocument::new(reader);
1344    /// let annotations = document.get_page_annotations(0)?;
1345    /// for annot in &annotations {
1346    ///     if let Some(contents) = annot.get("Contents").and_then(|c| c.as_string()) {
1347    ///         println!("Annotation: {:?}", contents);
1348    ///     }
1349    /// }
1350    /// # Ok(())
1351    /// # }
1352    /// ```
1353    pub fn get_page_annotations(&self, page_index: u32) -> ParseResult<Vec<PdfDictionary>> {
1354        let page = self.get_page(page_index)?;
1355
1356        if let Some(annots_array) = page.get_annotations() {
1357            let mut annotations = Vec::new();
1358            let mut reader = self.reader.borrow_mut();
1359
1360            for annot_ref in &annots_array.0 {
1361                if let Some(ref_nums) = annot_ref.as_reference() {
1362                    match reader.get_object(ref_nums.0, ref_nums.1) {
1363                        Ok(obj) => {
1364                            if let Some(dict) = obj.as_dict() {
1365                                annotations.push(dict.clone());
1366                            }
1367                        }
1368                        Err(_) => {
1369                            // Skip annotations that can't be loaded
1370                            continue;
1371                        }
1372                    }
1373                }
1374            }
1375
1376            Ok(annotations)
1377        } else {
1378            Ok(Vec::new())
1379        }
1380    }
1381
1382    /// Get all annotations from all pages in the document.
1383    ///
1384    /// Returns a vector of tuples containing (page_index, annotations) for each page
1385    /// that has annotations.
1386    ///
1387    /// # Returns
1388    ///
1389    /// A vector of tuples where the first element is the page index and the second
1390    /// is a vector of annotation dictionaries for that page.
1391    ///
1392    /// # Example
1393    ///
1394    /// ```rust,no_run
1395    /// # use oxidize_pdf::parser::{PdfDocument, PdfReader};
1396    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1397    /// # let reader = PdfReader::open("document.pdf")?;
1398    /// # let document = PdfDocument::new(reader);
1399    /// let all_annotations = document.get_all_annotations()?;
1400    /// for (page_idx, annotations) in all_annotations {
1401    ///     println!("Page {} has {} annotations", page_idx, annotations.len());
1402    /// }
1403    /// # Ok(())
1404    /// # }
1405    /// ```
1406    pub fn get_all_annotations(&self) -> ParseResult<Vec<(u32, Vec<PdfDictionary>)>> {
1407        let page_count = self.page_count()?;
1408        let mut all_annotations = Vec::new();
1409
1410        for i in 0..page_count {
1411            let annotations = self.get_page_annotations(i)?;
1412            if !annotations.is_empty() {
1413                all_annotations.push((i, annotations));
1414            }
1415        }
1416
1417        Ok(all_annotations)
1418    }
1419
1420    // --- VibeCoding Facade Methods ---
1421
1422    /// Export the document to LLM-optimized Markdown format.
1423    ///
1424    /// Delegates to [`crate::ai::export_to_markdown`]. Includes YAML frontmatter
1425    /// with document metadata followed by extracted text content.
1426    #[allow(deprecated)]
1427    pub fn to_markdown(&self) -> crate::error::Result<String> {
1428        crate::ai::export_to_markdown(self)
1429    }
1430
1431    /// Export the document to element-aware Markdown format.
1432    ///
1433    /// Unlike [`to_markdown`](Self::to_markdown), this method classifies elements
1434    /// by type and maps each to its canonical Markdown representation.
1435    pub fn to_element_markdown(&self) -> ParseResult<String> {
1436        let elements = self.partition()?;
1437        let exporter = crate::pipeline::export::ElementMarkdownExporter::default();
1438        Ok(exporter.export(&elements))
1439    }
1440
1441    /// Export the document to a contextual text format for LLM consumption.
1442    ///
1443    /// Delegates to [`crate::ai::export_to_contextual`].
1444    #[allow(deprecated)]
1445    pub fn to_contextual(&self) -> crate::error::Result<String> {
1446        crate::ai::export_to_contextual(self)
1447    }
1448
1449    /// Export the document to structured JSON format.
1450    ///
1451    /// Requires the `semantic` feature. Delegates to [`crate::ai::export_to_json`].
1452    #[cfg(feature = "semantic")]
1453    #[allow(deprecated)]
1454    pub fn to_json(&self) -> crate::error::Result<String> {
1455        crate::ai::export_to_json(self)
1456    }
1457
1458    /// Extract and chunk the document into RAG-ready chunks with full metadata.
1459    ///
1460    /// Uses default [`HybridChunkConfig`](crate::pipeline::HybridChunkConfig)
1461    /// (512 tokens, `AnyInlineContent` merge policy). Returns serializable
1462    /// [`RagChunk`](crate::pipeline::RagChunk)s with page numbers, bounding boxes,
1463    /// element types, and heading context — everything a vector store needs.
1464    ///
1465    /// # Example
1466    ///
1467    /// ```rust,no_run
1468    /// use oxidize_pdf::parser::{PdfDocument, PdfReader};
1469    ///
1470    /// let doc = PdfDocument::open("document.pdf")?;
1471    /// let chunks = doc.rag_chunks()?;
1472    /// for chunk in &chunks {
1473    ///     println!("Chunk {}: pages {:?}, ~{} tokens",
1474    ///         chunk.chunk_index, chunk.page_numbers, chunk.token_estimate);
1475    /// }
1476    /// # Ok::<(), Box<dyn std::error::Error>>(())
1477    /// ```
1478    pub fn rag_chunks(&self) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1479        self.rag_chunks_with(crate::pipeline::HybridChunkConfig::default())
1480    }
1481
1482    /// Extract and chunk the document with a custom chunking configuration.
1483    ///
1484    /// Use this when the default 512-token limit is too large or too small for your
1485    /// vector store or embedding model. All other metadata (pages, bounding boxes,
1486    /// element types, heading context) is identical to [`rag_chunks()`](Self::rag_chunks).
1487    ///
1488    /// # Example
1489    ///
1490    /// ```rust,no_run
1491    /// use oxidize_pdf::parser::{PdfDocument, PdfReader};
1492    /// use oxidize_pdf::pipeline::HybridChunkConfig;
1493    ///
1494    /// let doc = PdfDocument::open("document.pdf")?;
1495    /// let config = HybridChunkConfig {
1496    ///     max_tokens: 256,
1497    ///     ..HybridChunkConfig::default()
1498    /// };
1499    /// let chunks = doc.rag_chunks_with(config)?;
1500    /// println!("Got {} chunks at 256-token limit", chunks.len());
1501    /// # Ok::<(), Box<dyn std::error::Error>>(())
1502    /// ```
1503    pub fn rag_chunks_with(
1504        &self,
1505        config: crate::pipeline::HybridChunkConfig,
1506    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1507        let elements = self.partition()?;
1508        let context_mode = config.context_mode;
1509        let chunker = crate::pipeline::HybridChunker::new(config);
1510        let hybrid_chunks = chunker.chunk(&elements);
1511        Ok(self.build_rag_chunks(&hybrid_chunks, None, context_mode))
1512    }
1513
1514    /// Extract and chunk with a custom [`TokenCounter`](crate::pipeline::TokenCounter).
1515    ///
1516    /// Identical to [`rag_chunks_with`](Self::rag_chunks_with) except the injected
1517    /// counter governs both the chunk split/size decisions and the reported
1518    /// `token_estimate` on each [`RagChunk`](crate::pipeline::RagChunk). Use with
1519    /// [`TiktokenCounter`](crate::pipeline::TiktokenCounter) (feature `tiktoken`)
1520    /// to size chunks against a real subword tokenizer.
1521    ///
1522    /// Note: this flows through [`HybridChunker`](crate::pipeline::HybridChunker).
1523    /// The `unstable-spi` pipeline entry points (`rag_chunks_with_pipeline` /
1524    /// `rag_chunks_from_elements`) remain word-proxy only.
1525    pub fn rag_chunks_with_counter(
1526        &self,
1527        config: crate::pipeline::HybridChunkConfig,
1528        counter: std::sync::Arc<dyn crate::pipeline::TokenCounter>,
1529    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1530        let elements = self.partition()?;
1531        let context_mode = config.context_mode;
1532        let chunker = crate::pipeline::HybridChunker::new(config).with_token_counter(counter);
1533        let hybrid_chunks = chunker.chunk(&elements);
1534        Ok(self.build_rag_chunks(&hybrid_chunks, None, context_mode))
1535    }
1536
1537    /// Build RAG chunks stamped with source-document metadata.
1538    ///
1539    /// Auto-fills `title`/`author`/`creation_date`/`total_pages` from the info
1540    /// dictionary (only where the caller left them `None`); the caller-supplied
1541    /// `source` provides `filename`/`doc_hash` (and may override any auto-filled
1542    /// field). `doc_hash`, when set, becomes the stable prefix of every
1543    /// `chunk_id`. Same chunking pipeline as [`rag_chunks`](Self::rag_chunks).
1544    ///
1545    /// # Example
1546    ///
1547    /// ```rust,no_run
1548    /// use oxidize_pdf::parser::PdfDocument;
1549    /// use oxidize_pdf::pipeline::DocumentSource;
1550    ///
1551    /// let doc = PdfDocument::open("document.pdf")?;
1552    /// let mut source = DocumentSource::default();
1553    /// source.filename = Some("document.pdf".to_string());
1554    /// source.doc_hash = Some("sha256-prefix".to_string());
1555    /// let chunks = doc.rag_chunks_with_source(source)?;
1556    /// # Ok::<(), Box<dyn std::error::Error>>(())
1557    /// ```
1558    pub fn rag_chunks_with_source(
1559        &self,
1560        source: crate::pipeline::DocumentSource,
1561    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1562        self.rag_chunks_with_source_and_config(
1563            source,
1564            crate::pipeline::HybridChunkConfig::default(),
1565        )
1566    }
1567
1568    /// Like [`rag_chunks_with_source`](Self::rag_chunks_with_source) but with a
1569    /// custom chunking configuration — for callers that need both
1570    /// source-document stamping and a non-default token budget.
1571    ///
1572    /// # Example
1573    ///
1574    /// ```rust,no_run
1575    /// use oxidize_pdf::parser::PdfDocument;
1576    /// use oxidize_pdf::pipeline::{DocumentSource, HybridChunkConfig};
1577    ///
1578    /// let doc = PdfDocument::open("document.pdf")?;
1579    /// let source = DocumentSource::with_file(Some("document.pdf".into()), None);
1580    /// let config = HybridChunkConfig { max_tokens: 256, ..Default::default() };
1581    /// let chunks = doc.rag_chunks_with_source_and_config(source, config)?;
1582    /// # Ok::<(), Box<dyn std::error::Error>>(())
1583    /// ```
1584    pub fn rag_chunks_with_source_and_config(
1585        &self,
1586        mut source: crate::pipeline::DocumentSource,
1587        config: crate::pipeline::HybridChunkConfig,
1588    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1589        self.autofill_source(&mut source);
1590        let elements = self.partition()?;
1591        let context_mode = config.context_mode;
1592        let chunker = crate::pipeline::HybridChunker::new(config);
1593        let hybrid_chunks = chunker.chunk(&elements);
1594        Ok(self.build_rag_chunks(&hybrid_chunks, Some(source), context_mode))
1595    }
1596
1597    /// Fill `title`/`author`/`creation_date`/`total_pages` from the info
1598    /// dictionary where the caller left them `None`.
1599    fn autofill_source(&self, source: &mut crate::pipeline::DocumentSource) {
1600        if let Ok(meta) = self.metadata() {
1601            source.title = source.title.take().or(meta.title);
1602            source.author = source.author.take().or(meta.author);
1603            source.creation_date = source.creation_date.take().or(meta.creation_date);
1604            source.total_pages = source.total_pages.or(meta.page_count);
1605        }
1606        if source.total_pages.is_none() {
1607            source.total_pages = self.page_count().ok();
1608        }
1609    }
1610
1611    /// Run a custom [`AnalysisPipeline`](crate::pipeline::AnalysisPipeline):
1612    /// partition, optionally classify elements, apply the pipeline's chunking
1613    /// strategy, build linked `RagChunk`s (ids, prev/next, metadata, optional
1614    /// source) exactly as the other `rag_chunks*` entry points do, then run any
1615    /// enrichers over each chunk's `extra` bag.
1616    ///
1617    /// `AnalysisPipeline::new()` reproduces [`rag_chunks`](Self::rag_chunks).
1618    ///
1619    /// Partitioning uses the pipeline's
1620    /// [`PartitionConfig`](crate::pipeline::PartitionConfig) (default unless set
1621    /// via [`with_partition_config`](crate::pipeline::AnalysisPipeline::with_partition_config)),
1622    /// so a structure-aware consumer can override the table detector when it
1623    /// misclassifies the document (issue #345).
1624    ///
1625    /// **Stability:** requires `unstable-spi`; exempt from semver until promoted.
1626    ///
1627    /// # Example
1628    ///
1629    /// ```rust,no_run
1630    /// # use oxidize_pdf::parser::PdfDocument;
1631    /// # use oxidize_pdf::pipeline::AnalysisPipeline;
1632    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1633    /// let doc = PdfDocument::open("document.pdf")?;
1634    /// // Default pipeline == rag_chunks(); swap in a custom strategy/classifier/
1635    /// // enricher via the builder to extend it.
1636    /// let chunks = doc.rag_chunks_with_pipeline(&AnalysisPipeline::new())?;
1637    /// println!("{} chunks", chunks.len());
1638    /// # Ok(())
1639    /// # }
1640    /// ```
1641    #[cfg(feature = "unstable-spi")]
1642    pub fn rag_chunks_with_pipeline(
1643        &self,
1644        pipeline: &crate::pipeline::AnalysisPipeline,
1645    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1646        let elements = self.partition_with(pipeline.partition_config.clone())?;
1647        self.rag_chunks_from_elements(elements, pipeline)
1648    }
1649
1650    /// Run a custom [`AnalysisPipeline`](crate::pipeline::AnalysisPipeline)
1651    /// (classify → chunk → enrich) over **caller-provided** elements, instead of
1652    /// the document's own partition.
1653    ///
1654    /// This is the element-source seam behind
1655    /// [`rag_chunks_with_pipeline`](Self::rag_chunks_with_pipeline), which is
1656    /// exactly `self.rag_chunks_from_elements(self.partition_with(cfg)?, pipeline)`.
1657    /// Use it to feed externally-recovered elements (e.g. list items a two-column
1658    /// layout scrambles past the partitioner) into the same enriched chunk flow
1659    /// as the rest of the document — with uniform classification and enrichment,
1660    /// avoiding the `RagChunk` metadata-stamping workaround that bypasses both
1661    /// (issue #360). Partitioned and recovered elements can be mixed freely.
1662    ///
1663    /// The pipeline's [`PartitionConfig`](crate::pipeline::PartitionConfig) is not
1664    /// consulted here — the caller has already chosen the elements. The pipeline's
1665    /// [`source`](crate::pipeline::AnalysisPipeline::with_source), when set, is
1666    /// still autofilled from this document (title/author/page count) and stamped
1667    /// onto the chunks, exactly as in `rag_chunks_with_pipeline`.
1668    ///
1669    /// **Stability:** requires `unstable-spi`; exempt from semver until promoted.
1670    #[cfg(feature = "unstable-spi")]
1671    pub fn rag_chunks_from_elements(
1672        &self,
1673        mut elements: Vec<crate::pipeline::Element>,
1674        pipeline: &crate::pipeline::AnalysisPipeline,
1675    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1676        let mut source = pipeline.source.clone();
1677        if let Some(src) = source.as_mut() {
1678            self.autofill_source(src);
1679        }
1680        if let Some(classifier) = pipeline.classifier.as_deref() {
1681            // Two passes: read labels against an immutable slice, then apply —
1682            // the classifier inspects neighbours via `ClassifyContext`, so it
1683            // cannot run while the slice is being mutated.
1684            let labels: Vec<Option<crate::pipeline::ClassLabel>> = (0..elements.len())
1685                .map(|index| {
1686                    let ctx = crate::pipeline::ClassifyContext {
1687                        elements: &elements,
1688                        index,
1689                    };
1690                    classifier.classify(&elements[index], &ctx)
1691                })
1692                .collect();
1693            for (element, label) in elements.iter_mut().zip(labels) {
1694                if let Some(label) = label {
1695                    element.metadata_mut().class_label = Some(label.0.into_owned());
1696                }
1697            }
1698        }
1699        let groups = pipeline.chunking.chunk(&elements);
1700        let hybrid: Vec<crate::pipeline::HybridChunk> = groups
1701            .into_iter()
1702            .map(|g| crate::pipeline::HybridChunk::from_group(g, pipeline.max_tokens))
1703            .collect();
1704        // `mut` is needed only for the enricher pass below (gated `semantic`);
1705        // without that feature the binding is never mutated — silence the warning.
1706        #[allow(unused_mut)]
1707        let mut chunks = self.build_rag_chunks(&hybrid, source, pipeline.context_mode);
1708        #[cfg(feature = "semantic")]
1709        if !pipeline.enrichers.is_empty() {
1710            // Enrich each chunk's `extra` bag. The hybrid chunk (kept alongside)
1711            // supplies the source elements; text/heading_path are snapshotted to
1712            // release the immutable borrow before mutating `metadata`.
1713            for (chunk, hc) in chunks.iter_mut().zip(hybrid.iter()) {
1714                let text = chunk.text.clone();
1715                let heading_path = chunk.metadata.heading_path.clone();
1716                let ctx = crate::pipeline::EnrichContext {
1717                    text: &text,
1718                    elements: hc.elements(),
1719                    heading_path: &heading_path,
1720                };
1721                for enricher in &pipeline.enrichers {
1722                    enricher.enrich(&ctx, &mut chunk.metadata);
1723                }
1724            }
1725        }
1726        Ok(chunks)
1727    }
1728
1729    /// Build linked [`RagChunk`]s from hybrid chunks, optionally stamping a
1730    /// [`DocumentSource`](crate::pipeline::DocumentSource), then wiring
1731    /// prev/next ids. Shared by all `rag_chunks*` entry points (DRY).
1732    fn build_rag_chunks(
1733        &self,
1734        hybrid_chunks: &[crate::pipeline::HybridChunk],
1735        source: Option<crate::pipeline::DocumentSource>,
1736        context_mode: crate::pipeline::ContextMode,
1737    ) -> Vec<crate::pipeline::RagChunk> {
1738        let mut chunks: Vec<crate::pipeline::RagChunk> = match &source {
1739            Some(s) => hybrid_chunks
1740                .iter()
1741                .enumerate()
1742                .map(|(i, hc)| {
1743                    crate::pipeline::RagChunk::from_hybrid_chunk_with_source_and_mode(
1744                        i,
1745                        hc,
1746                        s,
1747                        context_mode,
1748                    )
1749                })
1750                .collect(),
1751            None => hybrid_chunks
1752                .iter()
1753                .enumerate()
1754                .map(|(i, hc)| {
1755                    crate::pipeline::RagChunk::from_hybrid_chunk_with_mode(i, hc, context_mode)
1756                })
1757                .collect(),
1758        };
1759        crate::pipeline::chunk_metadata::link_chunks(&mut chunks);
1760        chunks
1761    }
1762
1763    /// Extract and chunk the document using a pre-configured extraction profile.
1764    ///
1765    /// Combines [`partition_with_profile`](Self::partition_with_profile) with
1766    /// [`HybridChunker`](crate::pipeline::HybridChunker) using default chunking
1767    /// settings. Use [`rag_chunks_with`](Self::rag_chunks_with) when you need
1768    /// to tune `max_tokens` or `overlap_tokens`.
1769    ///
1770    /// # Example
1771    ///
1772    /// ```rust,no_run
1773    /// use oxidize_pdf::parser::PdfDocument;
1774    /// use oxidize_pdf::pipeline::ExtractionProfile;
1775    ///
1776    /// let doc = PdfDocument::open("document.pdf")?;
1777    /// let chunks = doc.rag_chunks_with_profile(ExtractionProfile::Rag)?;
1778    /// println!("Got {} RAG chunks", chunks.len());
1779    /// # Ok::<(), Box<dyn std::error::Error>>(())
1780    /// ```
1781    pub fn rag_chunks_with_profile(
1782        &self,
1783        profile: crate::pipeline::ExtractionProfile,
1784    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1785        let elements = self.partition_with_profile(profile)?;
1786        let chunker = crate::pipeline::HybridChunker::default();
1787        let hybrid_chunks = chunker.chunk(&elements);
1788        Ok(self.build_rag_chunks(&hybrid_chunks, None, crate::pipeline::ContextMode::Heading))
1789    }
1790
1791    /// Combine a pre-configured extraction profile with a custom chunking config.
1792    ///
1793    /// Use this when you need both profile-tuned partitioning (e.g. `Rag` with
1794    /// XYCut reading order) and a non-default chunk size.
1795    ///
1796    /// # Example
1797    ///
1798    /// ```rust,no_run
1799    /// use oxidize_pdf::parser::PdfDocument;
1800    /// use oxidize_pdf::pipeline::{ExtractionProfile, HybridChunkConfig};
1801    ///
1802    /// let doc = PdfDocument::open("document.pdf")?;
1803    /// let config = HybridChunkConfig { max_tokens: 256, ..Default::default() };
1804    /// let chunks = doc.rag_chunks_with_profile_config(ExtractionProfile::Rag, config)?;
1805    /// # Ok::<(), Box<dyn std::error::Error>>(())
1806    /// ```
1807    pub fn rag_chunks_with_profile_config(
1808        &self,
1809        profile: crate::pipeline::ExtractionProfile,
1810        config: crate::pipeline::HybridChunkConfig,
1811    ) -> ParseResult<Vec<crate::pipeline::RagChunk>> {
1812        let elements = self.partition_with_profile(profile)?;
1813        let context_mode = config.context_mode;
1814        let chunker = crate::pipeline::HybridChunker::new(config);
1815        let hybrid_chunks = chunker.chunk(&elements);
1816        Ok(self.build_rag_chunks(&hybrid_chunks, None, context_mode))
1817    }
1818
1819    /// Extract chunks as a JSON string ready for vector store ingestion.
1820    ///
1821    /// # Feature flags
1822    ///
1823    /// Requires the `semantic` feature: `oxidize-pdf = { features = ["semantic"] }`.
1824    /// Without it this method is not compiled.
1825    #[cfg(feature = "semantic")]
1826    pub fn rag_chunks_json(&self) -> ParseResult<String> {
1827        let chunks = self.rag_chunks()?;
1828        serde_json::to_string(&chunks).map_err(|e| ParseError::SerializationError(e.to_string()))
1829    }
1830
1831    /// Split the document text into chunks of approximately `target_tokens` size.
1832    ///
1833    /// Uses a default overlap of 10% of the target token count.
1834    #[deprecated(
1835        since = "2.2.0",
1836        note = "Use rag_chunks() for structure-aware RAG chunking"
1837    )]
1838    #[allow(deprecated)]
1839    pub fn chunk(
1840        &self,
1841        target_tokens: usize,
1842    ) -> crate::error::Result<Vec<crate::ai::DocumentChunk>> {
1843        let overlap = target_tokens / 10;
1844        self.chunk_with(target_tokens, overlap)
1845    }
1846
1847    /// Split the document text into chunks with explicit size and overlap control.
1848    #[deprecated(
1849        since = "2.2.0",
1850        note = "Use rag_chunks_with() for structure-aware RAG chunking"
1851    )]
1852    pub fn chunk_with(
1853        &self,
1854        target_tokens: usize,
1855        overlap: usize,
1856    ) -> crate::error::Result<Vec<crate::ai::DocumentChunk>> {
1857        let chunker = crate::ai::DocumentChunker::new(target_tokens, overlap);
1858        let extracted = self.extract_text()?;
1859        let page_texts: Vec<(usize, String)> = extracted
1860            .iter()
1861            .enumerate()
1862            .map(|(i, t)| (i + 1, t.text.clone()))
1863            .collect();
1864        chunker
1865            .chunk_text_with_pages(&page_texts)
1866            .map_err(|e| crate::error::PdfError::InvalidStructure(e.to_string()))
1867    }
1868
1869    /// Partition the document into typed elements using default configuration.
1870    ///
1871    /// Extracts text with layout preservation, then classifies fragments into
1872    /// [`Element`](crate::pipeline::Element) variants (Title, Paragraph, Table, etc.).
1873    pub fn partition(&self) -> ParseResult<Vec<crate::pipeline::Element>> {
1874        self.partition_with(crate::pipeline::PartitionConfig::default())
1875    }
1876
1877    /// Partition the document into typed elements with custom configuration.
1878    pub fn partition_with(
1879        &self,
1880        config: crate::pipeline::PartitionConfig,
1881    ) -> ParseResult<Vec<crate::pipeline::Element>> {
1882        let options = crate::text::ExtractionOptions {
1883            preserve_layout: true,
1884            reconstruct_paragraphs: true,
1885            ..Default::default()
1886        };
1887        self.do_partition_pages(options, config)
1888    }
1889
1890    /// Partition the document using a pre-configured extraction profile.
1891    pub fn partition_with_profile(
1892        &self,
1893        profile: crate::pipeline::ExtractionProfile,
1894    ) -> ParseResult<Vec<crate::pipeline::Element>> {
1895        let profile_cfg = profile.config();
1896        let options = crate::text::ExtractionOptions {
1897            preserve_layout: true,
1898            reconstruct_paragraphs: true,
1899            space_threshold: profile_cfg.extraction.space_threshold,
1900            detect_columns: profile_cfg.extraction.detect_columns,
1901            ..crate::text::ExtractionOptions::default()
1902        };
1903        self.do_partition_pages(options, profile_cfg.partition)
1904    }
1905
1906    fn do_partition_pages(
1907        &self,
1908        options: crate::text::ExtractionOptions,
1909        config: crate::pipeline::PartitionConfig,
1910    ) -> ParseResult<Vec<crate::pipeline::Element>> {
1911        // Read the gating flags before `config` is moved into the partitioner,
1912        // so we avoid cloning the config just to inspect two bools.
1913        let extract_graphics = config.detect_tables && config.prefer_ruling_tables;
1914
1915        // The reconstructed `pages` (extracted with `reconstruct_paragraphs = true`)
1916        // merge per-cell fragments into paragraph-granular fragments (issue #261),
1917        // which the ruling-based table detector cannot map back to grid cells. When
1918        // a page actually has a drawn table grid we re-extract just that page with
1919        // `reconstruct_paragraphs = false` to recover cell-granular fragments for
1920        // the detector; the reconstructed fragments still drive prose
1921        // classification. Inherit every other option (notably `space_threshold`
1922        // and `detect_columns`, which profiles override) so cell text is assembled
1923        // identically to the primary pass. Built before `options` is moved into
1924        // `extract_text_with_options`.
1925        let mut raw_options = options.clone();
1926        raw_options.reconstruct_paragraphs = false;
1927
1928        let pages = self.extract_text_with_options(options)?;
1929
1930        let partitioner = crate::pipeline::Partitioner::new(config);
1931        let mut graphics_extractor = crate::graphics::extraction::GraphicsExtractor::default();
1932        // Extracting per table-bearing page (rather than a second whole-document
1933        // pass) keeps the cost proportional to pages that need it and zero for
1934        // table-free documents even with `prefer_ruling_tables` on.
1935        let mut raw_extractor = crate::text::TextExtractor::with_options(raw_options);
1936
1937        let mut all_elements = Vec::new();
1938        for (page_idx, page_text) in pages.iter().enumerate() {
1939            let page_idx_u32 = u32::try_from(page_idx).map_err(|_| ParseError::SyntaxError {
1940                position: 0,
1941                message: format!("Page index {} exceeds u32 range", page_idx),
1942            })?;
1943            let page_height = self
1944                .get_page(page_idx_u32)
1945                .map(|p| p.height())
1946                .unwrap_or(842.0);
1947            let page_graphics = if extract_graphics {
1948                graphics_extractor.extract_from_page(self, page_idx).ok()
1949            } else {
1950                None
1951            };
1952            // Re-extract cell-granular fragments only for pages with a drawn grid.
1953            let raw_page = if page_graphics
1954                .as_ref()
1955                .is_some_and(|g| g.has_table_structure())
1956            {
1957                raw_extractor.extract_from_page(self, page_idx_u32).ok()
1958            } else {
1959                None
1960            };
1961            let raw_fragments = raw_page.as_ref().map(|pt| pt.fragments.as_slice());
1962            let elements = partitioner.partition_fragments_with_graphics_raw(
1963                &page_text.fragments,
1964                raw_fragments,
1965                page_graphics.as_ref(),
1966                page_idx_u32,
1967                page_height,
1968            );
1969            all_elements.extend(elements);
1970        }
1971
1972        Ok(all_elements)
1973    }
1974
1975    /// Partition the document into typed elements and build a relationship graph.
1976    ///
1977    /// Returns a tuple of `(elements, graph)` where the graph captures parent/child
1978    /// and next/prev relationships between elements by index.
1979    ///
1980    /// # Example
1981    ///
1982    /// ```rust,no_run
1983    /// use oxidize_pdf::parser::PdfDocument;
1984    /// use oxidize_pdf::pipeline::PartitionConfig;
1985    ///
1986    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1987    /// let doc = PdfDocument::open("document.pdf")?;
1988    /// let (elements, graph) = doc.partition_graph(PartitionConfig::default())?;
1989    ///
1990    /// for title_idx in graph.top_level_sections() {
1991    ///     println!("Section: {}", elements[title_idx].text());
1992    ///     for child_idx in graph.elements_in_section(title_idx) {
1993    ///         println!("  {}", elements[child_idx].text());
1994    ///     }
1995    /// }
1996    /// # Ok(())
1997    /// # }
1998    /// ```
1999    pub fn partition_graph(
2000        &self,
2001        config: crate::pipeline::PartitionConfig,
2002    ) -> ParseResult<(Vec<crate::pipeline::Element>, crate::pipeline::ElementGraph)> {
2003        let elements = self.partition_with(config)?;
2004        let graph = crate::pipeline::ElementGraph::build(&elements);
2005        Ok((elements, graph))
2006    }
2007}
2008
2009impl PdfDocument<File> {
2010    /// Open a PDF file by path — the simplest way to start working with a PDF.
2011    ///
2012    /// This is a convenience method that combines `PdfReader::open()` and
2013    /// `PdfDocument::new()` into a single call.
2014    ///
2015    /// # Example
2016    ///
2017    /// ```rust,no_run
2018    /// use oxidize_pdf::parser::PdfDocument;
2019    ///
2020    /// let doc = PdfDocument::open("report.pdf").unwrap();
2021    /// let text = doc.extract_text().unwrap();
2022    /// let markdown = doc.to_markdown().unwrap();
2023    /// ```
2024    pub fn open<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
2025        PdfReader::open_document(path)
2026    }
2027}
2028
2029#[cfg(test)]
2030mod tests {
2031    use super::*;
2032    use crate::parser::objects::{PdfObject, PdfString};
2033    use std::io::Cursor;
2034
2035    // Issue #369: PdfDocument<R> must be Send so callers (e.g. the PyO3
2036    // bindings) can release a thread-blocking lock around reader operations.
2037    #[test]
2038    fn test_pdf_document_is_send() {
2039        fn assert_send<T: Send>() {}
2040        assert_send::<PdfDocument<std::fs::File>>();
2041        assert_send::<PdfDocument<Cursor<Vec<u8>>>>();
2042        assert_send::<PdfDocument<Cursor<&'static [u8]>>>();
2043    }
2044
2045    // Issue #369: dropping the unshared Rc must not change cache behavior.
2046    #[test]
2047    fn test_resource_cache_roundtrip_after_owned_field() {
2048        let rm = ResourceManager::new();
2049        assert!(rm.get_cached((7, 0)).is_none());
2050        rm.cache_object((7, 0), PdfObject::Integer(42));
2051        assert_eq!(rm.get_cached((7, 0)), Some(PdfObject::Integer(42)));
2052        rm.clear_cache();
2053        assert!(rm.get_cached((7, 0)).is_none());
2054    }
2055
2056    // Helper function to create a minimal PDF in memory
2057    fn create_minimal_pdf() -> Vec<u8> {
2058        let mut pdf = Vec::new();
2059
2060        // PDF header
2061        pdf.extend_from_slice(b"%PDF-1.4\n");
2062
2063        // Catalog object
2064        pdf.extend_from_slice(b"1 0 obj\n");
2065        pdf.extend_from_slice(b"<< /Type /Catalog /Pages 2 0 R >>\n");
2066        pdf.extend_from_slice(b"endobj\n");
2067
2068        // Pages object
2069        pdf.extend_from_slice(b"2 0 obj\n");
2070        pdf.extend_from_slice(b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>\n");
2071        pdf.extend_from_slice(b"endobj\n");
2072
2073        // Page object
2074        pdf.extend_from_slice(b"3 0 obj\n");
2075        pdf.extend_from_slice(
2076            b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << >> >>\n",
2077        );
2078        pdf.extend_from_slice(b"endobj\n");
2079
2080        // Cross-reference table
2081        let xref_pos = pdf.len();
2082        pdf.extend_from_slice(b"xref\n");
2083        pdf.extend_from_slice(b"0 4\n");
2084        pdf.extend_from_slice(b"0000000000 65535 f \n");
2085        pdf.extend_from_slice(b"0000000009 00000 n \n");
2086        pdf.extend_from_slice(b"0000000058 00000 n \n");
2087        pdf.extend_from_slice(b"0000000115 00000 n \n");
2088
2089        // Trailer
2090        pdf.extend_from_slice(b"trailer\n");
2091        pdf.extend_from_slice(b"<< /Size 4 /Root 1 0 R >>\n");
2092        pdf.extend_from_slice(b"startxref\n");
2093        pdf.extend_from_slice(format!("{xref_pos}\n").as_bytes());
2094        pdf.extend_from_slice(b"%%EOF\n");
2095
2096        pdf
2097    }
2098
2099    // Helper to create a PDF with metadata
2100    fn create_pdf_with_metadata() -> Vec<u8> {
2101        let mut pdf = Vec::new();
2102
2103        // PDF header
2104        pdf.extend_from_slice(b"%PDF-1.5\n");
2105
2106        // Record positions for xref
2107        let obj1_pos = pdf.len();
2108
2109        // Catalog object
2110        pdf.extend_from_slice(b"1 0 obj\n");
2111        pdf.extend_from_slice(b"<< /Type /Catalog /Pages 2 0 R >>\n");
2112        pdf.extend_from_slice(b"endobj\n");
2113
2114        let obj2_pos = pdf.len();
2115
2116        // Pages object
2117        pdf.extend_from_slice(b"2 0 obj\n");
2118        pdf.extend_from_slice(b"<< /Type /Pages /Kids [] /Count 0 >>\n");
2119        pdf.extend_from_slice(b"endobj\n");
2120
2121        let obj3_pos = pdf.len();
2122
2123        // Info object
2124        pdf.extend_from_slice(b"3 0 obj\n");
2125        pdf.extend_from_slice(
2126            b"<< /Title (Test Document) /Author (Test Author) /Subject (Test Subject) >>\n",
2127        );
2128        pdf.extend_from_slice(b"endobj\n");
2129
2130        // Cross-reference table
2131        let xref_pos = pdf.len();
2132        pdf.extend_from_slice(b"xref\n");
2133        pdf.extend_from_slice(b"0 4\n");
2134        pdf.extend_from_slice(b"0000000000 65535 f \n");
2135        pdf.extend_from_slice(format!("{obj1_pos:010} 00000 n \n").as_bytes());
2136        pdf.extend_from_slice(format!("{obj2_pos:010} 00000 n \n").as_bytes());
2137        pdf.extend_from_slice(format!("{obj3_pos:010} 00000 n \n").as_bytes());
2138
2139        // Trailer
2140        pdf.extend_from_slice(b"trailer\n");
2141        pdf.extend_from_slice(b"<< /Size 4 /Root 1 0 R /Info 3 0 R >>\n");
2142        pdf.extend_from_slice(b"startxref\n");
2143        pdf.extend_from_slice(format!("{xref_pos}\n").as_bytes());
2144        pdf.extend_from_slice(b"%%EOF\n");
2145
2146        pdf
2147    }
2148
2149    #[test]
2150    fn test_pdf_document_new() {
2151        let pdf_data = create_minimal_pdf();
2152        let cursor = Cursor::new(pdf_data);
2153        let reader = PdfReader::new(cursor).unwrap();
2154        let document = PdfDocument::new(reader);
2155
2156        // Verify document is created with empty caches
2157        assert!(document.page_tree.borrow().is_none());
2158        assert!(document.metadata_cache.borrow().is_none());
2159    }
2160
2161    #[test]
2162    fn test_version() {
2163        let pdf_data = create_minimal_pdf();
2164        let cursor = Cursor::new(pdf_data);
2165        let reader = PdfReader::new(cursor).unwrap();
2166        let document = PdfDocument::new(reader);
2167
2168        let version = document.version().unwrap();
2169        assert_eq!(version, "1.4");
2170    }
2171
2172    #[test]
2173    fn test_page_count() {
2174        let pdf_data = create_minimal_pdf();
2175        let cursor = Cursor::new(pdf_data);
2176        let reader = PdfReader::new(cursor).unwrap();
2177        let document = PdfDocument::new(reader);
2178
2179        let count = document.page_count().unwrap();
2180        assert_eq!(count, 1);
2181    }
2182
2183    #[test]
2184    fn test_metadata() {
2185        let pdf_data = create_pdf_with_metadata();
2186        let cursor = Cursor::new(pdf_data);
2187        let reader = PdfReader::new(cursor).unwrap();
2188        let document = PdfDocument::new(reader);
2189
2190        let metadata = document.metadata().unwrap();
2191        assert_eq!(metadata.title, Some("Test Document".to_string()));
2192        assert_eq!(metadata.author, Some("Test Author".to_string()));
2193        assert_eq!(metadata.subject, Some("Test Subject".to_string()));
2194
2195        // Verify caching works
2196        let metadata2 = document.metadata().unwrap();
2197        assert_eq!(metadata.title, metadata2.title);
2198    }
2199
2200    #[test]
2201    fn test_get_page() {
2202        let pdf_data = create_minimal_pdf();
2203        let cursor = Cursor::new(pdf_data);
2204        let reader = PdfReader::new(cursor).unwrap();
2205        let document = PdfDocument::new(reader);
2206
2207        // Get first page
2208        let page = document.get_page(0).unwrap();
2209        assert_eq!(page.media_box, [0.0, 0.0, 612.0, 792.0]);
2210
2211        // Verify caching works
2212        let page2 = document.get_page(0).unwrap();
2213        assert_eq!(page.media_box, page2.media_box);
2214    }
2215
2216    #[test]
2217    fn test_get_page_out_of_bounds() {
2218        let pdf_data = create_minimal_pdf();
2219        let cursor = Cursor::new(pdf_data);
2220        let reader = PdfReader::new(cursor).unwrap();
2221        let document = PdfDocument::new(reader);
2222
2223        // Try to get page that doesn't exist
2224        let result = document.get_page(10);
2225        // With fallback lookup, this might succeed or fail gracefully
2226        if result.is_err() {
2227            assert!(result.unwrap_err().to_string().contains("Page"));
2228        } else {
2229            // If succeeds, should return a valid page
2230            let _page = result.unwrap();
2231        }
2232    }
2233
2234    #[test]
2235    fn test_resource_manager_caching() {
2236        let resources = ResourceManager::new();
2237
2238        // Test caching an object
2239        let obj_ref = (1, 0);
2240        let obj = PdfObject::String(PdfString("Test".as_bytes().to_vec()));
2241
2242        assert!(resources.get_cached(obj_ref).is_none());
2243
2244        resources.cache_object(obj_ref, obj.clone());
2245
2246        let cached = resources.get_cached(obj_ref).unwrap();
2247        assert_eq!(cached, obj);
2248
2249        // Test clearing cache
2250        resources.clear_cache();
2251        assert!(resources.get_cached(obj_ref).is_none());
2252    }
2253
2254    #[test]
2255    fn test_get_object() {
2256        let pdf_data = create_minimal_pdf();
2257        let cursor = Cursor::new(pdf_data);
2258        let reader = PdfReader::new(cursor).unwrap();
2259        let document = PdfDocument::new(reader);
2260
2261        // Get catalog object
2262        let catalog = document.get_object(1, 0).unwrap();
2263        if let PdfObject::Dictionary(dict) = catalog {
2264            if let Some(PdfObject::Name(name)) = dict.get("Type") {
2265                assert_eq!(name.0, "Catalog");
2266            } else {
2267                panic!("Expected /Type name");
2268            }
2269        } else {
2270            panic!("Expected dictionary object");
2271        }
2272    }
2273
2274    #[test]
2275    fn test_resolve_reference() {
2276        let pdf_data = create_minimal_pdf();
2277        let cursor = Cursor::new(pdf_data);
2278        let reader = PdfReader::new(cursor).unwrap();
2279        let document = PdfDocument::new(reader);
2280
2281        // Create a reference to the catalog
2282        let ref_obj = PdfObject::Reference(1, 0);
2283
2284        // Resolve it
2285        let resolved = document.resolve(&ref_obj).unwrap();
2286        if let PdfObject::Dictionary(dict) = resolved {
2287            if let Some(PdfObject::Name(name)) = dict.get("Type") {
2288                assert_eq!(name.0, "Catalog");
2289            } else {
2290                panic!("Expected /Type name");
2291            }
2292        } else {
2293            panic!("Expected dictionary object");
2294        }
2295    }
2296
2297    #[test]
2298    fn test_resolve_non_reference() {
2299        let pdf_data = create_minimal_pdf();
2300        let cursor = Cursor::new(pdf_data);
2301        let reader = PdfReader::new(cursor).unwrap();
2302        let document = PdfDocument::new(reader);
2303
2304        // Try to resolve a non-reference object
2305        let obj = PdfObject::String(PdfString("Test".as_bytes().to_vec()));
2306        let resolved = document.resolve(&obj).unwrap();
2307
2308        // Should return the same object
2309        assert_eq!(resolved, obj);
2310    }
2311
2312    #[test]
2313    fn test_invalid_pdf_data() {
2314        let invalid_data = b"This is not a PDF";
2315        let cursor = Cursor::new(invalid_data.to_vec());
2316        let result = PdfReader::new(cursor);
2317
2318        assert!(result.is_err());
2319    }
2320
2321    #[test]
2322    fn test_empty_page_tree() {
2323        // Create PDF with empty page tree
2324        let pdf_data = create_pdf_with_metadata(); // This has 0 pages
2325        let cursor = Cursor::new(pdf_data);
2326        let reader = PdfReader::new(cursor).unwrap();
2327        let document = PdfDocument::new(reader);
2328
2329        let count = document.page_count().unwrap();
2330        assert_eq!(count, 0);
2331
2332        // Try to get a page from empty document
2333        let result = document.get_page(0);
2334        assert!(result.is_err());
2335    }
2336
2337    #[test]
2338    fn test_extract_text_empty_document() {
2339        let pdf_data = create_pdf_with_metadata();
2340        let cursor = Cursor::new(pdf_data);
2341        let reader = PdfReader::new(cursor).unwrap();
2342        let document = PdfDocument::new(reader);
2343
2344        let text = document.extract_text().unwrap();
2345        assert!(text.is_empty());
2346    }
2347
2348    #[test]
2349    fn test_concurrent_access() {
2350        let pdf_data = create_minimal_pdf();
2351        let cursor = Cursor::new(pdf_data);
2352        let reader = PdfReader::new(cursor).unwrap();
2353        let document = PdfDocument::new(reader);
2354
2355        // Access multiple things concurrently
2356        let version = document.version().unwrap();
2357        let count = document.page_count().unwrap();
2358        let page = document.get_page(0).unwrap();
2359
2360        assert_eq!(version, "1.4");
2361        assert_eq!(count, 1);
2362        assert_eq!(page.media_box[2], 612.0);
2363    }
2364
2365    // Additional comprehensive tests
2366    mod comprehensive_tests {
2367        use super::*;
2368
2369        #[test]
2370        fn test_resource_manager_default() {
2371            let resources = ResourceManager::default();
2372            assert!(resources.get_cached((1, 0)).is_none());
2373        }
2374
2375        #[test]
2376        fn test_resource_manager_multiple_objects() {
2377            let resources = ResourceManager::new();
2378
2379            // Cache multiple objects
2380            resources.cache_object((1, 0), PdfObject::Integer(42));
2381            resources.cache_object((2, 0), PdfObject::Boolean(true));
2382            resources.cache_object(
2383                (3, 0),
2384                PdfObject::String(PdfString("test".as_bytes().to_vec())),
2385            );
2386
2387            // Verify all are cached
2388            assert!(resources.get_cached((1, 0)).is_some());
2389            assert!(resources.get_cached((2, 0)).is_some());
2390            assert!(resources.get_cached((3, 0)).is_some());
2391
2392            // Clear and verify empty
2393            resources.clear_cache();
2394            assert!(resources.get_cached((1, 0)).is_none());
2395            assert!(resources.get_cached((2, 0)).is_none());
2396            assert!(resources.get_cached((3, 0)).is_none());
2397        }
2398
2399        #[test]
2400        fn test_resource_manager_object_overwrite() {
2401            let resources = ResourceManager::new();
2402
2403            // Cache an object
2404            resources.cache_object((1, 0), PdfObject::Integer(42));
2405            assert_eq!(resources.get_cached((1, 0)), Some(PdfObject::Integer(42)));
2406
2407            // Overwrite with different object
2408            resources.cache_object((1, 0), PdfObject::Boolean(true));
2409            assert_eq!(resources.get_cached((1, 0)), Some(PdfObject::Boolean(true)));
2410        }
2411
2412        #[test]
2413        fn test_get_object_caching() {
2414            let pdf_data = create_minimal_pdf();
2415            let cursor = Cursor::new(pdf_data);
2416            let reader = PdfReader::new(cursor).unwrap();
2417            let document = PdfDocument::new(reader);
2418
2419            // Get object first time (should cache)
2420            let obj1 = document.get_object(1, 0).unwrap();
2421
2422            // Get same object again (should use cache)
2423            let obj2 = document.get_object(1, 0).unwrap();
2424
2425            // Objects should be identical
2426            assert_eq!(obj1, obj2);
2427
2428            // Verify it's cached
2429            assert!(document.resources.get_cached((1, 0)).is_some());
2430        }
2431
2432        #[test]
2433        fn test_get_object_different_generations() {
2434            let pdf_data = create_minimal_pdf();
2435            let cursor = Cursor::new(pdf_data);
2436            let reader = PdfReader::new(cursor).unwrap();
2437            let document = PdfDocument::new(reader);
2438
2439            // Get object with generation 0
2440            let _obj1 = document.get_object(1, 0).unwrap();
2441
2442            // Try to get same object with different generation (should fail)
2443            let result = document.get_object(1, 1);
2444            assert!(result.is_err());
2445
2446            // Original should still be cached
2447            assert!(document.resources.get_cached((1, 0)).is_some());
2448        }
2449
2450        #[test]
2451        fn test_get_object_nonexistent() {
2452            let pdf_data = create_minimal_pdf();
2453            let cursor = Cursor::new(pdf_data);
2454            let reader = PdfReader::new(cursor).unwrap();
2455            let document = PdfDocument::new(reader);
2456
2457            // Try to get non-existent object
2458            let result = document.get_object(999, 0);
2459            assert!(result.is_err());
2460        }
2461
2462        #[test]
2463        fn test_resolve_nested_references() {
2464            let pdf_data = create_minimal_pdf();
2465            let cursor = Cursor::new(pdf_data);
2466            let reader = PdfReader::new(cursor).unwrap();
2467            let document = PdfDocument::new(reader);
2468
2469            // Test resolving a reference
2470            let ref_obj = PdfObject::Reference(2, 0);
2471            let resolved = document.resolve(&ref_obj).unwrap();
2472
2473            // Should resolve to the pages object
2474            if let PdfObject::Dictionary(dict) = resolved {
2475                if let Some(PdfObject::Name(name)) = dict.get("Type") {
2476                    assert_eq!(name.0, "Pages");
2477                }
2478            }
2479        }
2480
2481        #[test]
2482        fn test_resolve_various_object_types() {
2483            let pdf_data = create_minimal_pdf();
2484            let cursor = Cursor::new(pdf_data);
2485            let reader = PdfReader::new(cursor).unwrap();
2486            let document = PdfDocument::new(reader);
2487
2488            // Test resolving different object types
2489            let test_objects = vec![
2490                PdfObject::Integer(42),
2491                PdfObject::Boolean(true),
2492                PdfObject::String(PdfString("test".as_bytes().to_vec())),
2493                PdfObject::Real(3.14),
2494                PdfObject::Null,
2495            ];
2496
2497            for obj in test_objects {
2498                let resolved = document.resolve(&obj).unwrap();
2499                assert_eq!(resolved, obj);
2500            }
2501        }
2502
2503        #[test]
2504        fn test_get_page_cached() {
2505            let pdf_data = create_minimal_pdf();
2506            let cursor = Cursor::new(pdf_data);
2507            let reader = PdfReader::new(cursor).unwrap();
2508            let document = PdfDocument::new(reader);
2509
2510            // Get page first time
2511            let page1 = document.get_page(0).unwrap();
2512
2513            // Get same page again
2514            let page2 = document.get_page(0).unwrap();
2515
2516            // Should be identical
2517            assert_eq!(page1.media_box, page2.media_box);
2518            assert_eq!(page1.rotation, page2.rotation);
2519            assert_eq!(page1.obj_ref, page2.obj_ref);
2520        }
2521
2522        #[test]
2523        fn test_metadata_caching() {
2524            let pdf_data = create_pdf_with_metadata();
2525            let cursor = Cursor::new(pdf_data);
2526            let reader = PdfReader::new(cursor).unwrap();
2527            let document = PdfDocument::new(reader);
2528
2529            // Get metadata first time
2530            let meta1 = document.metadata().unwrap();
2531
2532            // Get metadata again
2533            let meta2 = document.metadata().unwrap();
2534
2535            // Should be identical
2536            assert_eq!(meta1.title, meta2.title);
2537            assert_eq!(meta1.author, meta2.author);
2538            assert_eq!(meta1.subject, meta2.subject);
2539            assert_eq!(meta1.version, meta2.version);
2540        }
2541
2542        #[test]
2543        fn test_page_tree_initialization() {
2544            let pdf_data = create_minimal_pdf();
2545            let cursor = Cursor::new(pdf_data);
2546            let reader = PdfReader::new(cursor).unwrap();
2547            let document = PdfDocument::new(reader);
2548
2549            // Initially page tree should be None
2550            assert!(document.page_tree.borrow().is_none());
2551
2552            // After getting page count, page tree should be initialized
2553            let _count = document.page_count().unwrap();
2554            // Note: page_tree is private, so we can't directly check it
2555            // But we can verify it works by getting a page
2556            let _page = document.get_page(0).unwrap();
2557        }
2558
2559        #[test]
2560        fn test_get_page_resources() {
2561            let pdf_data = create_minimal_pdf();
2562            let cursor = Cursor::new(pdf_data);
2563            let reader = PdfReader::new(cursor).unwrap();
2564            let document = PdfDocument::new(reader);
2565
2566            let page = document.get_page(0).unwrap();
2567            let resources = document.get_page_resources(&page).unwrap();
2568
2569            // The minimal PDF has empty resources
2570            assert!(resources.is_some());
2571        }
2572
2573        #[test]
2574        fn test_get_page_content_streams_empty() {
2575            let pdf_data = create_minimal_pdf();
2576            let cursor = Cursor::new(pdf_data);
2577            let reader = PdfReader::new(cursor).unwrap();
2578            let document = PdfDocument::new(reader);
2579
2580            let page = document.get_page(0).unwrap();
2581            let streams = document.get_page_content_streams(&page).unwrap();
2582
2583            // Minimal PDF has no content streams
2584            assert!(streams.is_empty());
2585        }
2586
2587        #[test]
2588        fn test_extract_text_from_page() {
2589            let pdf_data = create_minimal_pdf();
2590            let cursor = Cursor::new(pdf_data);
2591            let reader = PdfReader::new(cursor).unwrap();
2592            let document = PdfDocument::new(reader);
2593
2594            let result = document.extract_text_from_page(0);
2595            // Should succeed even with empty page
2596            assert!(result.is_ok());
2597        }
2598
2599        #[test]
2600        fn test_extract_text_from_page_out_of_bounds() {
2601            let pdf_data = create_minimal_pdf();
2602            let cursor = Cursor::new(pdf_data);
2603            let reader = PdfReader::new(cursor).unwrap();
2604            let document = PdfDocument::new(reader);
2605
2606            let result = document.extract_text_from_page(999);
2607            // With fallback lookup, this might succeed or fail gracefully
2608            if result.is_err() {
2609                assert!(result.unwrap_err().to_string().contains("Page"));
2610            } else {
2611                // If succeeds, should return empty or valid text
2612                let _text = result.unwrap();
2613            }
2614        }
2615
2616        #[test]
2617        fn test_extract_text_with_options() {
2618            let pdf_data = create_minimal_pdf();
2619            let cursor = Cursor::new(pdf_data);
2620            let reader = PdfReader::new(cursor).unwrap();
2621            let document = PdfDocument::new(reader);
2622
2623            let options = crate::text::ExtractionOptions {
2624                preserve_layout: true,
2625                space_threshold: 0.5,
2626                newline_threshold: 15.0,
2627                ..Default::default()
2628            };
2629
2630            let result = document.extract_text_with_options(options);
2631            assert!(result.is_ok());
2632        }
2633
2634        #[test]
2635        fn test_version_different_pdf_versions() {
2636            // Test with different PDF versions
2637            let versions = vec!["1.3", "1.4", "1.5", "1.6", "1.7"];
2638
2639            for version in versions {
2640                let mut pdf_data = Vec::new();
2641
2642                // PDF header
2643                pdf_data.extend_from_slice(format!("%PDF-{version}\n").as_bytes());
2644
2645                // Track positions for xref
2646                let obj1_pos = pdf_data.len();
2647
2648                // Catalog object
2649                pdf_data.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
2650
2651                let obj2_pos = pdf_data.len();
2652
2653                // Pages object
2654                pdf_data
2655                    .extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
2656
2657                // Cross-reference table
2658                let xref_pos = pdf_data.len();
2659                pdf_data.extend_from_slice(b"xref\n");
2660                pdf_data.extend_from_slice(b"0 3\n");
2661                pdf_data.extend_from_slice(b"0000000000 65535 f \n");
2662                pdf_data.extend_from_slice(format!("{obj1_pos:010} 00000 n \n").as_bytes());
2663                pdf_data.extend_from_slice(format!("{obj2_pos:010} 00000 n \n").as_bytes());
2664
2665                // Trailer
2666                pdf_data.extend_from_slice(b"trailer\n");
2667                pdf_data.extend_from_slice(b"<< /Size 3 /Root 1 0 R >>\n");
2668                pdf_data.extend_from_slice(b"startxref\n");
2669                pdf_data.extend_from_slice(format!("{xref_pos}\n").as_bytes());
2670                pdf_data.extend_from_slice(b"%%EOF\n");
2671
2672                let cursor = Cursor::new(pdf_data);
2673                let reader = PdfReader::new(cursor).unwrap();
2674                let document = PdfDocument::new(reader);
2675
2676                let pdf_version = document.version().unwrap();
2677                assert_eq!(pdf_version, version);
2678            }
2679        }
2680
2681        #[test]
2682        fn test_page_count_zero() {
2683            let pdf_data = create_pdf_with_metadata(); // Has 0 pages
2684            let cursor = Cursor::new(pdf_data);
2685            let reader = PdfReader::new(cursor).unwrap();
2686            let document = PdfDocument::new(reader);
2687
2688            let count = document.page_count().unwrap();
2689            assert_eq!(count, 0);
2690        }
2691
2692        #[test]
2693        fn test_multiple_object_access() {
2694            let pdf_data = create_minimal_pdf();
2695            let cursor = Cursor::new(pdf_data);
2696            let reader = PdfReader::new(cursor).unwrap();
2697            let document = PdfDocument::new(reader);
2698
2699            // Access multiple objects
2700            let catalog = document.get_object(1, 0).unwrap();
2701            let pages = document.get_object(2, 0).unwrap();
2702            let page = document.get_object(3, 0).unwrap();
2703
2704            // Verify they're all different objects
2705            assert_ne!(catalog, pages);
2706            assert_ne!(pages, page);
2707            assert_ne!(catalog, page);
2708        }
2709
2710        #[test]
2711        fn test_error_handling_invalid_object_reference() {
2712            let pdf_data = create_minimal_pdf();
2713            let cursor = Cursor::new(pdf_data);
2714            let reader = PdfReader::new(cursor).unwrap();
2715            let document = PdfDocument::new(reader);
2716
2717            // Try to resolve an invalid reference
2718            let invalid_ref = PdfObject::Reference(999, 0);
2719            let result = document.resolve(&invalid_ref);
2720            assert!(result.is_err());
2721        }
2722
2723        #[test]
2724        fn test_concurrent_metadata_access() {
2725            let pdf_data = create_pdf_with_metadata();
2726            let cursor = Cursor::new(pdf_data);
2727            let reader = PdfReader::new(cursor).unwrap();
2728            let document = PdfDocument::new(reader);
2729
2730            // Access metadata and other properties concurrently
2731            let metadata = document.metadata().unwrap();
2732            let version = document.version().unwrap();
2733            let count = document.page_count().unwrap();
2734
2735            assert_eq!(metadata.title, Some("Test Document".to_string()));
2736            assert_eq!(version, "1.5");
2737            assert_eq!(count, 0);
2738        }
2739
2740        #[test]
2741        fn test_page_properties_comprehensive() {
2742            let pdf_data = create_minimal_pdf();
2743            let cursor = Cursor::new(pdf_data);
2744            let reader = PdfReader::new(cursor).unwrap();
2745            let document = PdfDocument::new(reader);
2746
2747            let page = document.get_page(0).unwrap();
2748
2749            // Test all page properties
2750            assert_eq!(page.media_box, [0.0, 0.0, 612.0, 792.0]);
2751            assert_eq!(page.crop_box, None);
2752            assert_eq!(page.rotation, 0);
2753            assert_eq!(page.obj_ref, (3, 0));
2754
2755            // Test width/height calculation
2756            assert_eq!(page.width(), 612.0);
2757            assert_eq!(page.height(), 792.0);
2758        }
2759
2760        #[test]
2761        fn test_memory_usage_efficiency() {
2762            let pdf_data = create_minimal_pdf();
2763            let cursor = Cursor::new(pdf_data);
2764            let reader = PdfReader::new(cursor).unwrap();
2765            let document = PdfDocument::new(reader);
2766
2767            // Access same page multiple times
2768            for _ in 0..10 {
2769                let _page = document.get_page(0).unwrap();
2770            }
2771
2772            // Should only have one copy in cache
2773            let page_count = document.page_count().unwrap();
2774            assert_eq!(page_count, 1);
2775        }
2776
2777        #[test]
2778        fn test_reader_borrow_safety() {
2779            let pdf_data = create_minimal_pdf();
2780            let cursor = Cursor::new(pdf_data);
2781            let reader = PdfReader::new(cursor).unwrap();
2782            let document = PdfDocument::new(reader);
2783
2784            // Multiple concurrent borrows should work
2785            let version = document.version().unwrap();
2786            let count = document.page_count().unwrap();
2787            let metadata = document.metadata().unwrap();
2788
2789            assert_eq!(version, "1.4");
2790            assert_eq!(count, 1);
2791            assert!(metadata.title.is_none());
2792        }
2793
2794        #[test]
2795        fn test_cache_consistency() {
2796            let pdf_data = create_minimal_pdf();
2797            let cursor = Cursor::new(pdf_data);
2798            let reader = PdfReader::new(cursor).unwrap();
2799            let document = PdfDocument::new(reader);
2800
2801            // Get object and verify caching
2802            let obj1 = document.get_object(1, 0).unwrap();
2803            let cached = document.resources.get_cached((1, 0)).unwrap();
2804
2805            assert_eq!(obj1, cached);
2806
2807            // Clear cache and get object again
2808            document.resources.clear_cache();
2809            let obj2 = document.get_object(1, 0).unwrap();
2810
2811            // Should be same content but loaded fresh
2812            assert_eq!(obj1, obj2);
2813        }
2814    }
2815
2816    #[test]
2817    fn test_resource_manager_new() {
2818        let resources = ResourceManager::new();
2819        assert!(resources.get_cached((1, 0)).is_none());
2820    }
2821
2822    #[test]
2823    fn test_resource_manager_cache_and_get() {
2824        let resources = ResourceManager::new();
2825
2826        // Cache an object
2827        let obj = PdfObject::Integer(42);
2828        resources.cache_object((10, 0), obj.clone());
2829
2830        // Should be retrievable
2831        let cached = resources.get_cached((10, 0));
2832        assert!(cached.is_some());
2833        assert_eq!(cached.unwrap(), obj);
2834
2835        // Non-existent object
2836        assert!(resources.get_cached((11, 0)).is_none());
2837    }
2838
2839    #[test]
2840    fn test_resource_manager_clear_cache() {
2841        let resources = ResourceManager::new();
2842
2843        // Cache multiple objects
2844        resources.cache_object((1, 0), PdfObject::Integer(1));
2845        resources.cache_object((2, 0), PdfObject::Integer(2));
2846        resources.cache_object((3, 0), PdfObject::Integer(3));
2847
2848        // Verify they're cached
2849        assert!(resources.get_cached((1, 0)).is_some());
2850        assert!(resources.get_cached((2, 0)).is_some());
2851        assert!(resources.get_cached((3, 0)).is_some());
2852
2853        // Clear cache
2854        resources.clear_cache();
2855
2856        // Should all be gone
2857        assert!(resources.get_cached((1, 0)).is_none());
2858        assert!(resources.get_cached((2, 0)).is_none());
2859        assert!(resources.get_cached((3, 0)).is_none());
2860    }
2861
2862    #[test]
2863    fn test_resource_manager_overwrite_cached() {
2864        let resources = ResourceManager::new();
2865
2866        // Cache initial object
2867        resources.cache_object((1, 0), PdfObject::Integer(42));
2868        assert_eq!(
2869            resources.get_cached((1, 0)).unwrap(),
2870            PdfObject::Integer(42)
2871        );
2872
2873        // Overwrite with new object
2874        resources.cache_object((1, 0), PdfObject::Integer(100));
2875        assert_eq!(
2876            resources.get_cached((1, 0)).unwrap(),
2877            PdfObject::Integer(100)
2878        );
2879    }
2880
2881    #[test]
2882    fn test_resource_manager_multiple_generations() {
2883        let resources = ResourceManager::new();
2884
2885        // Cache objects with different generations
2886        resources.cache_object((1, 0), PdfObject::Integer(10));
2887        resources.cache_object((1, 1), PdfObject::Integer(11));
2888        resources.cache_object((1, 2), PdfObject::Integer(12));
2889
2890        // Each should be distinct
2891        assert_eq!(
2892            resources.get_cached((1, 0)).unwrap(),
2893            PdfObject::Integer(10)
2894        );
2895        assert_eq!(
2896            resources.get_cached((1, 1)).unwrap(),
2897            PdfObject::Integer(11)
2898        );
2899        assert_eq!(
2900            resources.get_cached((1, 2)).unwrap(),
2901            PdfObject::Integer(12)
2902        );
2903    }
2904
2905    #[test]
2906    fn test_resource_manager_cache_complex_objects() {
2907        let resources = ResourceManager::new();
2908
2909        // Cache different object types
2910        resources.cache_object((1, 0), PdfObject::Boolean(true));
2911        resources.cache_object((2, 0), PdfObject::Real(3.14159));
2912        resources.cache_object(
2913            (3, 0),
2914            PdfObject::String(PdfString::new(b"Hello PDF".to_vec())),
2915        );
2916        resources.cache_object((4, 0), PdfObject::Name(PdfName::new("Type".to_string())));
2917
2918        let mut dict = PdfDictionary::new();
2919        dict.insert(
2920            "Key".to_string(),
2921            PdfObject::String(PdfString::new(b"Value".to_vec())),
2922        );
2923        resources.cache_object((5, 0), PdfObject::Dictionary(dict));
2924
2925        let array = vec![PdfObject::Integer(1), PdfObject::Integer(2)];
2926        resources.cache_object((6, 0), PdfObject::Array(PdfArray(array)));
2927
2928        // Verify all cached correctly
2929        assert_eq!(
2930            resources.get_cached((1, 0)).unwrap(),
2931            PdfObject::Boolean(true)
2932        );
2933        assert_eq!(
2934            resources.get_cached((2, 0)).unwrap(),
2935            PdfObject::Real(3.14159)
2936        );
2937        assert_eq!(
2938            resources.get_cached((3, 0)).unwrap(),
2939            PdfObject::String(PdfString::new(b"Hello PDF".to_vec()))
2940        );
2941        assert_eq!(
2942            resources.get_cached((4, 0)).unwrap(),
2943            PdfObject::Name(PdfName::new("Type".to_string()))
2944        );
2945        assert!(matches!(
2946            resources.get_cached((5, 0)).unwrap(),
2947            PdfObject::Dictionary(_)
2948        ));
2949        assert!(matches!(
2950            resources.get_cached((6, 0)).unwrap(),
2951            PdfObject::Array(_)
2952        ));
2953    }
2954
2955    // Tests for PdfDocument removed due to API incompatibilities
2956    // The methods tested don't exist in the current implementation
2957
2958    /*
2959        #[test]
2960        fn test_pdf_document_new_initialization() {
2961            // Create a minimal PDF for testing
2962            let data = b"%PDF-1.4
2963    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2964    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2965    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2966    xref
2967    0 4
2968    0000000000 65535 f
2969    0000000009 00000 n
2970    0000000052 00000 n
2971    0000000101 00000 n
2972    trailer<</Size 4/Root 1 0 R>>
2973    startxref
2974    164
2975    %%EOF";
2976            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
2977            let document = PdfDocument::new(reader);
2978
2979            // Document should be created successfully
2980            // Initially no page tree loaded
2981            assert!(document.page_tree.borrow().is_none());
2982            assert!(document.metadata_cache.borrow().is_none());
2983        }
2984
2985        #[test]
2986        fn test_pdf_document_version() {
2987            // Create a minimal PDF for testing
2988            let data = b"%PDF-1.4
2989    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2990    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
2991    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
2992    xref
2993    0 4
2994    0000000000 65535 f
2995    0000000009 00000 n
2996    0000000052 00000 n
2997    0000000101 00000 n
2998    trailer<</Size 4/Root 1 0 R>>
2999    startxref
3000    164
3001    %%EOF";
3002            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3003            let document = PdfDocument::new(reader);
3004
3005            let version = document.version().unwrap();
3006            assert!(!version.is_empty());
3007            // Most PDFs are version 1.4 to 1.7
3008            assert!(version.starts_with("1.") || version.starts_with("2."));
3009        }
3010
3011        #[test]
3012        fn test_pdf_document_page_count() {
3013            // Create a minimal PDF for testing
3014            let data = b"%PDF-1.4
3015    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3016    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3017    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3018    xref
3019    0 4
3020    0000000000 65535 f
3021    0000000009 00000 n
3022    0000000052 00000 n
3023    0000000101 00000 n
3024    trailer<</Size 4/Root 1 0 R>>
3025    startxref
3026    164
3027    %%EOF";
3028            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3029            let document = PdfDocument::new(reader);
3030
3031            let count = document.page_count().unwrap();
3032            assert!(count > 0);
3033        }
3034
3035        #[test]
3036        fn test_pdf_document_metadata() {
3037            // Create a minimal PDF for testing
3038            let data = b"%PDF-1.4
3039    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3040    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3041    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3042    xref
3043    0 4
3044    0000000000 65535 f
3045    0000000009 00000 n
3046    0000000052 00000 n
3047    0000000101 00000 n
3048    trailer<</Size 4/Root 1 0 R>>
3049    startxref
3050    164
3051    %%EOF";
3052            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3053            let document = PdfDocument::new(reader);
3054
3055            let metadata = document.metadata().unwrap();
3056            // Metadata should be cached after first access
3057            assert!(document.metadata_cache.borrow().is_some());
3058
3059            // Second call should use cache
3060            let metadata2 = document.metadata().unwrap();
3061            assert_eq!(metadata.title, metadata2.title);
3062        }
3063
3064        #[test]
3065        fn test_pdf_document_get_page() {
3066            // Create a minimal PDF for testing
3067            let data = b"%PDF-1.4
3068    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3069    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3070    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3071    xref
3072    0 4
3073    0000000000 65535 f
3074    0000000009 00000 n
3075    0000000052 00000 n
3076    0000000101 00000 n
3077    trailer<</Size 4/Root 1 0 R>>
3078    startxref
3079    164
3080    %%EOF";
3081            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3082            let document = PdfDocument::new(reader);
3083
3084            // Get first page
3085            let page = document.get_page(0).unwrap();
3086            assert!(page.width() > 0.0);
3087            assert!(page.height() > 0.0);
3088
3089            // Page tree should be loaded now
3090            assert!(document.page_tree.borrow().is_some());
3091        }
3092
3093        #[test]
3094        fn test_pdf_document_get_page_out_of_bounds() {
3095            // Create a minimal PDF for testing
3096            let data = b"%PDF-1.4
3097    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3098    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3099    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3100    xref
3101    0 4
3102    0000000000 65535 f
3103    0000000009 00000 n
3104    0000000052 00000 n
3105    0000000101 00000 n
3106    trailer<</Size 4/Root 1 0 R>>
3107    startxref
3108    164
3109    %%EOF";
3110            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3111            let document = PdfDocument::new(reader);
3112
3113            let page_count = document.page_count().unwrap();
3114
3115            // Try to get page beyond count
3116            let result = document.get_page(page_count + 10);
3117            assert!(result.is_err());
3118        }
3119
3120
3121        #[test]
3122        fn test_pdf_document_get_object() {
3123            // Create a minimal PDF for testing
3124            let data = b"%PDF-1.4
3125    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3126    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3127    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3128    xref
3129    0 4
3130    0000000000 65535 f
3131    0000000009 00000 n
3132    0000000052 00000 n
3133    0000000101 00000 n
3134    trailer<</Size 4/Root 1 0 R>>
3135    startxref
3136    164
3137    %%EOF";
3138            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3139            let document = PdfDocument::new(reader);
3140
3141            // Get an object (catalog is usually object 1 0)
3142            let obj = document.get_object(1, 0);
3143            assert!(obj.is_ok());
3144
3145            // Object should be cached
3146            assert!(document.resources.get_cached((1, 0)).is_some());
3147        }
3148
3149
3150
3151        #[test]
3152        fn test_pdf_document_extract_text_from_page() {
3153            // Create a minimal PDF for testing
3154            let data = b"%PDF-1.4
3155    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3156    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3157    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3158    xref
3159    0 4
3160    0000000000 65535 f
3161    0000000009 00000 n
3162    0000000052 00000 n
3163    0000000101 00000 n
3164    trailer<</Size 4/Root 1 0 R>>
3165    startxref
3166    164
3167    %%EOF";
3168            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3169            let document = PdfDocument::new(reader);
3170
3171            // Try to extract text from first page
3172            let result = document.extract_text_from_page(0);
3173            // Even if no text, should not error
3174            assert!(result.is_ok());
3175        }
3176
3177        #[test]
3178        fn test_pdf_document_extract_all_text() {
3179            // Create a minimal PDF for testing
3180            let data = b"%PDF-1.4
3181    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3182    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3183    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3184    xref
3185    0 4
3186    0000000000 65535 f
3187    0000000009 00000 n
3188    0000000052 00000 n
3189    0000000101 00000 n
3190    trailer<</Size 4/Root 1 0 R>>
3191    startxref
3192    164
3193    %%EOF";
3194            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3195            let document = PdfDocument::new(reader);
3196
3197            let extracted = document.extract_text().unwrap();
3198            let page_count = document.page_count().unwrap();
3199
3200            // Should have text for each page
3201            assert_eq!(extracted.len(), page_count);
3202        }
3203
3204
3205        #[test]
3206        fn test_pdf_document_ensure_page_tree() {
3207            // Create a minimal PDF for testing
3208            let data = b"%PDF-1.4
3209    1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
3210    2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3211    3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
3212    xref
3213    0 4
3214    0000000000 65535 f
3215    0000000009 00000 n
3216    0000000052 00000 n
3217    0000000101 00000 n
3218    trailer<</Size 4/Root 1 0 R>>
3219    startxref
3220    164
3221    %%EOF";
3222            let reader = PdfReader::new(std::io::Cursor::new(data.to_vec())).unwrap();
3223            let document = PdfDocument::new(reader);
3224
3225            // Initially no page tree
3226            assert!(document.page_tree.borrow().is_none());
3227
3228            // After ensuring, should be loaded
3229            document.ensure_page_tree().unwrap();
3230            assert!(document.page_tree.borrow().is_some());
3231
3232            // Second call should not error
3233            document.ensure_page_tree().unwrap();
3234        }
3235
3236        #[test]
3237        fn test_resource_manager_concurrent_access() {
3238            let resources = ResourceManager::new();
3239
3240            // Simulate concurrent-like access pattern
3241            resources.cache_object((1, 0), PdfObject::Integer(1));
3242            let obj1 = resources.get_cached((1, 0));
3243
3244            resources.cache_object((2, 0), PdfObject::Integer(2));
3245            let obj2 = resources.get_cached((2, 0));
3246
3247            // Both should be accessible
3248            assert_eq!(obj1.unwrap(), PdfObject::Integer(1));
3249            assert_eq!(obj2.unwrap(), PdfObject::Integer(2));
3250        }
3251
3252        #[test]
3253        fn test_resource_manager_large_cache() {
3254            let resources = ResourceManager::new();
3255
3256            // Cache many objects
3257            for i in 0..1000 {
3258                resources.cache_object((i, 0), PdfObject::Integer(i as i64));
3259            }
3260
3261            // Verify random access
3262            assert_eq!(resources.get_cached((500, 0)).unwrap(), PdfObject::Integer(500));
3263            assert_eq!(resources.get_cached((999, 0)).unwrap(), PdfObject::Integer(999));
3264            assert_eq!(resources.get_cached((0, 0)).unwrap(), PdfObject::Integer(0));
3265
3266            // Clear should remove all
3267            resources.clear_cache();
3268            assert!(resources.get_cached((500, 0)).is_none());
3269        }
3270        */
3271}