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