Releases: yfedoseev/pdf_oxide
Release list
v0.3.74 | Scientific and print-era PDF extraction fixes — per-glyph advance now folds `TJ` kerning per the spec so it matches the renderer (poppler/PDFium/pymupdf), fixing word spacing on justified and kerned text; displayed-math tokens no longer fuse into single words, dense LaTeX pages stop being misrouted to OCR, subscript indices stay subscripts, condensed headings and running footers recover their word gaps, stroke-drawn table rules and 90°-rotated pages read correctly, and scanned Hebrew/Arabic OCR layers extract in logical order.
Fixed
- Displayed-math tokens fused into one word —
dx/dt =extracted as=dt, and whole equations could collapse into a single token (#830, #836) — the word-gap merge's backtrack check (gap ≤ font_size × 0.15) had no lower bound, so a run backtracking far behind the previous word's origin (a fraction bar returning to typeset the denominator, a relation sign closing an equation) still satisfied "a large negative gap ≤ a small positive threshold" and merged; because the merge is incremental, a chain of such backtracks could collapse an entire displayed equation — and in the worst corpus case, the start of the following sentence — into one word. The fix landed in two stages: the composed-text emitter (extract_text/to_markdown/to_html) was guarded first, then the identical guard (real baseline offset, origin-or-left backtrack, multi-em overlap, gated off for RTL) was applied toextract_words_inner's post-clustering merge so word geometry is correct too. Because table detection consumes word geometry, the detector was hardened in the same change so the corrected words no longer fabricate phantom tables out of ordinary wrapped captions. - Subscript index numbers extracted as decimals —
P₁,₀becameP1.0(#816) — the decimal-merge rule joins two adjacent pure-digit runs with a.(a heuristic for split-box dollar amounts where the whole part and cents print in separate fixed-width boxes,123456+72→123456.72). Its upper gap bound was too permissive: real split-box amounts sit ~0.8–1.0× the font size apart, but subscript index digits are a smaller font spaced ~1.5–1.7× apart, so the old 2.0× ceiling let the rule invent decimals the document never contained. The ceiling is tightened to 1.3× the font size, separating genuine integer/cents boxes from widely spaced subscripts. - Born-digital pages were classified as
Scannedand routed topages_needing_ocr— 13.7% of a 6,269-page corpus (#840) — OCR-ing a page that already carries good native text replaces it with worse output, so a wrongScannedverdict is actively harmful. The dominant cause:gather_page_signalsand a secondtext_quality_gatecall site both built their word-fragmentation input by joining raw content-stream spans with a forced space after each one. Math typesetting draws each atom — a parenthesis, an operator, a subscript — as its own span, so(∞)became three one-character "words"; on a dense LaTeX page this inflated the fragmented-word ratio and collapsed average word length until the quality gate mistook it for a scan and overrode an otherwise-correctTextLayerverdict. Both call sites now build their word list fromextract_words— the same glyph/span clusteringextract_textrelies on, including the new math-backtrack guard — instead of one token per span. extract_wordssplit single string literals into fragments (module→m|odu|le) via phantom glyph gaps (#811) — TJ-offset space spans (ISO 32000-1 §9.4.4) were created with one char but an emptychar_widths, and the span merge kept the widths in lockstep by tail-append + tail-resize. Whenever a width-less span contributed chars anywhere but the tail, every subsequent width shifted one slot, so per-glyph decomposition paired each glyph's accurate x-origin with its neighbor's nominal width — phantom ~0.3 em intra-word gaps that the word-gap clusterer split on. Space spans now carry their advance from creation, and the merge normalizes every contribution at its own position — inserted separators get the real geometric gap they stand in for.PathContentgeometry ignoredstroke_width, so stroke-width-encoded table rules extracted as 1×0 pt specks (#812) — print-era generators draw a table's vertical rule as a ~1 pt segment stroked as wide as the table is tall (430 w … 0 0 m .998 0 l S). The geometric bbox of that path bears no resemblance to the rendered bar, sois_table_primitive()and the line-based table detector missed the whole grid and its text extracted column-major.stroke_widthis now CTM-scaled at extraction (§8.4.3.2 — the line width transforms like all other geometry), the newPathContent::rendered_bbox()exposes the stroke-inflated extents (exact perpendicular + cap inflation for straight segments, conservative half-width outset otherwise), and line classification, clustering, and the per-row/column separator checks all judge rendered extents. The geometricbboxis unchanged for every other consumer. Also exposed aspdf_oxide_path_get_rendered_bbox(C FFI) andrendered_bboxin the Python/WASM path dicts, and threaded through the go, ruby, php, swift, csharp, dart, elixir, zig, julia, r, objc, cpp, and node bindings.- 90°-rotated pages extracted in portrait order and words carried no rotation metadata (#813) — landscape tables typeset on portrait pages (text-matrix rotation, no
/Rotatekey) came out as interleaved word salad: the reading-order pipeline re-sorted spans with portrait-frame comparators, the plain-text assembler grouped lines in the portrait frame, androtation_degreeswas dropped at bothTextSpan::to_charsand word assembly. A dominant-rotation vote (half-or-more of the page's non-whitespace spans sharing one quadrant rotation, mirroring the tategaki vote) now orders the whole page in its rotated reading frame — coordinates are restored afterwards, so callers keep true page space — and minority rotated runs (margin stamps, figure labels) are ordered upright per rotation group and appended after the horizontal flow, matching the span path's existing firewall. Runs sharing a ±90° rotation no longer span-merge across rotated lines.rotation_degreesnow flows span → char →Wordand is exposed onWord(Rust/serde),PyWord, the WASM word JSON, andpdf_oxide_word_get_rotation(C FFI), and surfaced on the word type of the go, ruby, php, swift, csharp, dart, elixir, zig, julia, r, objc, cpp, and node bindings, plus the JVMTextWord(Java, inherited by the Kotlin/Scala/Clojure wrappers). - Scanned Hebrew/Arabic OCR text layers extracted reversed — every word both letter- and word-order-reversed (#826) — scanned RTL PDFs whose invisible OCR text layer emits one TJ array per recognized word (the standard OCR-sandwich shape, e.g. Tesseract-style producers) had two compounding bugs in the Tj/TJ buffer-flush path.
flush_tj_buffer(the defaultWordBoundaryMode::Tiebreakerpath) never received the confidence-gated geometric direction detector, so it still used the oldaccumulated_width > 0.0heuristic — true for nearly every non-empty RTL buffer — and reversed unconditionally instead of detecting direction; all three flush sites now route through one shared decision point (bidi::apply_rtl_verdict). And because already-logical invisible-OCR text and genuinely visual-order text have identical geometric signatures, text render mode is now threaded through so invisible runs (Tr3/7) skip the geometric heuristics entirely and trust extraction order as-is. FluentPageBuilder::rich_paragraphdrew consecutiveTextRuns flush together —TextRun::bold("Text Run 1") + TextRun::normal("Text Run 2")extracted asText Run 1Text Run 2(#837) — each run word-wraps and emits its own text, then advancescursor_xby exactly the emitted width, with nothing separating one run's end from the next's start, so a run boundary falling mid-line drew the next run against the previous one. Consecutive runs on the same line are now separated.- Stacked two-line column/table-header cells fused into one token —
Comparisonoverrateextracted asComparisonrate(#847) — when the structure-tree (tagged-content) assembler linearizes a header cell drawn as two stacked rows, the rows arrive as consecutive spans that horizontally overlap (negative gap) at a baseline drop sitting just under the same-line threshold, so the assembler treats them as one line and defers to the space decision — which, seeing a negative gap, returned no space and glued them. A negative gap combined with a genuine baseline shift is two stacked tokens, never intra-word kerning (which shares a baseline), so a separator is now inserted. Scoped to the tagged/structure-tree path so main-flow inputs (e.g. LaTeX math fraction stacks, already handled by dedicated line-break branches) stay byte-identical; a 419-PDF sweep confirmed the change is isolated to tagged tables/forms with only glyph-preserving spacing gains. - Per-glyph advance drifted behind the true rendered position on kerned/justified text, manufacturing phantom inter-glyph gaps (#847) — a sub-threshold
TJpositioning number (ISO 32000-1 §9.4.4) advanced the text matrix but was dropped from the run's stored per-glyph advance (char_widths/accumulated width), so on a line drawn as one continuous buffer the many small post-space kerning offsets accumulated into a multi-point undershoot: the reconstructed glyph positions fell behind where the glyphs actually render. Poppler/PDFium/pymupdf all agree on the true position because they fold the offset into the advance; pdf_oxide was the sole outlier (−2.3 pt over one measured line, concentrated at word gaps). The stored advance now folds the exact §9.4.4 displacement —−Tj/1000 × Tfs × Th— into the run, so per-glyph geometry equals the text-matrix position by construction (closing ~72% of the drift on the worst case; the residual is the/Widths-vs-substitute-font-metric difference, a separate axis). This is a generic positioning fix, not a heuristic — it is the same advance the renderer uses — and it is what lets the narrow-word-gap rescue below operate on true gaps instead of phantom ones (a phantom ~0.15 em gap is what previously over-splitmatched→match ed, forcing this rescue to be held back). A companion guard tightens the cross-font single-letter glue ceiling from 0.25 em to 0.12 em: 0.25 em is a full word space, so a word followed by a single-letter variable set in ...
v0.3.73 | Two independent reading-order sort panics fixed — a non-transitive vertical-CJK (tategaki) column comparator and an oversized-literal lexer overflow — so malformed and scanned PDFs no longer crash extraction instead of returning text.
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.
v0.3.72 | Rotated-page text extraction & a transitive-dependency security patch — the spatial extractors no longer garble text on rotated pages, and the optional Office-export path clears an untrusted-XML denial-of-service advisory.
Security
office_oxide0.1.2 → 0.1.3 (clears RUSTSEC-2026-0194 / RUSTSEC-2026-0195) — the optional Office-document export path depended onoffice_oxide0.1.2, whose transitivequick-xml0.40 has an unbounded per-xmlnsheap allocation inNsReader::pushthat a crafted DOCX/XLSX/PPTX could use to exhaust memory (a denial-of-service on untrusted input).office_oxide0.1.3 upgrades toquick-xml0.41, which bounds the allocation. pdf_oxide's ownquick-xmlwas already 0.41; this bump closes the remaining transitive path so the dependency tree is advisory-clean.
Fixed
-
extract_words/extract_spans/extract_text_linesgarbled text on rotated pages (#804) — on rotated pages the spatial extractors clustered along the wrong axis and fused unrelated cells into giant tokens (a whole column returned as a single 1000+ character "word", separate rows fused into one line). Two independent root causes were fixed:- Page
/Rotate90/270 (§7.7.3.3). Span bounding boxes were mapped into the page's displayed frame before word/line clustering, but a span decomposes into characters by laying glyphs horizontally along its bbox with their raw advance widths — a representation that cannot express a run whose visual direction has become vertical. Every raw text row therefore collapsed onto one displayed band and perpendicular columns fused. Because the horizontal clustering is already correct in raw user space (andextract_charsalready reports raw coordinates), 90°/270° pages now keep their span geometry in raw space; all four spatial APIs agree. (180° pages, where text stays horizontal, keep their existing mirror.) - Rotated text matrices (
rotation_degrees = ±90— vertical column headers, chart-axis labels). A run drawn with a rotated text matrix advances along a rotated axis, but the extractor stores a span bbox flattened onto the x-axis (width = Σ advances, height = font), so adjacent rotated columns overlap and the reading-order word merge and y-band line grouping fused them. Rotated runs are now excluded from both the cross-span word merge and the line grouping — each stays its own word(s) and its own line.
Thanks @ankursri494 for the report and the public, PII-free reproducers.
- Page
Thanks to @ankursri494 (#804) for reporting the issue that drove this release.
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.
v0.3.71 | Spec-alignment & extraction-leadership release — the renderer gains tiling patterns, Type 3 fonts, and mesh shadings; the markdown converter gains first-class tables, images, links, headings, nested lists, running header/footer removal, and footnotes; plus accurate per-glyph coordinates on the word/span APIs, correct PDF/X ICC validation, and a structure-tree parser that no longer drops large trees.
Added
- Renderer spec alignment (ISO 32000-1) — the CPU rasteriser now paints several previously-unsupported constructs: tiling patterns (PatternType 1, §8.7.3), Type 3 font glyphs (CharProcs executed under the font matrix with
d0/d1, §9.6.5), mesh shadings (free-form and lattice-form Gouraud triangle meshes and Coons/tensor patches — types 4/5/6/7 — plus function-based type 1, §8.7.4.5), text rendering modes 4–7 (glyph-outline clip accumulation acrossBT/ET, §9.3.6), and colour-key masking (/Mask [ranges], §8.9.6.4). JPEG 2000 images with chroma-subsampled components are now upsampled and decoded rather than skipped. - First-class tables in the markdown/HTML converters — the pipeline converter renders detected tables directly (pipe tables with header rows and colspan handling), replacing the fragile text-post-processing path.
- Images, links, and document structure in markdown — figures are emitted as
,/Linkannotations become[text](uri)/<a href>(with a safe-scheme gate), heading hierarchy is inferred as#–######, indentation-based nested lists are preserved, cross-page running headers/footers are detected and filtered, and superscript-marker + page-bottom footnotes become[^n]references. - Hybrid-reference files (
/XRefStm, §7.5.8.4) — a classic trailer's cross-reference-stream supplement is now parsed and merged, so hybrid PDFs resolve all objects.
Fixed
- Per-glyph coordinates in
extract_words/extract_spans/extract_text_linesdrifted on CID/Type 0 fonts (#780, part 2) — these APIs reconstructed each glyph's x-position by summing nominal advance widths, which omits the ISO 32000-1 §9.4.3TJ-array kerning, so positions drifted cumulatively along a line (up to tens of points) versusextract_chars. Each glyph's x now comes from the accurate content-stream position (matchingextract_charsand Poppler'spdftotext -bbox); on the reporter's repro, glyphs within 0.5 pt of the reference went from 15 % to 97 %. Word segmentation is unchanged (the char-width array is untouched), so complex-script extraction does not regress. Thanks @ankursri494 for the report and reproducer. - Valid ICC profiles reported as
[XCOLOR-005] … not a valid streaminvalidate_pdf_x(#797) — an ICCBased colour space embeds its profile as a stream (§8.6.5.5,[ /ICCBased stream ]), but the validator only accepted a bare dictionary and flagged every conforming profile (including the Ghent Workgroup PDF/X-4 suite). It now reads/Nfrom the stream dictionary. Thanks @takoportal for the detailed report and repro. - Structure-tree parsing dropped large trees under a hard-coded budget (#801) —
parse_structure_treeimposed a 200 ms wall-clock budget and a 10 000-element cap and returned no structure tree at all when either was exceeded (e.g. the 756-page ISO 32000-1 specification), which is non-deterministic across machines and silently loses data. The default now parses the complete tree; callers that need to bound the work can opt in via the newparse_structure_tree_with_budget(&doc, Option<Duration>)(anddoc.structure_tree_with_budget(…)). The redundant post-parse size check is removed. Thanks @bjorn3 for the report and proposed API. - Inter-word spaces dropped on justified
TJ-positioned text (#803) — on documents whose words are positioned withTJ/Tdoffsets in embedded Type 0 / Identity-H subset fonts (e.g. the 214-page ISO 21111-10 standard), whole runs extracted glued together —All rights reservedcame out asAllrightsreserved. The word-gap detector derives its threshold from the font's space-glyph advance, but under Identity-H character code0x20maps to CID 32 — an arbitrary glyph, not the space (ISO 32000-2 §9.7.5.2, §9.10.2: the space is reached through the font's CMap/ToUnicode, never code0x20). Reading that ~0.56 em glyph advance as the space width inflated the threshold so far that genuine ~0.25 em word gaps fell below it and were suppressed. Identity-encoded Type 0 fonts now fall back to the 0.25 em typographic default; non-Identity CMaps that legitimately place a space at0x20still use their explicit/Wentry. Thanks @Goldziher for the precise report and geometry. - Numeric median selection — heading/base-font-size statistics now use
select_nth_unstable_by(exact O(n)) instead of a full sort.
Thanks to @ankursri494 (#780), @takoportal (#797), @bjorn3 (#801), and @Goldziher (#803) for reporting the issues that drove this release.
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.
v0.3.70 | Extraction-fidelity release — kerning-split words rejoined in plain text, table/form line cells split consistently regardless of word width, resolved `/BaseFont` names on the span/word APIs, and content-stream order exposed on extracted spans and words.
Added
- Content-stream order exposed on extracted spans and words (#779) —
extract_wordsandextract_text_linesnow carry the originating span'ssequence(the content-stream emission order). It is surfaced idiomatically on the word/span types of every language binding — Python, Node.js and WASM, Go, the JVM (Java/Kotlin/Scala/Clojure), C#, Ruby, PHP, C and C++, Objective-C, Swift, Dart, R, Julia, Zig, and Elixir — via the new C-ABI accessorpdf_oxide_word_get_sequence. This lets consumers tell genuinely-consecutive draw calls apart from spatially-close-but-stream-distant ones (e.g. table cells vs. overlays), independent of the final reading order. Thanks @ankursri494 for the request.
Fixed
- A word split by a spurious space when its glyph runs overlap slightly (#791) — a single word drawn as two adjacent same-font runs whose glyphs overlap by a fraction of a point (ordinary tight kerning, e.g.
(PLANAL)then(TINA)positioned just insidePLANAL's right edge) was extracted asPLANAL TINA. The plain-text assembler now recognises this case — a negative inter-run gap, same font/weight/style, word characters on both sides, real (varying) per-glyph metrics, and not a lowercase→uppercase word boundary — and joins the runs with no inserted space, reconstructingPLANALTINA, matching pdftotext / PyMuPDF / lopdf on the same file. The spans are left unmerged, so page layout, reading order, and table detection are unaffected. Thanks @schelip for the report and minimal repro. extract_text --format linesmerged table/form cells across column gaps inconsistently (#792) — a flat 50 pt column-gap threshold made cell splitting depend on how wide each row's words happened to be, so a header row of short values (CEP/Cidade/UF) split into one line per cell while the value row directly below it (73751-452/PLANALTINA/GO, wider words, same gutters) merged into a single line. The threshold in line clustering is now font-relative ((font_size × 3).max(30 pt)), so rows sharing the same columns split the same way. Thanks @schelip for the report.- Span-derived APIs reported unresolved (alias) font names (#780, part 1) —
extract_spans,extract_words, andextract_text_linesreported the page's/Resources/Fontalias (e.g.F1) rather than the resolved/BaseFont(e.g.Helvetica,CIDFont+F1). They now resolve to the base font, matchingextract_charsand pdfminer.six / pdfplumber. (The second part of #780 — per-glyph coordinate drift on CID/Type0 fonts inextract_words— is tracked for a follow-up release.) Thanks @ankursri494 for the report. - Cased and caseless non-Latin prose no longer mis-detected as spatial tables — the no-rulings table detector's prose-paragraph guard now recognises sentence boundaries in cased non-Latin scripts and treats the Bengali/Devanagari danda (
।,॥) as a sentence terminator, so complex-script running prose that happens to align into columns is not extracted as a table grid.
Thanks to @schelip (#791, #792) and @ankursri494 (#779, #780) for reporting the issues that drove this release.
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.
v0.3.69 | Language-bindings release — idiomatic bindings for **C++, Swift, Kotlin, Dart, R, Julia, Zig, Scala, Clojure, Objective-C, and Elixir**, each over the stable C ABI, with per-language CI, package-registry publishing, cross-language regression examples, and single-source version management.
Added
- Eleven new language bindings, each with an idiomatic wrapper, an api-coverage test (one assertion per public method), runnable CI-asserted examples, a README with install coordinates, and a dedicated CI workflow (Linux+macOS) running the same verification set:
- C++ (
cpp/) — header-only C++17 RAII wrapper; CMake withinstall/exporttargets and a Conan recipe. - Swift (
swift/) — SwiftPM package + C module map. - Kotlin (
kotlin/) — thin facade over the Java JNI binding. - Dart/Flutter (
dart/) —dart:ffi. - R (
r/) —.CallC shim, external-pointer handles. - Julia (
julia/) —ccall. - Zig (
zig/) —@cImport. - Scala (
scala/) — thin facade over the Java JNI binding (Scala 3). - Clojure (
clojure/) — direct Java interop over the JNI binding. - Objective-C (
objc/) — NSObject wrappers over the C ABI. - Elixir (
elixir/) — dirty-scheduler NIF (CPU-bound work never blocks the BEAM).
- C++ (
- Package-registry publishing wired into the release pipeline for the new bindings: Maven Central (Kotlin, Scala), Clojars (Clojure), Hex.pm (Elixir), and pub.dev (Dart, via GitHub OIDC). Objective-C ships as a Trunk-free CocoaPods binary pod — an
xcframework+ podspec uploaded as release assets and installed via a:podspecURL — since CocoaPods Trunk goes read-only on 2026-12-02. C++ (vcpkg/Conan), R (CRAN), Julia (General registry), and Swift/Zig (git tag) are documented indocs/RELEASING-bindings.md. - Cross-language regression examples — alongside each binding's basic example, three shared-scenario examples (HTML extraction, word geometry, table extraction) run with output assertions in every binding's CI workflow.
- Single-source version management —
scripts/sync_version.pypropagates the canonicalCargo.tomlversion into every binding manifest and version/parity assert (--checkverifies,--set X.Y.Zbumps everything). AVersion ConsistencyCI workflow fails if any binding drifts.
Fixed
- Non-Identity-ordered Type0 fonts no longer emit a wrong character for CIDs missing from
/ToUnicode(#773, #775) — for an embedded Type0 font whose/ToUnicodeCMap omits some drawn CIDs (e.g. a ligature glyph with no single Unicode codepoint), the decode path fell back to a numeric guess — the GID via the standard glyph-name table → AGL, or the CID itself as a code point (char::from_u32) — emitting a plausible-but-wrong, content-like character that varied per subset (e.g. atiligature →:/D, sonotificacao→no:ficacao). The glyph has no Unicode anywhere in the file (no/ToUnicodeentry, nopostname, no GSUB), so the letters are unrecoverable, but substituting a wrong character is silent corruption. When a usable/ToUnicodeis present, the GID→AGL guess is now suppressed for all Type0 fonts, and the CID-as-Unicode guess is suppressed for fonts whoseCIDSystemInfoordering is notIdentity, so an uncovered CID there decodes toU+FFFDinstead. For Identity-ordered (Adobe-Identity-0) fonts the CID-as-Unicode guess is restricted to whitespace (U+0020→ space, which producers routinely omit and is reliably CID == codepoint); any other uncovered CID likewise decodes toU+FFFD. A font with no/ToUnicodestill uses the CID-as-Unicode heuristic exactly as before, and the authoritative embedded-cmap/postlookups are unchanged. This also resolves the opt-in-flag request (#775) by making the detectable-gap behaviour the default rather than a configuration flag. Thanks @schelip for reporting both issues and contributing the fix.
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.
v0.3.68 | Extraction fidelity release — symbolic TrueType character mis-decoding corrected via the `(3,0)`/`(1,0)` cmap, same-row span ordering preserved in plain-text output, JPEG 2000 (`JPXDecode`) image XObjects decoded via OpenJPEG, and RTL Farsi body text recovered from tagged Type0/CID PDFs.
Added
- JPEG 2000 (
JPXDecode) image XObjects decoded via OpenJPEG —render_pagepreviously skipped image XObjects whose stream was compressed with/JPXDecode, silently dropping page content. The OpenJPEG library (viajpeg2k) now decodes them at render time; multi-component images are colour-managed and alpha-composited exactly as other image types. Thanks @potatochipcoconut for the report.
Fixed
- RTL Farsi body text recovered from tagged Type0/CID PDFs (#758) — Type0/CID composite fonts with a valid
/ToUnicodeCMap had ~92% of their body text silently dropped in v0.3.66 on RTL (Farsi) documents. The tagged-structure traversal now correctly assembles CID-encoded spans before the RTL reconstruction pass, recovering the full body. Thanks @Goldziher for the report. - Symbolic TrueType fonts no longer mis-decode characters (#760) — a simple symbolic TrueType font (FontDescriptor
Flagsbit 3, no/Encoding, no/ToUnicode) decoded its content bytes by treating each byte directly as a glyph ID, producing wrong-but-plausible characters (e.g.Ç→Ê,SOLUÇÃO→SOLUÊÃO). The fix parses the embedded font's(3,0)symbol (or(1,0)Macintosh)cmapsubtable into a byte→GID map so the correct byte→GID→Unicode hop is applied; fonts without such a subtable still use the byte as the GID. Thanks @schelip for the report and fix. - Same-row spans no longer reordered or split in plain-text output (#752) — when one logical line was emitted as spans at the same Y in different reading-order groups whose boxes overlapped by a fraction of a point,
to_plain_textinterleaved the overlapping group as a vertical column (hoisting a fragment to the front) and forced a space between the overlapping fragments (splitting a word). A group whose spans share a Y row is now excluded from columnar detection, and the cross-group same-Y space rule is replaced by the standardhas_horizontal_gapthreshold used by the other converters. Thanks @schelip for the report and fix.
Documentation
- macOS/Rust OCR setup guide corrected —
ORT_LIB_LOCATIONis inert with theload-dynamicONNX Runtime feature; the guide now documentsORT_DYLIB_PATH, the variable actually read at runtime.
Dependencies
- pyo3 0.28 → 0.29 — fixes two security vulnerabilities: a missing
Syncbound onPyCFunction::new_closureclosures, and a possible out-of-bounds read inBoundTupleIterator::nth_back/BoundListIterator::nth_back. - phf 0.13 → 0.14, bytes 1.11 → 1.12, log 0.4.32 → 0.4.33, p12-keystore 0.3.0 → 0.3.1.
- GitHub Actions:
actions/checkoutv7.0.0,actions/setup-javav5.3.0,softprops/action-gh-releasev3.0.1,ruby/setup-rubyv1.314.0,taiki-e/install-actionv2.82.2. - Patch/minor updates for
rustls,time,zerocopy,zeroize,wasm-bindgen,wide, and ~35 other transitive crates.
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.
v0.3.67 | Reading-order quality release for untagged scientific papers — manuscript-line-number rails lifted out of the body, dense two-column bodies kept apart at a measured gutter, comma-bearing statistic subscripts rejoined, repeated journal pagination footers suppressed, whole-page placed-PDF article bodies recovered, and single-column pages protected from false column detection.
Fixed
- Whole-page placed-PDF article bodies are no longer dropped — when a publisher places an entire typeset article body inside one Adobe InDesign
/PlacedPDFmarked-content region (e.g. MATEC Web of Conferences), its text is now extracted instead of suppressed. The placed-figure suppression — which removes a decorative draft-galley overlay whose glyphs duplicate logical text living outside the region — is now coverage-aware: a cheap content-stream pre-scan keeps the placed text when it dominates the page and the non-placed text is negligible (the placed region is the content), and suppresses it only when it is a minority overlay. This matches pymupdf/pdftotext, recovers the full body on whole-body-placed pages, and preserves the de-duplication win on decorative-overlay pages. - Single-column pages are no longer scrambled by false column detection — the topological block-reordering pass now requires each side-by-side column to span at least four text rows. A single-column page whose few body lines happen to have a wide mid-line word gap (a sentence space after a period) was previously split into two phantom columns and read out of order; it now stays on the row-aware path and reads correctly.
- Marginalia line-number rails no longer interleave into the body — a narrow, sparse, body-aligned left-margin rail of numerals (manuscript line numbers
118 119 120 …, a folio rail) is now lifted out before geometric column ordering and re-appended at the end of the reading order, instead of being woven into the prose by a flat row-aware sort. Tightly gated (narrow outer band, sparse, ≥3 lines, predominantly numeric, a clear detached gutter, running alongside the body) so genuine narrow first columns, table label columns, and ordinary pages are unaffected. - Dense two-column bodies are kept apart at a measured gutter — the multi-region block builder now refuses to join two spans that straddle the page's measured central gutter, even when a tight-leading over-wide advance makes the cross-gutter gap look like a normal word space. This stops the two columns from fusing into one block (which previously left the page on a row-major interleave) on dense, tightly-led academic two-column pages. Purely subtractive — single-column, grid, and off-centre pages are unchanged.
- Comma-bearing statistic subscripts rejoin to their base — an F-statistic's degrees of freedom (
4,176inF4,176) or a multi-affiliation marker (1,2), which the producer set as a single comma-joined subscript, is now merged back into its base instead of being stranded as separateF/4/176tokens. - Repeated journal pagination footers are suppressed — a constant-text running citation/pagination line (a DOI, a journal
Volume … | Article …reference, or a journal URL host with a digit) that recurs in the top/bottom margin on a strong majority of pages is now treated as furniture and excluded from the body, matching how reference extractors handle running heads and folios (ISO 32000-1 §14.8.2.2). The shape gate is deliberately narrow — government-form control numbers such asOMB No. 1545-0115are kept.
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.
v0.3.66 | Extraction-quality release — multi-region and two-column reading order, right-to-left Arabic/Hebrew reconstruction in Markdown and HTML, soft-wrap de-gluing and de-hyphenation, and a subset-font cipher-encoding fix — plus destructive redaction of Identity-H (composite) text, CCITT Group 3 fax decoding, and a 16-bit-per-component image fix.
Added
- Destructive redaction of Identity-H composite (Type 0) text (#748) — destructive redaction previously refused all Type 0 fonts. It now supports the common Identity-H case (
Type0+Encoding /Identity-H, horizontal writing mode): 2-byte big-endian CIDs are decoded and widths read from/W(ISO 32000-1 §9.7.4.3 / §9.7.5.2), target glyphs are removed, and the engine stays fail-closed — refusing Identity-V, legacy embedded CMaps, and odd-length strings it cannot safely rewrite. Thanks @shota-sh. - CCITT Group 3 (T.4) fax decoding (#738) — extends the in-house CCITT decoder (Group 4 landed in v0.3.65) to Group 3: pure 1-D Modified Huffman (
K = 0) and mixed 1-D/2-D (K > 0), with end-of-line handling and the same partial-row recovery contract as the Group 4 path. Group-3-encoded fax images that previously rendered blank now decode to their real content. Thanks @potatochipcoconut.
Fixed
- 16-bit-per-component images no longer panic on extraction (#750) — extracting an image with
/BitsPerComponent 16hit an internal assertion (an uncatchable panic across the FFI boundary) because 16-bit big-endian samples were stored verbatim into a double-sized buffer. 16-bit samples are now down-converted to 8-bit at parse time, and the PNG encoder rejects a mismatched buffer with a recoverable error instead of asserting. Thanks @shtyker. - Multi-region and two-column reading order — pages with genuine side-by-side regions (two-column bodies, a publisher sidebar beside the body, multi-column newsletters and forms) are now linearised with a topological block order — a precede relation over text blocks — instead of a flat row-aware sort that interleaved columns line by line. Two stacked text lines merged by the same-line Y tolerance are de-interleaved by baseline rather than X-sorted into each other. Self-gating: pages without horizontally-disjoint, vertically-overlapping regions are unchanged.
- Right-to-left Markdown and HTML reconstruction — Arabic and Hebrew now read correctly in Markdown and HTML, not only in plain text. RTL lines are reconstructed at glyph fidelity from the raw spans before the converter orders them; an inter-word punctuation span between two RTL words is kept (instead of being dropped as decoration and gluing the words); a line-final word that sits just below its baseline band is rejoined to its line; and the producer's authoritative inter-word space after a dual-joining letter is preserved.
- Soft-wrap de-gluing and de-hyphenation — generator PDFs that emit one show-string per visual line with no trailing separator no longer glue the last word of a wrapped line to the first word of the next line; a line-break separator is synthesised, since text positioning is purely geometric and a line break encodes no space character (ISO 32000-1 §9.4). The spurious space left after a line-final hyphen on a wrapped line is suppressed so hyphenated words rejoin cleanly.
- Markdown/HTML gutter-crossing word rejoin — a word the producer split into two show-strings across the column gutter (a lone name initial, the two digits of a split number) is rejoined for Markdown and HTML, while genuinely separate adjacent column lines are kept apart and an interior emphasis (bold/italic) span inside a rejoined run is no longer dropped.
- Subset-font cipher encoding no longer overrides the named encoding — a Type1/CFF subset font whose re-indexed built-in encoding disagrees with the producer-declared named
/Encoding(e.g. WinAnsi / MacRoman) on most overlapping codes is detected as a subset cipher and the named encoding is kept, fixing whole-page mojibake on affected documents.
Dependencies
- Bumped
taffy0.10.1 → 0.11.0 (#741),pdfium-render0.9.1 → 0.9.2 (#743),regex1.12.3 → 1.12.4 (#746),brotli8.0.3 → 8.0.4 (#744), andpyo3-log0.13.3 → 0.13.4 (#745); CI bumps forruby/setup-ruby(#742) andtaiki-e/install-action(#740).
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.
v0.3.65 | Multilingual and layout extraction quality — right-to-left bidi reconstruction for Arabic and Hebrew, multi-region reading order for publisher sidebars and two-column academic pages, and CJK/Indic word segmentation — plus an in-house CCITT Group 4 fax decoder that honours `EncodedByteAlign`, structured two-column surfacing, and a batch of O(n²) hot-path removals.
Added
- Two-column structured extraction and tagged-structure surfacing (#734) —
extract_structurednow reports a per-linecolumn_indexfor multi-column pages and, on tagged PDFs, surfaces marginal labels (Lbl→ marginal label) and the nearest enclosing section (Sect/Art/Part→ a document-stablesection_idwith cross-page continuity), per ISO 32000-1 §14.8.4. Additive and zero-risk for untagged input. Thanks @lggcs. - Reading-order threads for linked content (#458) — article-thread (
/Threads→/Bbead) ordering is surfaced so content that flows across columns and pages can be read in author-intended order. - In-house CCITT Group 4 (T.6) fax decoder (#738) — a from-scratch decoder for
CCITTFaxDecodeimages that correctly honoursEncodedByteAlign(ITU-T T.6 2D mode codes, Modified-Huffman run tables, reference-line changing-element walk), with partial-row recovery on truncated streams. Replaces a path that could silently fall back to an all-white image; bilevel fax images now decode to their real content. Thanks @potatochipcoconut.
Fixed
- Right-to-left Arabic/Hebrew text reconstructed in logical order — several classes of RTL extraction defect are corrected so Arabic and Hebrew read correctly instead of scrambled:
- Cross-span cluster reversal (SEG-AR) — producers that draw an Arabic word as interleaved base-glyph and zero-width mark spans (the mark's x falling inside a neighbouring word) had their letters atom-sorted to word edges, scrambling e.g.
الثدييات→ثالدييات. Pure-RTL lines with such zero-width-inside-a-span runs are now collapsed into a single visual-order span — glyphs ordered by x, combining marks bound to their base, word boundaries taken from the producer's own standalone space spans — then reversed to logical order (UAX #9 L2). A representative Arabic page improved from a heavily garbled paragraph to fully correct text. - RTL number preservation (SEG-AR / SEG-HE) — Arabic-Indic and Latin digit runs embedded in RTL text are no longer reversed:
٤٣٤١now reads١٤٣٤(1434) and a Hebrewל ,2009-now readsל-2009,, matching a conformant bidi reorder. - Glyph-advance preservation when merging scrambled-RTL spans — merging adjacent RTL spans no longer corrupts true glyph positions, and a real word break bordering non-cursive punctuation is kept (rather than suppressed as a cursive-shatter space) on
/ReversedCharsproducers.
- Cross-span cluster reversal (SEG-AR) — producers that draw an Arabic word as interleaved base-glyph and zero-width mark spans (the mark's x falling inside a neighbouring word) had their letters atom-sorted to word edges, scrambling e.g.
- Multi-region reading order for publisher sidebars and two-column pages — narrow publisher-metadata sidebars are now segregated from the body and emitted after it (title and body merged top-to-bottom, sidebar last) instead of being interleaved, across text, Markdown, and HTML. Bottom-spanning blocks that follow a multi-column region are peeled correctly, numbered-list markers are skipped in rowspan-label reordering, and two-column prose is linearised column-major. A figure Form XObject's
/BBoxclip (ISO 32000-1 §8.10.1) now drops a draft-galley underlay a conformant renderer would clip — gated to figure-sized forms so a full-page content-frame wrapper keeps its body. - CJK and Indic word segmentation — Korean number/counter spacing and line-break rejoining (
1 만년→1만년), and stray spaces before Bengali/Devanagari/Latin punctuation (प्राणी ।→प्राणी।), are corrected. Adobe predefined CIDFont collections decode through the documented CID → Unicode path (ISO 32000-1 §9.3.3).
Changed
- Performance — O(n²) and O(n·m) hot-path removals — drop-cap initial pairing uses a windowed binary search; the rotated-character filter is skipped entirely on unrotated pages; and table filtering, XY-cut, hyphen merging, and word extraction lose their quadratic hot paths. Text/Markdown/HTML output is unchanged by these changes.
- Redundant clip-mask clone dropped in
apply_pending_clip(#654) — the render path no longer clones the clip mask when it is about to be replaced, trimming an allocation per clipped paint. Pixel output is sub-perceptually unchanged. Thanks @RayVR.
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.