oxidize_pdf/parser/
document.rs

1//! PDF Document wrapper - High-level interface for PDF parsing and manipulation
2//!
3//! This module provides a robust, high-level interface for working with PDF documents.
4//! It solves Rust's borrow checker challenges through careful use of interior mutability
5//! (RefCell) and separation of concerns between parsing, caching, and page access.
6//!
7//! # Architecture
8//!
9//! The module uses a layered architecture:
10//! - **PdfDocument**: Main entry point with RefCell-based state management
11//! - **ResourceManager**: Centralized object caching with interior mutability
12//! - **PdfReader**: Low-level file access (wrapped in RefCell)
13//! - **PageTree**: Lazy-loaded page navigation
14//!
15//! # Key Features
16//!
17//! - **Automatic caching**: Objects are cached after first access
18//! - **Resource management**: Shared resources are handled efficiently
19//! - **Page navigation**: Fast access to any page in the document
20//! - **Reference resolution**: Automatic resolution of indirect references
21//! - **Text extraction**: Built-in support for extracting text from pages
22//!
23//! # Example
24//!
25//! ```rust,no_run
26//! use oxidize_pdf_core::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
52use super::objects::{PdfDictionary, PdfObject};
53use super::page_tree::{PageTree, ParsedPage};
54use super::reader::PdfReader;
55use super::{ParseError, ParseResult};
56use std::cell::RefCell;
57use std::collections::HashMap;
58use std::io::{Read, Seek};
59use std::rc::Rc;
60
61/// Resource manager for efficient PDF object caching.
62///
63/// The ResourceManager provides centralized caching of PDF objects to avoid
64/// repeated parsing and to share resources between different parts of the document.
65/// It uses RefCell for interior mutability, allowing multiple immutable references
66/// to the document while still being able to update the cache.
67///
68/// # Caching Strategy
69///
70/// - Objects are cached on first access
71/// - Cache persists for the lifetime of the document
72/// - Manual cache clearing is supported for memory management
73///
74/// # Example
75///
76/// ```rust,no_run
77/// use oxidize_pdf_core::parser::document::ResourceManager;
78///
79/// let resources = ResourceManager::new();
80/// 
81/// // Objects are cached automatically when accessed through PdfDocument
82/// // Manual cache management:
83/// resources.clear_cache(); // Free memory when needed
84/// ```
85pub struct ResourceManager {
86    /// Cached objects indexed by (object_number, generation_number)
87    object_cache: RefCell<HashMap<(u32, u16), PdfObject>>,
88}
89
90impl Default for ResourceManager {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl ResourceManager {
97    /// Create a new resource manager
98    pub fn new() -> Self {
99        Self {
100            object_cache: RefCell::new(HashMap::new()),
101        }
102    }
103
104    /// Get an object from cache if available.
105    ///
106    /// # Arguments
107    ///
108    /// * `obj_ref` - Object reference (object_number, generation_number)
109    ///
110    /// # Returns
111    ///
112    /// Cloned object if cached, None otherwise.
113    ///
114    /// # Example
115    ///
116    /// ```rust,no_run
117    /// # use oxidize_pdf_core::parser::document::ResourceManager;
118    /// # let resources = ResourceManager::new();
119    /// if let Some(obj) = resources.get_cached((10, 0)) {
120    ///     println!("Object 10 0 R found in cache");
121    /// }
122    /// ```
123    pub fn get_cached(&self, obj_ref: (u32, u16)) -> Option<PdfObject> {
124        self.object_cache.borrow().get(&obj_ref).cloned()
125    }
126
127    /// Cache an object for future access.
128    ///
129    /// # Arguments
130    ///
131    /// * `obj_ref` - Object reference (object_number, generation_number)
132    /// * `obj` - The PDF object to cache
133    ///
134    /// # Example
135    ///
136    /// ```rust,no_run
137    /// # use oxidize_pdf_core::parser::document::ResourceManager;
138    /// # use oxidize_pdf_core::parser::objects::PdfObject;
139    /// # let resources = ResourceManager::new();
140    /// resources.cache_object((10, 0), PdfObject::Integer(42));
141    /// ```
142    pub fn cache_object(&self, obj_ref: (u32, u16), obj: PdfObject) {
143        self.object_cache.borrow_mut().insert(obj_ref, obj);
144    }
145
146    /// Clear all cached objects to free memory.
147    ///
148    /// Use this when processing large documents to manage memory usage.
149    ///
150    /// # Example
151    ///
152    /// ```rust,no_run
153    /// # use oxidize_pdf_core::parser::document::ResourceManager;
154    /// # let resources = ResourceManager::new();
155    /// // After processing many pages
156    /// resources.clear_cache();
157    /// println!("Cache cleared to free memory");
158    /// ```
159    pub fn clear_cache(&self) {
160        self.object_cache.borrow_mut().clear();
161    }
162}
163
164/// High-level PDF document interface for parsing and manipulation.
165///
166/// `PdfDocument` provides a clean, safe API for working with PDF files.
167/// It handles the complexity of PDF structure, object references, and resource
168/// management behind a simple interface.
169///
170/// # Type Parameter
171///
172/// * `R` - The reader type (must implement Read + Seek)
173///
174/// # Architecture Benefits
175///
176/// - **RefCell Usage**: Allows multiple parts of the API to access the document
177/// - **Lazy Loading**: Pages and resources are loaded on demand
178/// - **Automatic Caching**: Frequently accessed objects are cached
179/// - **Safe API**: Borrow checker issues are handled internally
180///
181/// # Example
182///
183/// ```rust,no_run
184/// use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
185/// use std::fs::File;
186///
187/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
188/// // From a file
189/// let reader = PdfReader::open("document.pdf")?;
190/// let document = PdfDocument::new(reader);
191///
192/// // From any Read + Seek source
193/// let file = File::open("document.pdf")?;
194/// let reader = PdfReader::new(file)?;
195/// let document = PdfDocument::new(reader);
196///
197/// // Use the document
198/// let page_count = document.page_count()?;
199/// for i in 0..page_count {
200///     let page = document.get_page(i)?;
201///     // Process page...
202/// }
203/// # Ok(())
204/// # }
205/// ```
206pub struct PdfDocument<R: Read + Seek> {
207    /// The underlying PDF reader wrapped for interior mutability
208    reader: RefCell<PdfReader<R>>,
209    /// Page tree navigator (lazily initialized)
210    page_tree: RefCell<Option<PageTree>>,
211    /// Shared resource manager for object caching
212    resources: Rc<ResourceManager>,
213    /// Cached document metadata to avoid repeated parsing
214    metadata_cache: RefCell<Option<super::reader::DocumentMetadata>>,
215}
216
217impl<R: Read + Seek> PdfDocument<R> {
218    /// Create a new PDF document from a reader
219    pub fn new(reader: PdfReader<R>) -> Self {
220        Self {
221            reader: RefCell::new(reader),
222            page_tree: RefCell::new(None),
223            resources: Rc::new(ResourceManager::new()),
224            metadata_cache: RefCell::new(None),
225        }
226    }
227
228    /// Get the PDF version of the document.
229    ///
230    /// # Returns
231    ///
232    /// PDF version string (e.g., "1.4", "1.7", "2.0")
233    ///
234    /// # Example
235    ///
236    /// ```rust,no_run
237    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
238    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
239    /// # let reader = PdfReader::open("document.pdf")?;
240    /// # let document = PdfDocument::new(reader);
241    /// let version = document.version()?;
242    /// println!("PDF version: {}", version);
243    /// # Ok(())
244    /// # }
245    /// ```
246    pub fn version(&self) -> ParseResult<String> {
247        Ok(self.reader.borrow().version().to_string())
248    }
249
250    /// Get the total number of pages in the document.
251    ///
252    /// # Returns
253    ///
254    /// The page count as an unsigned 32-bit integer.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the page tree is malformed or missing.
259    ///
260    /// # Example
261    ///
262    /// ```rust,no_run
263    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
264    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
265    /// # let reader = PdfReader::open("document.pdf")?;
266    /// # let document = PdfDocument::new(reader);
267    /// let count = document.page_count()?;
268    /// println!("Document has {} pages", count);
269    ///
270    /// // Iterate through all pages
271    /// for i in 0..count {
272    ///     let page = document.get_page(i)?;
273    ///     // Process page...
274    /// }
275    /// # Ok(())
276    /// # }
277    /// ```
278    pub fn page_count(&self) -> ParseResult<u32> {
279        self.reader.borrow_mut().page_count()
280    }
281
282    /// Get document metadata including title, author, creation date, etc.
283    ///
284    /// Metadata is cached after first access for performance.
285    ///
286    /// # Returns
287    ///
288    /// A `DocumentMetadata` struct containing all available metadata fields.
289    ///
290    /// # Example
291    ///
292    /// ```rust,no_run
293    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
294    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
295    /// # let reader = PdfReader::open("document.pdf")?;
296    /// # let document = PdfDocument::new(reader);
297    /// let metadata = document.metadata()?;
298    ///
299    /// if let Some(title) = &metadata.title {
300    ///     println!("Title: {}", title);
301    /// }
302    /// if let Some(author) = &metadata.author {
303    ///     println!("Author: {}", author);
304    /// }
305    /// if let Some(creation_date) = &metadata.creation_date {
306    ///     println!("Created: {}", creation_date);
307    /// }
308    /// println!("PDF Version: {}", metadata.version);
309    /// # Ok(())
310    /// # }
311    /// ```
312    pub fn metadata(&self) -> ParseResult<super::reader::DocumentMetadata> {
313        // Check cache first
314        if let Some(metadata) = self.metadata_cache.borrow().as_ref() {
315            return Ok(metadata.clone());
316        }
317
318        // Load metadata
319        let metadata = self.reader.borrow_mut().metadata()?;
320        self.metadata_cache.borrow_mut().replace(metadata.clone());
321        Ok(metadata)
322    }
323
324    /// Initialize the page tree if not already done
325    fn ensure_page_tree(&self) -> ParseResult<()> {
326        if self.page_tree.borrow().is_none() {
327            let page_count = self.page_count()?;
328            let pages_dict = self.load_pages_dict()?;
329            let page_tree = PageTree::new_with_pages_dict(page_count, pages_dict);
330            self.page_tree.borrow_mut().replace(page_tree);
331        }
332        Ok(())
333    }
334
335    /// Load the pages dictionary
336    fn load_pages_dict(&self) -> ParseResult<PdfDictionary> {
337        let mut reader = self.reader.borrow_mut();
338        let pages = reader.pages()?;
339        Ok(pages.clone())
340    }
341
342    /// Get a page by index (0-based).
343    ///
344    /// Pages are cached after first access. This method handles page tree
345    /// traversal and property inheritance automatically.
346    ///
347    /// # Arguments
348    ///
349    /// * `index` - Zero-based page index (0 to page_count-1)
350    ///
351    /// # Returns
352    ///
353    /// A complete `ParsedPage` with all properties and inherited resources.
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if:
358    /// - Index is out of bounds
359    /// - Page tree is malformed
360    /// - Required page properties are missing
361    ///
362    /// # Example
363    ///
364    /// ```rust,no_run
365    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
366    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
367    /// # let reader = PdfReader::open("document.pdf")?;
368    /// # let document = PdfDocument::new(reader);
369    /// // Get the first page
370    /// let page = document.get_page(0)?;
371    ///
372    /// // Access page properties
373    /// println!("Page size: {}x{} points", page.width(), page.height());
374    /// println!("Rotation: {}°", page.rotation);
375    ///
376    /// // Get content streams
377    /// let streams = page.content_streams_with_document(&document)?;
378    /// println!("Page has {} content streams", streams.len());
379    /// # Ok(())
380    /// # }
381    /// ```
382    pub fn get_page(&self, index: u32) -> ParseResult<ParsedPage> {
383        self.ensure_page_tree()?;
384
385        // First check if page is already loaded
386        if let Some(page_tree) = self.page_tree.borrow().as_ref() {
387            if let Some(page) = page_tree.get_cached_page(index) {
388                return Ok(page.clone());
389            }
390        }
391
392        // Load the page
393        let page = self.load_page_at_index(index)?;
394
395        // Cache it
396        if let Some(page_tree) = self.page_tree.borrow_mut().as_mut() {
397            page_tree.cache_page(index, page.clone());
398        }
399
400        Ok(page)
401    }
402
403    /// Load a specific page by index
404    fn load_page_at_index(&self, index: u32) -> ParseResult<ParsedPage> {
405        // Get the pages root
406        let pages_dict = self.load_pages_dict()?;
407
408        // Navigate to the specific page
409        let page_info = self.find_page_in_tree(&pages_dict, index, 0, None)?;
410
411        Ok(page_info)
412    }
413
414    /// Find a page in the page tree
415    fn find_page_in_tree(
416        &self,
417        node: &PdfDictionary,
418        target_index: u32,
419        current_index: u32,
420        inherited: Option<&PdfDictionary>,
421    ) -> ParseResult<ParsedPage> {
422        let node_type = node
423            .get_type()
424            .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
425
426        match node_type {
427            "Pages" => {
428                // This is a page tree node
429                let kids = node
430                    .get("Kids")
431                    .and_then(|obj| obj.as_array())
432                    .ok_or_else(|| ParseError::MissingKey("Kids".to_string()))?;
433
434                // Merge inherited attributes
435                let mut merged_inherited = inherited.cloned().unwrap_or_else(PdfDictionary::new);
436
437                // Inheritable attributes
438                for key in ["Resources", "MediaBox", "CropBox", "Rotate"] {
439                    if let Some(value) = node.get(key) {
440                        if !merged_inherited.contains_key(key) {
441                            merged_inherited.insert(key.to_string(), value.clone());
442                        }
443                    }
444                }
445
446                // Find which kid contains our target page
447                let mut current_idx = current_index;
448                for kid_ref in &kids.0 {
449                    let kid_ref =
450                        kid_ref
451                            .as_reference()
452                            .ok_or_else(|| ParseError::SyntaxError {
453                                position: 0,
454                                message: "Kids array must contain references".to_string(),
455                            })?;
456
457                    // Get the kid object
458                    let kid_obj = self.get_object(kid_ref.0, kid_ref.1)?;
459                    let kid_dict = kid_obj.as_dict().ok_or_else(|| ParseError::SyntaxError {
460                        position: 0,
461                        message: "Page tree node must be a dictionary".to_string(),
462                    })?;
463
464                    let kid_type = kid_dict
465                        .get_type()
466                        .ok_or_else(|| ParseError::MissingKey("Type".to_string()))?;
467
468                    let count = if kid_type == "Pages" {
469                        kid_dict
470                            .get("Count")
471                            .and_then(|obj| obj.as_integer())
472                            .ok_or_else(|| ParseError::MissingKey("Count".to_string()))?
473                            as u32
474                    } else {
475                        1
476                    };
477
478                    if target_index < current_idx + count {
479                        // Found the right subtree/page
480                        if kid_type == "Page" {
481                            // This is the page we want
482                            return self.create_parsed_page(
483                                kid_ref,
484                                kid_dict,
485                                Some(&merged_inherited),
486                            );
487                        } else {
488                            // Recurse into this subtree
489                            return self.find_page_in_tree(
490                                kid_dict,
491                                target_index,
492                                current_idx,
493                                Some(&merged_inherited),
494                            );
495                        }
496                    }
497
498                    current_idx += count;
499                }
500
501                Err(ParseError::SyntaxError {
502                    position: 0,
503                    message: "Page not found in tree".to_string(),
504                })
505            }
506            "Page" => {
507                // This is a page object
508                if target_index != current_index {
509                    return Err(ParseError::SyntaxError {
510                        position: 0,
511                        message: "Page index mismatch".to_string(),
512                    });
513                }
514
515                // We need the reference, but we don't have it here
516                // This case shouldn't happen if we're navigating properly
517                Err(ParseError::SyntaxError {
518                    position: 0,
519                    message: "Direct page object without reference".to_string(),
520                })
521            }
522            _ => Err(ParseError::SyntaxError {
523                position: 0,
524                message: format!("Invalid page tree node type: {node_type}"),
525            }),
526        }
527    }
528
529    /// Create a ParsedPage from a page dictionary
530    fn create_parsed_page(
531        &self,
532        obj_ref: (u32, u16),
533        page_dict: &PdfDictionary,
534        inherited: Option<&PdfDictionary>,
535    ) -> ParseResult<ParsedPage> {
536        // Extract page attributes
537        let media_box = self
538            .get_rectangle(page_dict, inherited, "MediaBox")?
539            .ok_or_else(|| ParseError::MissingKey("MediaBox".to_string()))?;
540
541        let crop_box = self.get_rectangle(page_dict, inherited, "CropBox")?;
542
543        let rotation = self
544            .get_integer(page_dict, inherited, "Rotate")?
545            .unwrap_or(0) as i32;
546
547        // Get inherited resources
548        let inherited_resources = if let Some(inherited) = inherited {
549            inherited
550                .get("Resources")
551                .and_then(|r| r.as_dict())
552                .cloned()
553        } else {
554            None
555        };
556
557        Ok(ParsedPage {
558            obj_ref,
559            dict: page_dict.clone(),
560            inherited_resources,
561            media_box,
562            crop_box,
563            rotation,
564        })
565    }
566
567    /// Get a rectangle value
568    fn get_rectangle(
569        &self,
570        node: &PdfDictionary,
571        inherited: Option<&PdfDictionary>,
572        key: &str,
573    ) -> ParseResult<Option<[f64; 4]>> {
574        let array = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
575
576        if let Some(array) = array.and_then(|obj| obj.as_array()) {
577            if array.len() != 4 {
578                return Err(ParseError::SyntaxError {
579                    position: 0,
580                    message: format!("{key} must have 4 elements"),
581                });
582            }
583
584            let rect = [
585                array.get(0).unwrap().as_real().unwrap_or(0.0),
586                array.get(1).unwrap().as_real().unwrap_or(0.0),
587                array.get(2).unwrap().as_real().unwrap_or(0.0),
588                array.get(3).unwrap().as_real().unwrap_or(0.0),
589            ];
590
591            Ok(Some(rect))
592        } else {
593            Ok(None)
594        }
595    }
596
597    /// Get an integer value
598    fn get_integer(
599        &self,
600        node: &PdfDictionary,
601        inherited: Option<&PdfDictionary>,
602        key: &str,
603    ) -> ParseResult<Option<i64>> {
604        let value = node.get(key).or_else(|| inherited.and_then(|i| i.get(key)));
605
606        Ok(value.and_then(|obj| obj.as_integer()))
607    }
608
609    /// Get an object by its reference numbers.
610    ///
611    /// This method first checks the cache, then loads from the file if needed.
612    /// Objects are automatically cached after loading.
613    ///
614    /// # Arguments
615    ///
616    /// * `obj_num` - Object number
617    /// * `gen_num` - Generation number
618    ///
619    /// # Returns
620    ///
621    /// The resolved PDF object.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if:
626    /// - Object doesn't exist
627    /// - Object is part of an encrypted object stream
628    /// - File is corrupted
629    ///
630    /// # Example
631    ///
632    /// ```rust,no_run
633    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
634    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
635    /// # let reader = PdfReader::open("document.pdf")?;
636    /// # let document = PdfDocument::new(reader);
637    /// // Get object 10 0 R
638    /// let obj = document.get_object(10, 0)?;
639    ///
640    /// // Check object type
641    /// match obj {
642    ///     PdfObject::Dictionary(dict) => {
643    ///         println!("Object is a dictionary with {} entries", dict.0.len());
644    ///     }
645    ///     PdfObject::Stream(stream) => {
646    ///         println!("Object is a stream");
647    ///     }
648    ///     _ => {}
649    /// }
650    /// # Ok(())
651    /// # }
652    /// ```
653    pub fn get_object(&self, obj_num: u32, gen_num: u16) -> ParseResult<PdfObject> {
654        // Check resource cache first
655        if let Some(obj) = self.resources.get_cached((obj_num, gen_num)) {
656            return Ok(obj);
657        }
658
659        // Load from reader
660        let obj = {
661            let mut reader = self.reader.borrow_mut();
662            reader.get_object(obj_num, gen_num)?.clone()
663        };
664
665        // Cache it
666        self.resources.cache_object((obj_num, gen_num), obj.clone());
667
668        Ok(obj)
669    }
670
671    /// Resolve a reference to get the actual object.
672    ///
673    /// If the input is a Reference, fetches the referenced object.
674    /// Otherwise returns a clone of the input object.
675    ///
676    /// # Arguments
677    ///
678    /// * `obj` - The object to resolve (may be a Reference or direct object)
679    ///
680    /// # Returns
681    ///
682    /// The resolved object (never a Reference).
683    ///
684    /// # Example
685    ///
686    /// ```rust,no_run
687    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
688    /// # use oxidize_pdf_core::parser::objects::PdfObject;
689    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
690    /// # let reader = PdfReader::open("document.pdf")?;
691    /// # let document = PdfDocument::new(reader);
692    /// # let page = document.get_page(0)?;
693    /// // Contents might be a reference or direct object
694    /// if let Some(contents) = page.dict.get("Contents") {
695    ///     let resolved = document.resolve(contents)?;
696    ///     match resolved {
697    ///         PdfObject::Stream(_) => println!("Single content stream"),
698    ///         PdfObject::Array(_) => println!("Multiple content streams"),
699    ///         _ => println!("Unexpected content type"),
700    ///     }
701    /// }
702    /// # Ok(())
703    /// # }
704    /// ```
705    pub fn resolve(&self, obj: &PdfObject) -> ParseResult<PdfObject> {
706        match obj {
707            PdfObject::Reference(obj_num, gen_num) => self.get_object(*obj_num, *gen_num),
708            _ => Ok(obj.clone()),
709        }
710    }
711
712    /// Get content streams for a specific page.
713    ///
714    /// This method handles both single streams and arrays of streams,
715    /// automatically decompressing them according to their filters.
716    ///
717    /// # Arguments
718    ///
719    /// * `page` - The page to get content streams from
720    ///
721    /// # Returns
722    ///
723    /// Vector of decompressed content stream data ready for parsing.
724    ///
725    /// # Example
726    ///
727    /// ```rust,no_run
728    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
729    /// # use oxidize_pdf_core::parser::content::ContentParser;
730    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
731    /// # let reader = PdfReader::open("document.pdf")?;
732    /// # let document = PdfDocument::new(reader);
733    /// let page = document.get_page(0)?;
734    /// let streams = document.get_page_content_streams(&page)?;
735    ///
736    /// // Parse content streams
737    /// for stream_data in streams {
738    ///     let operations = ContentParser::parse(&stream_data)?;
739    ///     println!("Stream has {} operations", operations.len());
740    /// }
741    /// # Ok(())
742    /// # }
743    /// ```
744    pub fn get_page_content_streams(&self, page: &ParsedPage) -> ParseResult<Vec<Vec<u8>>> {
745        let mut streams = Vec::new();
746
747        if let Some(contents) = page.dict.get("Contents") {
748            let resolved_contents = self.resolve(contents)?;
749
750            match &resolved_contents {
751                PdfObject::Stream(stream) => {
752                    streams.push(stream.decode()?);
753                }
754                PdfObject::Array(array) => {
755                    for item in &array.0 {
756                        let resolved = self.resolve(item)?;
757                        if let PdfObject::Stream(stream) = resolved {
758                            streams.push(stream.decode()?);
759                        }
760                    }
761                }
762                _ => {
763                    return Err(ParseError::SyntaxError {
764                        position: 0,
765                        message: "Contents must be a stream or array of streams".to_string(),
766                    })
767                }
768            }
769        }
770
771        Ok(streams)
772    }
773
774    /// Extract text from all pages in the document.
775    ///
776    /// Uses the default text extraction settings. For custom settings,
777    /// use `extract_text_with_options`.
778    ///
779    /// # Returns
780    ///
781    /// A vector of `ExtractedText`, one for each page in the document.
782    ///
783    /// # Example
784    ///
785    /// ```rust,no_run
786    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
787    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
788    /// # let reader = PdfReader::open("document.pdf")?;
789    /// # let document = PdfDocument::new(reader);
790    /// let extracted_pages = document.extract_text()?;
791    ///
792    /// for (page_num, page_text) in extracted_pages.iter().enumerate() {
793    ///     println!("=== Page {} ===", page_num + 1);
794    ///     println!("{}", page_text.text);
795    ///     println!();
796    /// }
797    /// # Ok(())
798    /// # }
799    /// ```
800    pub fn extract_text(&self) -> ParseResult<Vec<crate::text::ExtractedText>> {
801        let extractor = crate::text::TextExtractor::new();
802        extractor.extract_from_document(self)
803    }
804
805    /// Extract text from a specific page.
806    ///
807    /// # Arguments
808    ///
809    /// * `page_index` - Zero-based page index
810    ///
811    /// # Returns
812    ///
813    /// Extracted text with optional position information.
814    ///
815    /// # Example
816    ///
817    /// ```rust,no_run
818    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
819    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
820    /// # let reader = PdfReader::open("document.pdf")?;
821    /// # let document = PdfDocument::new(reader);
822    /// // Extract text from first page only
823    /// let page_text = document.extract_text_from_page(0)?;
824    /// println!("First page text: {}", page_text.text);
825    ///
826    /// // Access text fragments with positions (if preserved)
827    /// for fragment in &page_text.fragments {
828    ///     println!("'{}' at ({}, {})", fragment.text, fragment.x, fragment.y);
829    /// }
830    /// # Ok(())
831    /// # }
832    /// ```
833    pub fn extract_text_from_page(
834        &self,
835        page_index: u32,
836    ) -> ParseResult<crate::text::ExtractedText> {
837        let extractor = crate::text::TextExtractor::new();
838        extractor.extract_from_page(self, page_index)
839    }
840
841    /// Extract text with custom extraction options.
842    ///
843    /// Allows fine control over text extraction behavior including
844    /// layout preservation, spacing thresholds, and more.
845    ///
846    /// # Arguments
847    ///
848    /// * `options` - Text extraction configuration
849    ///
850    /// # Returns
851    ///
852    /// A vector of `ExtractedText`, one for each page.
853    ///
854    /// # Example
855    ///
856    /// ```rust,no_run
857    /// # use oxidize_pdf_core::parser::{PdfDocument, PdfReader};
858    /// # use oxidize_pdf_core::text::ExtractionOptions;
859    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
860    /// # let reader = PdfReader::open("document.pdf")?;
861    /// # let document = PdfDocument::new(reader);
862    /// // Configure extraction to preserve layout
863    /// let options = ExtractionOptions {
864    ///     preserve_layout: true,
865    ///     space_threshold: 0.3,
866    ///     newline_threshold: 10.0,
867    /// };
868    ///
869    /// let extracted_pages = document.extract_text_with_options(options)?;
870    ///
871    /// // Text fragments will include position information
872    /// for page_text in extracted_pages {
873    ///     for fragment in &page_text.fragments {
874    ///         println!("{:?}", fragment);
875    ///     }
876    /// }
877    /// # Ok(())
878    /// # }
879    /// ```
880    pub fn extract_text_with_options(
881        &self,
882        options: crate::text::ExtractionOptions,
883    ) -> ParseResult<Vec<crate::text::ExtractedText>> {
884        let extractor = crate::text::TextExtractor::with_options(options);
885        extractor.extract_from_document(self)
886    }
887}