v0.3.5 Release
This release delivers major performance improvements, 100% pass rate on 3,830 PDFs, comprehensive error recovery for 28+ real-world PDF failures, and spec-correct rendering — the biggest stability release to date.
⚡ Performance
- Font caching across pages — Document-level cache keyed by
ObjectRefavoids re-parsing shared fonts on every page. For a 1000-page document sharing 20 fonts, this reduces font parsing from 40,000 operations to 20 - Page object caching —
get_page()caches resolved page objects, eliminating repeated page tree traversal for multi-page extraction - Structure tree caching — Structure tree result cached after first access, avoiding redundant parsing on every
extract_text()call - BT operator early-out —
extract_spans(),extract_chars()skip the full text extraction pipeline for image-only pages with no text operators - Larger I/O buffer —
BufReadercapacity increased from 8 KB to 256 KB for files >100 MB, reducing syscall overhead on giant PDFs - Xref reconstruction threshold removed — Eliminated
xref.len() < 5heuristic that caused 5-13s delays on valid portfolio PDFs
✅ Verified — 3,830-PDF Corpus
| Corpus | PDFs | Pass Rate |
|---|---|---|
| veraPDF (PDF/A compliance) | 2,907 | 100% |
| Mozilla pdf.js | 897 | 100% |
| SafeDocs (targeted edge cases) | 26 | 100% |
| Total | 3,830 | 100% |
| Metric | Result |
|---|---|
| p50 latency | 0.6ms |
| p90 latency | 3.0ms |
| p99 latency | 33ms |
| Under 10ms | 97.6% of PDFs |
| Timeouts | 0 |
| Panics | 0 |
🔐 Added — Encryption
- Owner password authentication (Algorithm 7 for R≤4, Algorithm 12 for R≥5)
- R≤4: MD5 hash chain → RC4 key → decrypts
/Oto recover user password - R≥5: SHA-256 with SASLprep normalization and owner validation/key salts
- R≤4: MD5 hash chain → RC4 key → decrypts
- R≥5 user password verification — Full AES-256 with SHA-256 validation/key salts per PDF spec §7.6.4.3.3
- Public API —
PdfDocument::authenticate(password)exposed for Rust and Python
📋 Added — PDF/A & PDF/X Compliance Validation
- PDF/A: XMP metadata validation, color space scanning for device-dependent operators, AFRelationship validation for embedded files
- PDF/X: Page box relationships (TrimBox ⊆ BleedBox ⊆ MediaBox), ExtGState transparency detection, ICC profile validation, device-dependent color detection
🎨 Added — Rendering
- Spec-correct clipping (PDF §8.5.4) — Clip state scoped to
q/Qsave/restore via clip stack; new clips intersect with existing clip region;W/W*no longer consume the current path; clip mask applied to all painting operations including text and images - Glyph advance width — Text position advances per PDF spec §9.4.4:
tx = (w0/1000 × Tfs + Tc + Tw) × Th - Form XObject rendering —
/Matrixtransform, resource inheritance, recursive content stream execution
🛡️ Fixed — Error Recovery (28+ real-world PDFs)
- Missing objects resolve to Null — Per PDF spec §7.3.10, unresolvable indirect references return
Nullinstead of errors (fixes 16 files) - Lenient header version parsing — Valid headers with unusual version strings no longer rejected
- Non-standard encryption — V=1,R=3 combinations handled leniently
- Non-dictionary Resources — Invalid
/Resourcesentries treated as empty resources - Null nodes in page tree — Non-dictionary child nodes gracefully skipped
- Corrupt content streams — Malformed streams return empty content instead of errors
- Enhanced page tree scanning —
/Resources+/Parentheuristic and/Kidsdirect resolution added as fallback
🖼️ Fixed — Image Extraction
- Content stream processing —
extract_images()now findsDooperator calls, extracting images previously missed - Nested Form XObjects — Recursive extraction with cycle detection
- Inline images —
BI...ID...EIparsing with abbreviation expansion per PDF spec - 4-corner affine transform — Image bounding boxes correctly handle rotation, shear, and negative scaling
- ColorSpace indirect references — Resolved before extraction (e.g.,
7 0 R)
🔧 Fixed — Parser & Page Access
- Multi-line object headers —
1 0\nobjformat from Google PDFs now supported - Extended header search — Search window extended from 1024 to 8192 bytes
- Missing
/Contentsentry — Returns empty content instead of erroring - Cyclic page tree detection — Tracks visited nodes to prevent stack overflow
- Pages without
/Type— Found via/MediaBoxor/Contentskeys - DoS protection —
/Countvalidated against PDF spec Annex C.2 limit (8,388,607)
🔒 Fixed — Encryption & Content Streams
- Short encryption key — AES decryption with undersized keys returns error instead of panic
- Xref stream hardening — Invalid entry sizes or out-of-bounds data no longer cause panics
- Dictionary-as-Stream fallback — Bare dictionaries treated as empty streams
- Filter abbreviations —
AHx,A85,LZW,Fl,RL,CCF,DCTand case-insensitive matching - Operator limit — Configurable limit (default 1,000,000) prevents pathological slowdowns
🧹 Code Quality
- Structure tree indirect references —
ObjectRefvariants resolved at parse time - Lexer
Rtoken disambiguation —1 0 RGcorrectly parsed as color operator - Stream whitespace trimming — Only strips CR/LF, no longer strips NUL/spaces from binary data
- 8 previously ignored tests un-ignored and fixed
🏆 Community Contributors
🥇 @SeanPedersen — Huge thanks to Sean for reporting multiple issues (#41, #44, #45, #46) that drove the entire stability focus of this release. His real-world testing uncovered a parser bug with Google-generated PDFs, image extraction failures, and performance problems — each triggering deep investigation and significant fixes. 🙏🔍
📥 Installation
Rust (crates.io)
cargo add pdf_oxidePython (PyPI)
pip install pdf_oxidePre-built Binaries
Download archives for Linux, macOS, and Windows from the assets below.
Release Date: February 15, 2026
Branch: main
Tag: v0.3.5
Closes: #41, #44, #45, #46, #47–#62, #63–#79