Skip to main content

oxidize_pdf/parser/
mod.rs

1//! PDF Parser Module - Complete PDF parsing and rendering support
2//!
3//! This module provides a comprehensive, 100% native Rust implementation for parsing PDF files
4//! according to the ISO 32000-1 (PDF 1.7) and ISO 32000-2 (PDF 2.0) specifications.
5//!
6//! # Overview
7//!
8//! The parser is designed to support building PDF renderers, content extractors, and analysis tools.
9//! It provides multiple levels of API access:
10//!
11//! - **High-level**: `PdfDocument` for easy document manipulation
12//! - **Mid-level**: `ParsedPage`, content streams, and resources
13//! - **Low-level**: Direct access to PDF objects and streams
14//!
15//! # Quick Start
16//!
17//! ```rust,no_run
18//! use oxidize_pdf::parser::{PdfDocument, PdfReader};
19//! use oxidize_pdf::parser::content::ContentParser;
20//!
21//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! // Open a PDF document
23//! let reader = PdfReader::open("document.pdf")?;
24//! let document = PdfDocument::new(reader);
25//!
26//! // Get document information
27//! println!("Pages: {}", document.page_count()?);
28//! println!("Version: {}", document.version()?);
29//!
30//! // Process first page
31//! let page = document.get_page(0)?;
32//! println!("Page size: {}x{} points", page.width(), page.height());
33//!
34//! // Parse content streams
35//! let streams = page.content_streams_with_document(&document)?;
36//! for stream in streams {
37//!     let operations = ContentParser::parse(&stream)?;
38//!     println!("Operations: {}", operations.len());
39//! }
40//!
41//! // Extract text
42//! let text = document.extract_text_from_page(0)?;
43//! println!("Text: {}", text.text);
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! # Architecture
49//!
50//! ```text
51//! ┌─────────────────────────────────────────────────┐
52//! │                 PdfDocument                     │ ← High-level API
53//! │  ┌──────────┐ ┌──────────┐ ┌────────────────┐  │
54//! │  │PdfReader │ │PageTree  │ │ResourceManager │  │
55//! │  └──────────┘ └──────────┘ └────────────────┘  │
56//! └─────────────────────────────────────────────────┘
57//!            │              │              │
58//!            ↓              ↓              ↓
59//! ┌─────────────────────────────────────────────────┐
60//! │              ParsedPage                         │ ← Page API
61//! │  ┌──────────┐ ┌──────────┐ ┌────────────────┐  │
62//! │  │Properties│ │Resources │ │Content Streams │  │
63//! │  └──────────┘ └──────────┘ └────────────────┘  │
64//! └─────────────────────────────────────────────────┘
65//!            │              │              │
66//!            ↓              ↓              ↓
67//! ┌─────────────────────────────────────────────────┐
68//! │         ContentParser & PdfObject               │ ← Low-level API
69//! │  ┌──────────┐ ┌──────────┐ ┌────────────────┐  │
70//! │  │Tokenizer │ │Operators │ │Object Types    │  │
71//! │  └──────────┘ └──────────┘ └────────────────┘  │
72//! └─────────────────────────────────────────────────┘
73//! ```
74//!
75//! # Features
76//!
77//! - **Complete PDF Object Model**: All PDF object types supported
78//! - **Content Stream Parsing**: Full operator support for rendering
79//! - **Resource Management**: Fonts, images, color spaces, patterns
80//! - **Text Extraction**: With position and formatting information
81//! - **Page Navigation**: Efficient page tree traversal
82//! - **Stream Filters**: Decompression support (FlateDecode, ASCIIHex, etc.)
83//! - **Reference Resolution**: Automatic handling of indirect objects
84//!
85//! # Example: Building a Simple Renderer
86//!
87//! ```rust,no_run
88//! use oxidize_pdf::parser::{PdfDocument, PdfReader};
89//! use oxidize_pdf::parser::content::{ContentParser, ContentOperation};
90//!
91//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
92//! struct SimpleRenderer {
93//!     current_path: Vec<(f32, f32)>,
94//! }
95//!
96//! impl SimpleRenderer {
97//!     fn render_page(document: &PdfDocument<std::fs::File>, page_idx: u32) -> Result<(), Box<dyn std::error::Error>> {
98//!         let page = document.get_page(page_idx)?;
99//!         let streams = page.content_streams_with_document(&document)?;
100//!         
101//!         let mut renderer = SimpleRenderer {
102//!             current_path: Vec::new(),
103//!         };
104//!         
105//!         for stream in streams {
106//!             let operations = ContentParser::parse(&stream)?;
107//!             for op in operations {
108//!                 match op {
109//!                     ContentOperation::MoveTo(x, y) => {
110//!                         renderer.current_path.clear();
111//!                         renderer.current_path.push((x, y));
112//!                     }
113//!                     ContentOperation::LineTo(x, y) => {
114//!                         renderer.current_path.push((x, y));
115//!                     }
116//!                     ContentOperation::Stroke => {
117//!                         println!("Draw path with {} points", renderer.current_path.len());
118//!                         renderer.current_path.clear();
119//!                     }
120//!                     ContentOperation::ShowText(text) => {
121//!                         println!("Draw text: {:?}", String::from_utf8_lossy(&text));
122//!                     }
123//!                     _ => {} // Handle other operations
124//!                 }
125//!             }
126//!         }
127//!         Ok(())
128//!     }
129//! }
130//! # Ok(())
131//! # }
132//! ```
133
134pub mod content;
135pub mod document;
136pub mod encoding;
137pub mod encryption_handler;
138pub mod filter_impls;
139pub mod filters;
140pub mod header;
141pub mod lexer;
142pub mod object_stream;
143pub mod objects;
144pub mod optimized_reader;
145pub mod outline;
146pub mod page_tree;
147pub mod reader;
148pub mod stack_safe;
149pub mod stack_safe_tests;
150pub mod trailer;
151pub mod xref;
152pub mod xref_stream;
153pub mod xref_types;
154
155#[cfg(test)]
156mod stream_length_tests;
157#[cfg(test)]
158pub mod test_helpers;
159
160use crate::error::OxidizePdfError;
161
162// Re-export main types for convenient access
163pub use self::content::{ContentOperation, ContentParser, TextElement};
164pub use self::document::{PdfDocument, ResourceManager};
165pub use self::encoding::{
166    CharacterDecoder, EncodingOptions, EncodingResult, EncodingType, EnhancedDecoder,
167};
168pub use self::encryption_handler::{
169    ConsolePasswordProvider, EncryptionHandler, EncryptionInfo, InteractiveDecryption,
170    PasswordProvider, PasswordResult,
171};
172pub use self::objects::{PdfArray, PdfDictionary, PdfName, PdfObject, PdfStream, PdfString};
173pub use self::optimized_reader::OptimizedPdfReader;
174pub use self::outline::OutlineReadOptions;
175pub use self::page_tree::ParsedPage;
176pub use self::reader::{DocumentMetadata, PdfReader};
177
178/// Result type for parser operations
179pub type ParseResult<T> = Result<T, ParseError>;
180
181/// Options for parsing PDF files with different levels of strictness
182///
183/// # Example
184///
185/// ```rust
186/// use oxidize_pdf::parser::ParseOptions;
187///
188/// // Create tolerant options for handling corrupted PDFs
189/// let options = ParseOptions::tolerant();
190/// assert!(!options.strict_mode);
191/// assert!(options.recover_from_stream_errors);
192///
193/// // Create custom options
194/// let custom = ParseOptions {
195///     strict_mode: false,
196///     recover_from_stream_errors: true,
197///     ignore_corrupt_streams: false, // Still report errors but try to recover
198///     partial_content_allowed: true,
199///     max_recovery_attempts: 10,     // Try harder to recover
200///     log_recovery_details: false,   // Quiet recovery
201///     lenient_streams: true,
202///     max_recovery_bytes: 5000,
203///     collect_warnings: true,
204///     lenient_encoding: true,
205///     preferred_encoding: None,
206///     lenient_syntax: true,
207/// };
208/// ```
209#[derive(Debug, Clone)]
210pub struct ParseOptions {
211    /// Strict mode enforces PDF specification compliance (default: true)
212    pub strict_mode: bool,
213    /// Attempt to recover from stream decoding errors (default: false)
214    ///
215    /// When enabled, the parser will try multiple strategies to decode
216    /// corrupted streams, including:
217    /// - Raw deflate without zlib wrapper
218    /// - Decompression with checksum validation disabled
219    /// - Skipping corrupted header bytes
220    pub recover_from_stream_errors: bool,
221    /// Skip corrupted streams instead of failing (default: false)
222    ///
223    /// When enabled, corrupted streams will return empty data instead
224    /// of causing parsing to fail entirely.
225    pub ignore_corrupt_streams: bool,
226    /// Allow partial content when full parsing fails (default: false)
227    pub partial_content_allowed: bool,
228    /// Maximum number of recovery attempts for corrupted data (default: 3)
229    pub max_recovery_attempts: usize,
230    /// Enable detailed logging of recovery attempts (default: false)
231    ///
232    /// Note: Requires the "logging" feature to be enabled
233    pub log_recovery_details: bool,
234    /// Enable lenient parsing for malformed streams with incorrect Length fields
235    pub lenient_streams: bool,
236    /// Maximum number of bytes to search ahead when recovering from stream errors
237    pub max_recovery_bytes: usize,
238    /// Collect warnings instead of failing on recoverable errors
239    pub collect_warnings: bool,
240    /// Enable lenient character encoding (use replacement characters for invalid sequences)
241    pub lenient_encoding: bool,
242    /// Preferred character encoding for text decoding
243    pub preferred_encoding: Option<encoding::EncodingType>,
244    /// Enable automatic syntax error recovery
245    pub lenient_syntax: bool,
246}
247
248impl Default for ParseOptions {
249    fn default() -> Self {
250        Self {
251            strict_mode: true,
252            recover_from_stream_errors: false,
253            ignore_corrupt_streams: false,
254            partial_content_allowed: false,
255            max_recovery_attempts: 3,
256            log_recovery_details: false,
257            lenient_streams: false,   // Strict mode by default
258            max_recovery_bytes: 1000, // Search up to 1KB ahead
259            collect_warnings: false,  // Don't collect warnings by default
260            lenient_encoding: true,   // Enable lenient encoding by default
261            preferred_encoding: None, // Auto-detect encoding
262            lenient_syntax: false,    // Strict syntax parsing by default
263        }
264    }
265}
266
267impl ParseOptions {
268    /// Create options for strict parsing (default)
269    pub fn strict() -> Self {
270        Self {
271            strict_mode: true,
272            recover_from_stream_errors: false,
273            ignore_corrupt_streams: false,
274            partial_content_allowed: false,
275            max_recovery_attempts: 0,
276            log_recovery_details: false,
277            lenient_streams: false,
278            max_recovery_bytes: 0,
279            collect_warnings: false,
280            lenient_encoding: false,
281            preferred_encoding: None,
282            lenient_syntax: false,
283        }
284    }
285
286    /// Create options for tolerant parsing that attempts recovery
287    pub fn tolerant() -> Self {
288        Self {
289            strict_mode: false,
290            recover_from_stream_errors: true,
291            ignore_corrupt_streams: false,
292            partial_content_allowed: true,
293            max_recovery_attempts: 5,
294            log_recovery_details: true,
295            lenient_streams: true,
296            max_recovery_bytes: 5000,
297            collect_warnings: true,
298            lenient_encoding: true,
299            preferred_encoding: None,
300            lenient_syntax: true,
301        }
302    }
303
304    /// Create lenient parsing options for maximum compatibility (alias for tolerant)
305    pub fn lenient() -> Self {
306        Self::tolerant()
307    }
308
309    /// Create options that skip corrupted content
310    pub fn skip_errors() -> Self {
311        Self {
312            strict_mode: false,
313            recover_from_stream_errors: true,
314            ignore_corrupt_streams: true,
315            partial_content_allowed: true,
316            max_recovery_attempts: 1,
317            log_recovery_details: false,
318            lenient_streams: true,
319            max_recovery_bytes: 5000,
320            collect_warnings: false,
321            lenient_encoding: true,
322            preferred_encoding: None,
323            lenient_syntax: true,
324        }
325    }
326}
327
328/// Warnings that can be collected during lenient parsing
329#[derive(Debug, Clone)]
330pub enum ParseWarning {
331    /// Stream length mismatch was corrected
332    StreamLengthCorrected {
333        declared_length: usize,
334        actual_length: usize,
335        object_id: Option<(u32, u16)>,
336    },
337    /// Invalid character encoding was recovered
338    InvalidEncoding {
339        position: usize,
340        recovered_text: String,
341        encoding_used: Option<encoding::EncodingType>,
342        replacement_count: usize,
343    },
344    /// Missing required key with fallback used
345    MissingKeyWithFallback { key: String, fallback_value: String },
346    /// Syntax error was recovered
347    SyntaxErrorRecovered {
348        position: usize,
349        expected: String,
350        found: String,
351        recovery_action: String,
352    },
353    /// Invalid object reference was skipped
354    InvalidReferenceSkipped {
355        object_id: (u32, u16),
356        reason: String,
357    },
358}
359
360/// PDF Parser errors covering all failure modes during parsing.
361///
362/// # Error Categories
363///
364/// - **I/O Errors**: File access and reading issues
365/// - **Format Errors**: Invalid PDF structure or syntax
366/// - **Unsupported Features**: Encryption, newer PDF versions
367/// - **Reference Errors**: Invalid or circular object references
368/// - **Stream Errors**: Decompression or filter failures
369///
370/// # Example
371///
372/// ```rust
373/// use oxidize_pdf::parser::{PdfReader, ParseError};
374///
375/// # fn example() -> Result<(), ParseError> {
376/// match PdfReader::open("missing.pdf") {
377///     Ok(_) => println!("File opened"),
378///     Err(ParseError::Io(e)) => println!("IO error: {}", e),
379///     Err(ParseError::InvalidHeader) => println!("Not a valid PDF"),
380///     Err(e) => println!("Other error: {}", e),
381/// }
382/// # Ok(())
383/// # }
384/// ```
385///
386/// # Error Recovery and Tolerant Parsing
387///
388/// The parser supports different levels of error tolerance for handling corrupted or
389/// non-standard PDF files:
390///
391/// ```rust,no_run
392/// use oxidize_pdf::parser::{PdfReader, ParseOptions};
393/// use std::fs::File;
394///
395/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
396/// // Strict parsing (default) - fails on any deviation from PDF spec
397/// let strict_reader = PdfReader::open("document.pdf")?;
398///
399/// // Tolerant parsing - attempts to recover from errors
400/// let file = File::open("corrupted.pdf")?;
401/// let tolerant_reader = PdfReader::new_with_options(file, ParseOptions::tolerant())?;
402///
403/// // Skip errors mode - ignores corrupt streams and returns partial content
404/// let file = File::open("problematic.pdf")?;
405/// let skip_errors_reader = PdfReader::new_with_options(file, ParseOptions::skip_errors())?;
406/// # Ok(())
407/// # }
408/// ```
409#[derive(Debug, thiserror::Error)]
410pub enum ParseError {
411    /// I/O error during file operations
412    #[error("IO error: {0}")]
413    Io(#[from] std::io::Error),
414
415    /// PDF file doesn't start with valid header (%PDF-)
416    #[error("Invalid PDF header")]
417    InvalidHeader,
418
419    /// PDF version is not supported
420    #[error("Unsupported PDF version: {0}")]
421    UnsupportedVersion(String),
422
423    /// Syntax error in PDF structure
424    #[error("Syntax error at position {position}: {message}")]
425    SyntaxError { position: usize, message: String },
426
427    #[error("Unexpected token: expected {expected}, found {found}")]
428    UnexpectedToken { expected: String, found: String },
429
430    /// Invalid or non-existent object reference
431    #[error("Invalid object reference: {0} {1} R")]
432    InvalidReference(u32, u16),
433
434    /// Required dictionary key is missing
435    #[error("Missing required key: {0}")]
436    MissingKey(String),
437
438    #[error("Invalid xref table")]
439    InvalidXRef,
440
441    #[error("Invalid trailer")]
442    InvalidTrailer,
443
444    #[error("Circular reference detected")]
445    CircularReference,
446
447    /// Error decoding/decompressing stream data
448    #[error("Stream decode error: {0}")]
449    StreamDecodeError(String),
450
451    /// PDF is encrypted and could not be automatically decrypted
452    #[error(
453        "PDF is encrypted and could not be decrypted (unsupported encryption or password required)"
454    )]
455    EncryptionNotSupported,
456
457    /// Wrong password provided for encrypted PDF
458    #[error("Wrong password: the provided password is incorrect")]
459    WrongPassword,
460
461    /// PDF is locked - must call unlock() before reading objects
462    #[error("PDF is locked: call unlock() with the correct password before reading objects")]
463    PdfLocked,
464
465    /// Empty file
466    #[error("File is empty (0 bytes)")]
467    EmptyFile,
468
469    /// Stream length mismatch (only in strict mode)
470    #[error(
471        "Stream length mismatch: declared {declared} bytes, but found endstream at {actual} bytes"
472    )]
473    StreamLengthMismatch { declared: usize, actual: usize },
474
475    /// Character encoding error
476    #[error("Character encoding error at position {position}: {message}")]
477    CharacterEncodingError { position: usize, message: String },
478
479    /// Unexpected character in PDF content
480    #[error("Unexpected character: {character}")]
481    UnexpectedCharacter { character: String },
482
483    /// Serialization error (e.g. JSON serialization of RAG chunks)
484    #[error("Serialization error: {0}")]
485    SerializationError(String),
486}
487
488impl From<ParseError> for OxidizePdfError {
489    fn from(err: ParseError) -> Self {
490        OxidizePdfError::ParseError(err.to_string())
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    #[test]
499    fn test_module_exports() {
500        // Verify that all important types are properly exported
501
502        // Test that we can create a PdfObject
503        let _obj = PdfObject::Null;
504
505        // Test that we can create a PdfDictionary
506        let _dict = PdfDictionary::new();
507
508        // Test that we can create a PdfArray
509        let _array = PdfArray::new();
510
511        // Test that we can create a PdfName
512        let _name = PdfName::new("Test".to_string());
513
514        // Test that we can create a PdfString
515        let _string = PdfString::new(b"Test".to_vec());
516    }
517
518    #[test]
519    fn test_parse_error_conversion() {
520        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
521        let parse_error = ParseError::Io(io_error);
522        let oxidize_error: OxidizePdfError = parse_error.into();
523
524        match oxidize_error {
525            OxidizePdfError::ParseError(_) => assert!(true),
526            _ => assert!(false, "Expected ParseError variant"),
527        }
528    }
529
530    #[test]
531    fn test_parse_error_messages() {
532        let errors = vec![
533            ParseError::InvalidHeader,
534            ParseError::UnsupportedVersion("2.5".to_string()),
535            ParseError::InvalidXRef,
536            ParseError::InvalidTrailer,
537            ParseError::CircularReference,
538            ParseError::EncryptionNotSupported,
539        ];
540
541        for error in errors {
542            let message = error.to_string();
543            assert!(!message.is_empty());
544        }
545    }
546
547    // ============= ParseOptions Tests =============
548
549    #[test]
550    fn test_parse_options_default() {
551        let opts = ParseOptions::default();
552        assert!(opts.strict_mode); // default is true
553        assert!(!opts.recover_from_stream_errors); // default is false
554        assert!(!opts.ignore_corrupt_streams); // default is false
555        assert!(!opts.partial_content_allowed); // default is false
556        assert_eq!(opts.max_recovery_attempts, 3);
557        assert!(!opts.log_recovery_details);
558        assert!(!opts.lenient_streams);
559        assert_eq!(opts.max_recovery_bytes, 1000); // default is 1000
560        assert!(!opts.collect_warnings);
561        assert!(opts.lenient_encoding); // default is true
562        assert!(opts.preferred_encoding.is_none());
563        assert!(!opts.lenient_syntax);
564    }
565
566    #[test]
567    fn test_parse_options_strict() {
568        let opts = ParseOptions::strict();
569        assert!(opts.strict_mode);
570        assert!(!opts.recover_from_stream_errors);
571        assert!(!opts.ignore_corrupt_streams);
572        assert!(!opts.partial_content_allowed);
573        assert!(!opts.lenient_streams);
574        assert!(!opts.collect_warnings);
575        assert!(!opts.lenient_encoding);
576        assert!(!opts.lenient_syntax);
577    }
578
579    #[test]
580    fn test_parse_options_tolerant() {
581        let opts = ParseOptions::tolerant();
582        assert!(!opts.strict_mode);
583        assert!(opts.recover_from_stream_errors);
584        assert!(!opts.ignore_corrupt_streams);
585        assert!(opts.partial_content_allowed);
586        assert!(opts.lenient_streams);
587        assert!(opts.collect_warnings);
588        assert!(opts.lenient_encoding);
589        assert!(opts.lenient_syntax);
590    }
591
592    #[test]
593    fn test_parse_options_lenient() {
594        let opts = ParseOptions::lenient();
595        assert!(!opts.strict_mode);
596        assert!(opts.recover_from_stream_errors);
597        assert!(!opts.ignore_corrupt_streams); // lenient (tolerant) doesn't ignore
598        assert!(opts.partial_content_allowed);
599        assert!(opts.lenient_streams);
600        assert!(opts.collect_warnings);
601        assert!(opts.lenient_encoding);
602        assert!(opts.lenient_syntax);
603        assert_eq!(opts.max_recovery_attempts, 5);
604        assert_eq!(opts.max_recovery_bytes, 5000);
605    }
606
607    #[test]
608    fn test_parse_options_skip_errors() {
609        let opts = ParseOptions::skip_errors();
610        assert!(!opts.strict_mode);
611        assert!(opts.recover_from_stream_errors);
612        assert!(opts.ignore_corrupt_streams); // skip_errors does ignore
613        assert!(opts.partial_content_allowed);
614        assert!(opts.lenient_streams);
615        assert!(!opts.collect_warnings); // skip_errors doesn't collect warnings
616        assert!(opts.lenient_encoding);
617        assert!(opts.lenient_syntax);
618        assert_eq!(opts.max_recovery_attempts, 1);
619        assert_eq!(opts.max_recovery_bytes, 5000);
620    }
621
622    #[test]
623    fn test_parse_options_builder() {
624        let mut opts = ParseOptions::default();
625        opts.strict_mode = false;
626        opts.recover_from_stream_errors = true;
627        opts.max_recovery_attempts = 10;
628        opts.lenient_encoding = true;
629
630        assert!(!opts.strict_mode);
631        assert!(opts.recover_from_stream_errors);
632        assert_eq!(opts.max_recovery_attempts, 10);
633        assert!(opts.lenient_encoding);
634    }
635
636    #[test]
637    fn test_parse_error_variants() {
638        // Test all ParseError variants
639        let errors = vec![
640            ParseError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test")),
641            ParseError::InvalidHeader,
642            ParseError::UnsupportedVersion("3.0".to_string()),
643            ParseError::InvalidXRef,
644            ParseError::InvalidTrailer,
645            ParseError::InvalidReference(1, 0),
646            ParseError::MissingKey("Type".to_string()),
647            ParseError::CircularReference,
648            ParseError::EncryptionNotSupported,
649            ParseError::EmptyFile,
650            ParseError::StreamDecodeError("decode error".to_string()),
651            ParseError::StreamLengthMismatch {
652                declared: 100,
653                actual: 50,
654            },
655            ParseError::CharacterEncodingError {
656                position: 10,
657                message: "invalid UTF-8".to_string(),
658            },
659            ParseError::SyntaxError {
660                position: 100,
661                message: "unexpected token".to_string(),
662            },
663            ParseError::UnexpectedToken {
664                expected: "dict".to_string(),
665                found: "array".to_string(),
666            },
667        ];
668
669        for error in errors {
670            // Test Display implementation
671            let display = format!("{}", error);
672            assert!(!display.is_empty());
673
674            // Test conversion to OxidizePdfError
675            let _oxidize_err: OxidizePdfError = error.into();
676        }
677    }
678
679    #[test]
680    fn test_pdf_object_creation() {
681        // Test all PdfObject variants
682        let null = PdfObject::Null;
683        let boolean = PdfObject::Boolean(true);
684        let integer = PdfObject::Integer(42);
685        let _real = PdfObject::Real(3.14);
686        let _string = PdfObject::String(PdfString::new(b"test".to_vec()));
687        let _name = PdfObject::Name(PdfName::new("Test".to_string()));
688        let _array = PdfObject::Array(PdfArray::new());
689        let _dict = PdfObject::Dictionary(PdfDictionary::new());
690        // PdfStream doesn't have a public constructor, skip it for now
691        // let stream = PdfObject::Stream(...);
692        let _reference = PdfObject::Reference(1, 0);
693
694        // Test pattern matching
695        match null {
696            PdfObject::Null => assert!(true),
697            _ => panic!("Expected Null"),
698        }
699
700        match boolean {
701            PdfObject::Boolean(v) => assert!(v),
702            _ => panic!("Expected Boolean"),
703        }
704
705        match integer {
706            PdfObject::Integer(v) => assert_eq!(v, 42),
707            _ => panic!("Expected Integer"),
708        }
709    }
710
711    #[test]
712    fn test_pdf_dictionary_operations() {
713        let mut dict = PdfDictionary::new();
714
715        // Test insertion
716        dict.insert(
717            "Type".to_string(),
718            PdfObject::Name(PdfName::new("Page".to_string())),
719        );
720        dict.insert("Count".to_string(), PdfObject::Integer(10));
721
722        // Test retrieval
723        assert!(dict.get("Type").is_some());
724        assert!(dict.get("Count").is_some());
725        assert!(dict.get("Missing").is_none());
726
727        // Test contains
728        assert!(dict.contains_key("Type"));
729        assert!(!dict.contains_key("Missing"));
730
731        // Test get_type
732        let type_name = dict.get_type();
733        assert_eq!(type_name, Some("Page"));
734    }
735
736    #[test]
737    fn test_pdf_array_operations() {
738        let mut array = PdfArray::new();
739
740        // Test push (direct access to inner Vec)
741        array.0.push(PdfObject::Integer(1));
742        array.0.push(PdfObject::Integer(2));
743        array.0.push(PdfObject::Integer(3));
744
745        // Test length
746        assert_eq!(array.len(), 3);
747
748        // Test is_empty
749        assert!(!array.is_empty());
750
751        // Test get
752        assert!(array.get(0).is_some());
753        assert!(array.get(10).is_none());
754
755        // Test iteration (direct access to inner Vec)
756        let mut sum = 0;
757        for obj in array.0.iter() {
758            if let PdfObject::Integer(v) = obj {
759                sum += v;
760            }
761        }
762        assert_eq!(sum, 6);
763    }
764
765    #[test]
766    fn test_pdf_name_operations() {
767        let name1 = PdfName::new("Type".to_string());
768        let name2 = PdfName::new("Type".to_string());
769        let name3 = PdfName::new("Subtype".to_string());
770
771        // Test equality
772        assert_eq!(name1, name2);
773        assert_ne!(name1, name3);
774
775        // Test inner field access (PdfName.0 is pub)
776        assert_eq!(name1.0, "Type");
777    }
778
779    #[test]
780    fn test_pdf_string_operations() {
781        // Test literal string
782        let literal = PdfString::new(b"Hello World".to_vec());
783        // PdfString has public inner field
784        assert_eq!(literal.0, b"Hello World");
785
786        // Test empty string
787        let empty = PdfString::new(Vec::new());
788        assert!(empty.0.is_empty());
789    }
790
791    // PdfStream tests removed - no public constructor
792
793    #[test]
794    fn test_parse_options_modifications() {
795        let mut opts = ParseOptions::default();
796
797        // Test field modifications
798        opts.strict_mode = false;
799        assert!(!opts.strict_mode);
800
801        opts.recover_from_stream_errors = true;
802        assert!(opts.recover_from_stream_errors);
803
804        opts.max_recovery_attempts = 20;
805        assert_eq!(opts.max_recovery_attempts, 20);
806
807        opts.lenient_streams = true;
808        assert!(opts.lenient_streams);
809
810        // Skip encoding type test - types not matching
811        // opts.preferred_encoding = Some(...);
812    }
813
814    // Content operation and encoding tests removed - types don't match actual implementation
815
816    #[test]
817    fn test_resource_types() {
818        // Test that we can create resource dictionaries
819        let mut resources = PdfDictionary::new();
820
821        // Add Font resources
822        let mut fonts = PdfDictionary::new();
823        fonts.insert("F1".to_string(), PdfObject::Reference(10, 0));
824        resources.insert("Font".to_string(), PdfObject::Dictionary(fonts));
825
826        // Add XObject resources
827        let mut xobjects = PdfDictionary::new();
828        xobjects.insert("Im1".to_string(), PdfObject::Reference(20, 0));
829        resources.insert("XObject".to_string(), PdfObject::Dictionary(xobjects));
830
831        // Verify resources structure
832        assert!(resources.contains_key("Font"));
833        assert!(resources.contains_key("XObject"));
834    }
835}