v0.3.77 | Search-index control lands in every first-party binding: `prepare_search()`/`clear_search_index()` (added to the Rust core in 0.3.76 alongside the new per-page search-index cache) can now be called from Python, JavaScript/WASM, Java/Kotlin/
prepare_search()/clear_search_index()exposed across every language binding — callers can now build the search-index cache at a controlled point instead of paying for it on the firstsearch()call, and free it before heavy extraction on the same document object, from any binding, not just Rust. Also fills in a pre-existing gap in the PHP binding, which had no publicsearch()method at all (#952).include_artifactsoption onextract_text()/to_markdown()/to_markdown_all()/to_plain_text()/to_plain_text_all()(Python;ConversionOptions.include_artifactsin Rust) — matches theinclude_artifactsparameterextract_words()/extract_text_lines()already had, defaulttrue.
extract_text(),to_markdown()/to_markdown_all(), andto_plain_text()unconditionally dropped content tagged/Artifact(ISO 32000-1:2008 §14.8.2.2.1 — running headers/footers, page numbers, watermarks), with no override, unlikeextract_words()/extract_text_lines()which already defaulted to including artifact-tagged content for backward compatibility. On documents that tag a repeated footer carrying real information (e.g. a section identifier on every page of an engineering spec) as an artifact, this silently dropped that content from the vast majority of pages. All five methods now default to including artifact-tagged content, withinclude_artifacts=Falseavailable for the spec-correct exclusion behavior (#954).
Issues reported by:
- @ankursri494 — #952 (
prepare_search()/clear_search_index()missing from every binding but Rust) - @tealtonyplanhub — #954 (
extract_text()silently drops/Artifact-tagged content with no override)
Thank you!
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 | 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 |
See CHANGELOG.md for full details.
v0.3.76 | Redaction/editor persistence and rendering-accuracy release: DOM edits and `add_text()` overlays on source-loaded pages now survive `save()` without leaving dangling `/Contents` references; multi-input (DeviceN) Type 0 tint transforms, non-
- PDF/A conversion exposed in the Java binding, with idiomatic Kotlin/Scala/Clojure facades (
PdfAConverter,ConversionResult/ConversionAction/ActionType/ConversionErrorinfyi.oxide.pdf.compliance) — completes PDF/A conversion coverage across every first-party binding (#948).
Editor / redaction persistence
PdfDocument.save()silently discarded DOM edits (set_text(),remove_element(),erase_header()/erase_footer()/erase_artifacts()) made to a page loaded from an existing PDF — the overlay-merge path that reconciles DOM edits back into a page's/Contentson save had a defect that dropped the edit entirely; edits made via the DOM API now persist throughsave()/save_page()(#940).- A page whose original
/Contentswas already a multi-entry array (ISO 32000-1 §7.7.3.3) kept a dangling reference to its other original streams after destructive redaction — the merge step that replaces/Contentswith the redacted stream assumed a single original stream at array position 0; it now replaces every original content reference, keeping only genuine overlay/addition streams. The same defect class was independently hit through two different triggers:set_text()/remove_element()on a page whose/Contentswas already an array, andadd_text()combined withapply_redactions_destructive()on the same page (#940, #941, #799). add_text()overlay font registered under the raw font name while the content stream emitted themap_font_name()-transformed name, leaving a dangling/Tfresource for bold/italic overlays, generic family names (Arial,sans-serif), and Symbol/ZapfDingbats — registration now keys off the exact name theTfoperator emits, and/Resources//Resources/Fontare resolved through indirect references (common for pages loaded from existing documents) (#941).save_page()discarded overlay text staged by a priorsave_page()call on the same page —overlay_additionsnow accumulates across calls instead of being overwritten (#941).
Rendering
- CCITT Group 3/4
/ImageMaskXObjects were treated as raw stencil rows instead of decoded — compressed fax data is now actually decompressed;DecodeParms/filter-chain handling,K < 0Group 4 semantics, and allocation on oversized/malformed input are also corrected (#935, #939). - Separation/DeviceN colour spaces with a genuinely multi-channel (N>1) Type 0 (sampled) tint transform rendered black or dropped shapes entirely — the sampled-function evaluator only ever handled a single input dimension, forwarding just the first
scnoperand and silently falling back togray = 1 − components[0]; it now performs full N-dimensional multilinear interpolation across the sample grid (ISO 32000-1 §7.10.2's general algorithm, of which the prior 1-D case is the N=1 special case), gated by aMAX_SAMPLED_FUNCTION_DIMS = 8bound against a pathological/Sizearray (#849, #859). - Non-CCITT 1-bit
/DeviceGrayimages (uncompressed orFlateDecode) were force-fed through the CCITT decompressor and silently dropped when decompression failed — the CCITT path is now gated on the XObject's filter actually beingCCITTFaxDecode; the non-CCITT case is unpacked directly, folding/Decode [1 0]inversion the same way the CCITT path already does (#860). - Inline images (
BI…ID…EI) were parsed and classified as a paint operator but never actually painted — the renderer's operator dispatch had no match arm forOperator::InlineImage; it now expands the abbreviated dictionary keys and routes through the samerender_image/render_image_maskpath used forDo-invoked image XObjects (#860). - Spatial table-cell ownership misattributed text near cell boundaries — stale per-glyph offsets outside a text span's bounding box are now rejected for singleton spans, and exact half-open internal grid intervals keep superscripts and boundary-adjacent text in their correct geometric cell; outer-edge tolerance near the table boundary is unchanged (#937, #938).
- Repeated
search()/search_page()calls on the same document re-extracted and re-postprocessed every page's full spans on every call — a multi-pattern scan over one document costO(searches × full extraction)with no benefit from repetition, since the existing span cache is bounded to 8 entries. An unbounded, lighter-weight per-page search index (page text + span bounding boxes only, no font/glyph data) is now built lazily on first use and reused across calls;prepare_search()/clear_search_index()give callers control over eager population and memory reclamation. Measured: repeatsearch()calls on a 60-page document dropped from ~30ms to ~20-50µs per call after the first (#936).
- Test/CI stability: two
ocr-feature-gated CCITT diagnostic test files still calledfax0.2'su16decode_g4signature after the crate's 0.3 API bump (u32), breakingcargo clippy --all-targets --features rendering,barcodes,signatures,ocr— exactly the combo CONTRIBUTING.md's recommended pre-commit hook uses (#945).
Rendering fixes from @Goldziher — CCITT /ImageMask decoding (#935, #939) and spatial table-cell ownership (#937, #938). Redaction/overlay persistence from @thomnico — the add_text()/destructive-redaction interaction (#941, #799, superseding an earlier iteration). Thank you!
Issues reported by:
- @lightedlogic — #940 (
save()doesn't persist DOM edits made viaset_text()/remove_element()) - @ankursri494 — #936 (repeated
search()calls on the same document don't get faster) - @ultrasaurus — #945 (recommended pre-commit hook fails on
main) - @bfchiheb — #947 (PDF/A conversion missing from the Java binding)
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 | 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 |
See CHANGELOG.md for full details.
v0.3.75 | Rendering-accuracy and extraction-fidelity release. DeviceCMYK now renders through measured process inks; page rotation (including `/Rotate 270` and negative values) and Separation/DeviceN tint transforms are honoured; inline images and CMY
ReadingOrder::Structure— order a Tagged PDF by a pre-order traversal of its/StructTreeRoot(ISO 32000-1 §14.8.2.3), fixing table and complex-layout order where a geometric XY-cut guesses; falls back toColumnAwarewhen the structure tree is absent or not trustworthy (#877).- Mapping provenance —
extract_spansreports which ISO 32000-1 §9.10.2 tier (ToUnicode / encoding / heuristic) produced each span's text, surfaced across all bindings (#893). extract_spans_filtered_with_reading_order— reading-order extraction combined with optional-content (OCG) and ink-coverage filtering (#883).- Type 0 (sampled) and Type 3 (stitching) tint-transform evaluation in the renderer for Separation/DeviceN colours (#849).
- CJK vertical-writing running header/footer detection — recognizes folios in the left/right side bands on tategaki pages (#889).
resolve_named_destinationis now public (#881); aremove_artifactsmarkdown-conversion example (#845).
Rendering
- DeviceCMYK rendered via the naive
1−(C+K)additive clamp — now converts through measured SWOP-style process inks across every composite / vector / text / image path, so0 0 0 1 kblack renders#231F20and process cyan renders#00ADEFinstead of over-saturated additive values (#861). /Rotate 270rendered MIRRORED rather than rotated; negative/Rotatevalues were mishandled (%instead ofrem_euclid); real/Rotateand indirect Separation alternates are now resolved correctly (#862, #848, #854).
Text extraction
- Form-XObject (
Do) text went missing when stray operands preceded the name — a dropped/malformedcmleft dangling numeric operands, soDoreadoperands[0](a stray number) and resolved to an empty name; it now reads the operand immediately preceding the operator per ISO 32000-1 §7.8.2 (#914). - Pages were lost when
/Count-based page counting returned 0 — now recovered by walking the page tree (#909). - Inline images (
BI/ID/EI) were parsed but never decoded —/Subtypeis now supplied and the Table 92 abbreviated keys/values expanded (#863). - Unique
/PlacedPDFbodies are kept instead of suppressed (#896); spans entirely outside the MediaBox are dropped fromextract_spans_with_reading_order(#894); the top-level fill colour set beforeBTis preserved (scn/cs/rgno longer dropped) (#857). - Running-header/footer detection now requires position-consistency before removing repeated text as chrome, and recognizes non-Latin folio digits (#888, #887).
Images
/Decode [1 0]is honoured for 1-bit CCITT/DeviceGray images (#856); the jpeg-decoder Adobe inversion is undone for CMYK JPEGs (#855).
Recovery / parsing
- A truncated file that lost its own Catalog is recovered by rebuilding one from the surviving pages (#890); a file padded after
%%EOFis no longer rejected outright as 0 pages (#875).
Fonts
- The referenced
/Encoding /Differencesis folded into the font identity hash, and subset choice inextract_embedded_fontsis made deterministic (#878, #853).
Writer
- Coloured text on a registered embedded font rendered black —
FluentPageBuilder::inline_color(and anyTextStyle.color) was silently dropped for embedded fonts —PdfWriter::add_elementrouted embedded-font text through the deliberately colour-agnosticadd_embedded_text(the HTML painter sets and resets the fill colour around its own calls) without ever emitting the element's fill colour, so norgoperator reached the content stream and the glyphs painted in whatever fill colour was last set (default black). The base-14 path (add_text_content) always emitsrgfromstyle.color; the embedded path now matches it by emittingfill_colorbefore the glyph run. No restore is needed — every text element sets its own colour, mirroring the base-14 branch's "always set explicitly" contract.
extract_words/extract_textspent most of their time re-deriving per-glyph facts that cannot change (#882) — text extraction asked each font for its weight and slant once per glyph, inside the show-text loop. Both answers are name-derived: the weight lowercases the base font name (allocating) and runs up to a dozen substring searches, and the slant lowercases it again for two more — so a 13,234-page document repeated ~14 substring scans and 2 allocations 48.7M times for a value fixed at font-load. A sampling profile putstr::containsand friends at ~38% of all samples. The Standard-14 width lookup had the same shape, re-stripping the subset prefix and re-scanning the 15-name table per glyph purely to choose a width table. Both are now resolved once per font and memoized, mirroring the existing byte-width-table memo. Alongside:postprocess_spansrescanned every glyph on the page per span to find its baseline (O(spans × chars) — now a bracketed y-sorted index); the page's characters were re-parsed for span post-processing and re-copied on every access (now cached and shared); the word and line paths materialized every glyph twice; article threads — a document-wide parse that walks the whole page tree — were re-parsed per page; the glyph dedup rebuilt the whole array to drop a handful; and the word-merge loop re-derived RTL-ness from an accumulating buffer, costing O(k²) characters per merge chain (the exact blow-up the backtrack guard above it exists to prevent). Measured on the reporter's PDFs:extract_words156.9s → ~121s andextract_spans88.7s → ~54s on a 13,234-page document;extract_text6.03s → 3.94s on a 2,124-page one. Output is byte-identical — verified across a 419-PDF corpus forextract_spans/extract_chars/extract_words/extract_text_lines(including geometry and per-glyph x-offsets) and for text/markdown/HTML.
- Text shaping migrated from
rustybuzztoharfrust(#899);fax0.2 → 0.3 (#873); thettf-parsermigration decision for RUSTSEC-2026-0192 is documented (#900). office_oxidebumped to 0.1.8 (#904, #932). A combined dependency roundup (crates + CI actions + Go), plus routine crate and CI-action bumps (#931, #907, #898, #872, #869, #870, #871, #864, #865, #866, #867, #868, #874, #891).- Test/CI stability: the flaky
structured_warningsround-trip test is fixed (#912); misc test guards and binding-format fixes (#846, #897, #880).
The majority of this release was contributed by @ajbufort — rendering (/Rotate handling and Separation/DeviceN tint transforms #848, #849, #854, #862; DeviceCMYK process inks #861), image decode (#855, #856), text extraction (#857, #863, #894, #896, #909), fonts (#853, #878), recovery/parsing (#875, #890), and reading-order / span filtering (#877, #881, #883). Additional fixes from @norbusan (#911) and @ultrasaurus (#845, #846). Thank you!
Issues reported by:
- @ankursri494 — #882 (
extract_words/extract_textfar slower thanextract_spanson large PDFs) - @tobocop2 — #876 (signal to callers when a page's text cannot be extracted)
- @ultrasaurus — #879 (
remove_footersremoved real content on IRS forms) - @norbusan — #913 (text inside a Form XObject missing from extraction)
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 | 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 |
See CHANGELOG.md for full details.
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 fus
- 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 a different font run (romansolution→ math-italicU) was wrongly glued intosolutionU; drop-caps and small-caps initials — the glue's real target — sit tight against their word at ~0 em, so 0.12 em keeps them while releasing genuine word→variable boundaries (poppler and PDFium keep the space). - Condensed headings and tracked runs typeset with no space glyph fused adjacent words —
conformance test plans→conformancetestplans(#847) — a bold heading or a running header whose word separation is pureTd/TJpositioning (no0x20glyph) opens inter-word gaps of only ~0.18 em, below the intra-word kerning guard (0.75× the space-glyph advance), so the words glued. Because it now runs on the accurate per-glyph advance above, the gap distribution reflects the real render rather than the old undershoot. A fixed magnitude can't separate a 0.18 em word gap from ~0.15 em kerning — but within one line the intra-word glyph gaps cluster near zero while the inter-word gaps form a distinct larger cluster. A per-line multi-level bimodal split of the gap distribution now pins the word boundary regardless of absolute magnitude — splitting at every gap level above the intra-word cluster, so a condensed running footer (© ISO 2021 – All rights reserved) recovers its ~0.10 em word gaps too, matching the advance-aware extractors (pdfminer, poppler, Adobe Acrobat) that pymupdf/pdfplumber miss. It only ever adds a space, only when the suppression came from the geometric kerning guard (a newSpaceSource::IntraWordKerningmarker) — never the semantic no-space rules, so complex-script text (Devanagari, Bengali, …), CJK, ligatures, and RTL are untouched. Two guards keep it off dense math, whose sub/superscript gaps are the same ~0.10 em magnitude: it never fires across a super/subscript baseline shift, nor when another glyph's ink occupies the gap (a subscript drawn between a variable and the next symbol,λᵢr→ keepsλᵢr, neverλ i r). 419-PDF sweep: glyph-preserving spacing gains on headings/footers/condensed runs, zero fusions, zero over-segmentation of math or complex scripts. (A word boundary whose two glyphs overlap — negative advance, e.g. therights reservedseam — carries no geometric signal and is recovered by no extractor, Adobe included.)
- Bumped
crossbeam-epochto 0.9.20 (RUSTSEC-2026-0204) (#827).
- Pinned the Go toolchain to 1.26.5 in CI (GO-2026-5856) (#834).
- Consolidated the July 2026 Dependabot cargo + github-actions updates (#835).
- Added tests that
remove_footerspreserves body content (#800). - Fixed the broken
--all-featurestest commands in the PR template and dev guide (#838). - Bumped
office_oxideto 0.1.6.
Community fixes merged this release:
- @tobocop2 — reported and submitted the fixes for the fragmented-word, stroke-encoded table-rule, and rotated-page bugs (#811, #812, #813 → #814), the subscript-decimal bug (#816 → #817), and the displayed-math relation-sign fusion (#830 → #831); also reported the word-layer math fusion (#836) and the born-digital misclassification (#840). A standout contribution across the whole release.
- @ultrasaurus (Sarah Allen) — contributed the
remove_footerscontent-preservation tests (#800).
Issues reported by:
- @tobocop2 — #811, #812, #813, #816, #830, #836, #840
- @RubberDuckShobe — #837 (
rich_paragraphrun spacing) - @palmoni5 — #826 (Hebrew OCR-sandwich reversal)
- @Goldziher (Na'aman Hirschfeld) — #847 (word fusion on positioned runs)
Thank you all — reporters and fixers alike.
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 | 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 |
See CHANGELOG.md for full details.
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.
-
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.
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 | 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 |
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.
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.
-
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.
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 | 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 |
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
- 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.
- 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.
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 | 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 |
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 spa
- 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.
- 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.
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 | 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 |
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 regressio
- 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.
- 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.
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 | 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 |
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
- 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.
- 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.
- 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.
- 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.
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 | 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 |
See CHANGELOG.md for full details.