Fixed
-
Reading-order sort could panic on malformed or scanned PDFs instead of returning text (#807) — Rust's
sort_by/sort_unstable_by(1.81+) detects a comparator that violates total order and panics withdoes not correctly implement a total order— uncatchable across the FFI boundary, aborting the host process across every binding. Two independent causes were fixed:- Tategaki (vertical-writing) column grouping (ISO 32000-1 §9.7.4.3,
WMode1).sort_spans_vertical_tategakiand its two duplicated call sites (postprocess_spans's tategaki intercept,TategakiStrategy) decided "same column" with a pairwise|a - b| <= tolcheck on each span's X-center. That check is not transitive: a chain of spans each withintolof its neighbor can span far more thantolend to end, so the comparator can claimA<B,B<C, andC<Aall at once. This is exactly what a scanned vertical-CJK OCR layer produces — hundreds of single-glyph, sub-point-wide spans whose X-centers step by a fraction of the column pitch. Columns are now found by single-linkage clustering of X-centers (order right-to-left, start a new column when the gap to the previous center exceeds the tolerance), then sorted by(column, Y)— a genuine total order, and more accurate than quantizing each center into a fixed-size band independently, which can split two spans only a couple points apart into different columns if they straddle a band boundary. - Oversized real-number literals silently overflowing to
Infinity. PDF 32000-1:2008 Annex C.2 bounds real values to approximately ±3.403×10^38, but the lexer parsed real literals viaf64::from_str, which saturates an all-digit literal past that limit tof64::INFINITYrather than erroring. Combined with a degenerate content-stream matrix (a zero CTM/Tmcomponent),0.0 × Infinityproduced a NaN glyph coordinate that could panic the same class of sort elsewhere in the pipeline. Oversized literals are now clamped to the spec's implementation limit at parse time, so an out-of-range literal can no longer poison downstream arithmetic into NaN.
@tobocop2 reported this, root-caused it, and submitted a working fix (#808) using single-linkage column clustering, along with a minimal repro and three real-world vertical-Japanese novels to stress-test against. We folded that clustering approach directly into this fix (verified byte-identical output against #808 on all three novels) alongside the separate lexer fix below, so #808 was closed in favor of this PR.
- Tategaki (vertical-writing) column grouping (ISO 32000-1 §9.7.4.3,
Thanks to @tobocop2 (#807, #808) for finding, root-causing, and fixing this.
Installation
Rust (crates.io)
cargo add pdf_oxidePython (PyPI)
pip install pdf_oxideJavaScript/WASM (npm)
npm install pdf-oxide-wasmCLI (Homebrew)
brew install yfedoseev/tap/pdf-oxideCLI (Scoop — Windows)
scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxideCLI (Shell installer)
curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | shCLI (cargo-binstall)
cargo binstall pdf_oxide_cliMCP Server (for AI assistants)
cargo install pdf_oxide_mcpPre-built Binaries
Download archives for Linux, macOS, and Windows from the assets below. Each archive includes both pdf-oxide (CLI) and pdf-oxide-mcp (MCP server).
Platform Support
| Platform | Architecture | Archive |
|---|---|---|
| Linux | x86_64 (glibc) | pdf_oxide-linux-x86_64-*.tar.gz
|
| Linux | x86_64 (musl) | pdf_oxide-linux-x86_64-musl-*.tar.gz
|
| Linux | ARM64 | pdf_oxide-linux-aarch64-*.tar.gz
|
| macOS | x86_64 (Intel) | pdf_oxide-macos-x86_64-*.tar.gz
|
| macOS | ARM64 (Apple Silicon) | pdf_oxide-macos-aarch64-*.tar.gz
|
| Windows | x86_64 | pdf_oxide-windows-x86_64-*.zip
|
Changelog
See CHANGELOG.md for full details.