Skip to content

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.

@github-actions github-actions released this 14 Jul 03:58
7910d3c

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 to extract_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₁,₀ became P1.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 + 72123456.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 Scanned and routed to pages_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 wrong Scanned verdict is actively harmful. The dominant cause: gather_page_signals and a second text_quality_gate call 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-correct TextLayer verdict. Both call sites now build their word list from extract_words — the same glyph/span clustering extract_text relies on, including the new math-backtrack guard — instead of one token per span.
  • extract_words split single string literals into fragments (modulem|odu|le) via phantom glyph gaps (#811) — TJ-offset space spans (ISO 32000-1 §9.4.4) were created with one char but an empty char_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.
  • PathContent geometry ignored stroke_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, so is_table_primitive() and the line-based table detector missed the whole grid and its text extracted column-major. stroke_width is now CTM-scaled at extraction (§8.4.3.2 — the line width transforms like all other geometry), the new PathContent::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 geometric bbox is unchanged for every other consumer. Also exposed as pdf_oxide_path_get_rendered_bbox (C FFI) and rendered_bbox in 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 /Rotate key) 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, and rotation_degrees was dropped at both TextSpan::to_chars and 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_degrees now flows span → char → Word and is exposed on Word (Rust/serde), PyWord, the WASM word JSON, and pdf_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 JVM TextWord (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 default WordBoundaryMode::Tiebreaker path) never received the confidence-gated geometric direction detector, so it still used the old accumulated_width > 0.0 heuristic — 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 (Tr 3/7) skip the geometric heuristics entirely and trust extraction order as-is.
  • FluentPageBuilder::rich_paragraph drew consecutive TextRuns flush together — TextRun::bold("Text Run 1") + TextRun::normal("Text Run 2") extracted as Text Run 1Text Run 2 (#837) — each run word-wraps and emits its own text, then advances cursor_x by 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 — Comparison over rate extracted as Comparisonrate (#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 TJ positioning 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-split matchedmatch 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 ...
Read more

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.

@github-actions github-actions released this 06 Jul 02:22
9b0f9c9

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 with does 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, WMode 1). sort_spans_vertical_tategaki and its two duplicated call sites (postprocess_spans's tategaki intercept, TategakiStrategy) decided "same column" with a pairwise |a - b| <= tol check on each span's X-center. That check is not transitive: a chain of spans each within tol of its neighbor can span far more than tol end to end, so the comparator can claim A<B, B<C, and C<A all 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 via f64::from_str, which saturates an all-digit literal past that limit to f64::INFINITY rather than erroring. Combined with a degenerate content-stream matrix (a zero CTM/Tm component), 0.0 × Infinity produced 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.

Thanks to @tobocop2 (#807, #808) for finding, root-causing, and fixing this.


Installation

Rust (crates.io)

cargo add pdf_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 05 Jul 06:20
16eba88

Security

  • office_oxide 0.1.2 → 0.1.3 (clears RUSTSEC-2026-0194 / RUSTSEC-2026-0195) — the optional Office-document export path depended on office_oxide 0.1.2, whose transitive quick-xml 0.40 has an unbounded per-xmlns heap allocation in NsReader::push that a crafted DOCX/XLSX/PPTX could use to exhaust memory (a denial-of-service on untrusted input). office_oxide 0.1.3 upgrades to quick-xml 0.41, which bounds the allocation. pdf_oxide's own quick-xml was already 0.41; this bump closes the remaining transitive path so the dependency tree is advisory-clean.

Fixed

  • extract_words / extract_spans / extract_text_lines garbled 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 /Rotate 90/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 (and extract_chars already 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.

Thanks to @ankursri494 (#804) for reporting the issue that drove this release.


Installation

Rust (crates.io)

cargo add pdf_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 04 Jul 18:45
7b1fc92

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 across BT/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 ![](…), /Link annotations 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_lines drifted 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.3 TJ-array kerning, so positions drifted cumulatively along a line (up to tens of points) versus extract_chars. Each glyph's x now comes from the accurate content-stream position (matching extract_chars and Poppler's pdftotext -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 stream in validate_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 /N from 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_tree imposed 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 new parse_structure_tree_with_budget(&doc, Option<Duration>) (and doc.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 with TJ/Td offsets in embedded Type 0 / Identity-H subset fonts (e.g. the 214-page ISO 21111-10 standard), whole runs extracted glued together — All rights reserved came out as Allrightsreserved. The word-gap detector derives its threshold from the font's space-glyph advance, but under Identity-H character code 0x20 maps 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 code 0x20). 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 at 0x20 still use their explicit /W entry. 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_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 02 Jul 19:47
46dbfe7

Added

  • Content-stream order exposed on extracted spans and words (#779)extract_words and extract_text_lines now carry the originating span's sequence (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 accessor pdf_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 inside PLANAL's right edge) was extracted as PLANAL 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, reconstructing PLANALTINA, 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 lines merged 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, and extract_text_lines reported the page's /Resources/Font alias (e.g. F1) rather than the resolved /BaseFont (e.g. Helvetica, CIDFont+F1). They now resolve to the base font, matching extract_chars and pdfminer.six / pdfplumber. (The second part of #780 — per-glyph coordinate drift on CID/Type0 fonts in extract_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_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 27 Jun 06:31
911270a

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 with install/export targets 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/) — .Call C 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).
  • 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 :podspec URL — 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 in docs/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 managementscripts/sync_version.py propagates the canonical Cargo.toml version into every binding manifest and version/parity assert (--check verifies, --set X.Y.Z bumps everything). A Version Consistency CI 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 /ToUnicode CMap 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. a ti ligature → : / D, so notificacaono:ficacao). The glyph has no Unicode anywhere in the file (no /ToUnicode entry, no post name, no GSUB), so the letters are unrecoverable, but substituting a wrong character is silent corruption. When a usable /ToUnicode is present, the GID→AGL guess is now suppressed for all Type0 fonts, and the CID-as-Unicode guess is suppressed for fonts whose CIDSystemInfo ordering is not Identity, so an uncovered CID there decodes to U+FFFD instead. 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 to U+FFFD. A font with no /ToUnicode still uses the CID-as-Unicode heuristic exactly as before, and the authoritative embedded-cmap/post lookups 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_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 24 Jun 12:59
8150a2a

Added

  • JPEG 2000 (JPXDecode) image XObjects decoded via OpenJPEGrender_page previously skipped image XObjects whose stream was compressed with /JPXDecode, silently dropping page content. The OpenJPEG library (via jpeg2k) 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 /ToUnicode CMap 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 Flags bit 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ÇÃOSOLUÊÃO). The fix parses the embedded font's (3,0) symbol (or (1,0) Macintosh) cmap subtable 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_text interleaved 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 standard has_horizontal_gap threshold used by the other converters. Thanks @schelip for the report and fix.

Documentation

  • macOS/Rust OCR setup guide correctedORT_LIB_LOCATION is inert with the load-dynamic ONNX Runtime feature; the guide now documents ORT_DYLIB_PATH, the variable actually read at runtime.

Dependencies

  • pyo3 0.28 → 0.29 — fixes two security vulnerabilities: a missing Sync bound on PyCFunction::new_closure closures, and a possible out-of-bounds read in BoundTupleIterator::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/checkout v7.0.0, actions/setup-java v5.3.0, softprops/action-gh-release v3.0.1, ruby/setup-ruby v1.314.0, taiki-e/install-action v2.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_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 20 Jun 09:13
3fbc9ca

Fixed

  • Whole-page placed-PDF article bodies are no longer dropped — when a publisher places an entire typeset article body inside one Adobe InDesign /PlacedPDF marked-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,176 in F4,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 separate F / 4 / 176 tokens.
  • 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 as OMB No. 1545-0115 are kept.

Installation

Rust (crates.io)

cargo add pdf_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 18 Jun 07:09
43323b0

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 16 hit 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 taffy 0.10.1 → 0.11.0 (#741), pdfium-render 0.9.1 → 0.9.2 (#743), regex 1.12.3 → 1.12.4 (#746), brotli 8.0.3 → 8.0.4 (#744), and pyo3-log 0.13.3 → 0.13.4 (#745); CI bumps for ruby/setup-ruby (#742) and taiki-e/install-action (#740).

Installation

Rust (crates.io)

cargo add pdf_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.

@github-actions github-actions released this 16 Jun 09:53
fd16e75

Added

  • Two-column structured extraction and tagged-structure surfacing (#734)extract_structured now reports a per-line column_index for multi-column pages and, on tagged PDFs, surfaces marginal labels (Lbl → marginal label) and the nearest enclosing section (Sect/Art/Part → a document-stable section_id with 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/B bead) 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 CCITTFaxDecode images that correctly honours EncodedByteAlign (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 /ReversedChars producers.
  • 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 /BBox clip (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_oxide

Python (PyPI)

pip install pdf_oxide

JavaScript/WASM (npm)

npm install pdf-oxide-wasm

CLI (Homebrew)

brew install yfedoseev/tap/pdf-oxide

CLI (Scoop — Windows)

scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxide

CLI (Shell installer)

curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | sh

CLI (cargo-binstall)

cargo binstall pdf_oxide_cli

MCP Server (for AI assistants)

cargo install pdf_oxide_mcp

Pre-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.