v0.3.64 | Composite-CJK page rendering — a bundled Droid Sans fallback now paints embed-less Type 0 fonts and the Adobe predefined CIDFont collections — plus a §11 transparency compositing surface with optional lcms2 colour management, cross-document font-cache correctness, valid annotation appearance streams, and math/CJK text-extraction polish (prime-notation spacing, signed unit exponents, CJK bracket spacing, table-header Markdown).
Added
- Substitute Adobe predefined CIDFonts with a bundled CJK fallback when rendering (#730) — a composite (Type 0) font that references an Adobe predefined character collection (Adobe-Japan1, Adobe-GB1, Adobe-CNS1, Adobe-KR — e.g.
Ryumin-Light,GothicBBB-Medium,STSong-Light,HYSMyeongJo-Medium) without embedding glyph outlines previously painted nothing. ISO 32000-2 §9.7.5.2 requires a conforming reader to ship those collections; the page renderer now maps CID → Unicode (via the bundledcid_mappingstables) → a Droid Sans Fallback glyph, producing a real sans-serif rendering even when the document asked for Mincho / Gothic. Gated behind the opt-incjk-render-fallbackfeature (~3.4 MB), independent ofcjk-form-fonts. - Bundled CJK fallback for embed-less composite fonts on CJK-fontless hosts — the same
cjk-render-fallbackfeature loads Droid Sans Fallback into the page renderer's font database so composite fonts that reference a glyph collection but embed no outlines still paint real glyphs instead of blank on machines with no system CJK font installed. - §11 transparency compositing surface and a pluggable
IccBackendtrait (#674) — adds an ISO 32000-1 §11 transparency surface to the rendering pipeline and anIccBackendcolour-management abstraction with an optionallcms2-backed implementation, alongside the defaultqcmspath.
Fixed
- Cross-document font cache now keys Type0/Identity-H fonts by
/ToUnicodecontent (#733, extends #595, #597, #598) — the cross-document cache hardening in #595/#597/#598 folds the/ToUnicodereference (object id/gen) into the font identity hash and keeps canonical subset fonts (AAAAAA+) out of the shared cache. This extends that coverage to two cases the reference-based key doesn't reach: a non-canonical subset tag such as/CIDFont+F1(emitted by some generators) stays eligible for cross-document sharing, and PDFs produced from a common template reuse the same/ToUnicodeobject number — so two genuinely different fonts that merely share a/BaseFontname produced an identical key. Processed in one long-lived process, a later document was then served an earlier font's parsedFontInfoand its glyphs decoded through the wrong/ToUnicode— a constant-offset cipher (SUMMARY→6800$5<) or control/PUA characters — though each document extracted correctly in isolation. The identity hash now folds the/ToUnicodestream bytes, the embedded/FontFile{,2,3}bytes, the descendant/Subtype, and a stream-form descendant/CIDToGIDMap, so same-named-but-different fonts get distinct keys regardless of subset-tag form or object reuse, while genuinely identical fonts still deduplicate across documents (the cache's purpose is preserved). Same bug class as the/Widthspoisoning fixed in #598. - Watermark annotations rendered as nothing in compliant viewers (#713) — a watermark's
/APappearance was serialized as a stream nested directly inside the annotation dictionary (/AP <</N <<…>> stream … endstream>>). A PDF stream must be an indirect object (ISO 32000-1:2008 §7.3.8); the inline form is invalid, so spec-compliant readers (e.g. MuPDF/PyMuPDF) rejected the annotation with "invalid key in dict" and the watermark never appeared — even though the bytes were present in the file. A sharedhoist_appearance_streamshelper now lifts nested/N,/D, and/Rappearance streams (including named-state sub-dictionaries) into freshly allocated indirect objects and replaces the slot with a reference, applied on both theDocumentBuilderwriter and the existing-pageDocumentEditor::save_pagepaths. Verified end-to-end with MuPDF: the watermark now parses and renders on both paths. - Fixed Python type stubs leaking the pyo3
Py<Self>receiver as a positional parameter (#728) — methods implemented in Rust with a by-value receiver (fn page(slf_handle: Py<Self>, …)— the idiom pyo3 uses to hand a method an owned handle to its own instance) were emitted by the rylai stub generator with that receiver re-exposed alongside the injectedself. - Bookmark titles encoded in UTF-16BE/LE decoded incorrectly (#729) — PDF outline
/Titlestrings may be UTF-16BE/LE with a leading BOM or PDFDocEncoding; decoding now routes throughoptional_content::decode_pdf_text_stringso non-Latin bookmark labels read correctly. - A damaged ICC profile no longer aborts the OutputIntents scan (#712, #731) — a single broken entry in
/OutputIntentspreviously stopped the whole colour-profile scan; the scan now skips the damaged profile, logs it, and continues so the remaining intents are still read. - Spurious space inside prime-notation numbers (math/astronomy) — a prime's narrow metric advance (
w₀, ISO 32000-1 §9.4.4) is small relative to its inked form, so the geometric word-break heuristic injected a space before a following decimal or period:0′′.28→0′′. 28,Y′.→Y ′ .. Arc-second values and prime-suffixed tokens (0′′.28,1′′.47,δnn′.) now stay intact, while genuine feet-and-inches like5′ 6″are preserved. Surfaced by the py-pdf/benchmarks text-extraction corpus. - Signed unit exponents wrongly rewritten to Unicode subscripts — the super/subscript pass synthesized Unicode sub/superscript glyphs from glyph geometry, overriding the authoritative ToUnicode decode (§9.10) and firing inconsistently on identical occurrences; a scientific unit exponent such as
s−1could becomes₋₁. A run that opens with a minus/hyphen sign and contains a digit is now left as ASCII, matching the plaintext convention every reference extractor follows. - Markdown table header cells no longer force-bolded — emitted table headers (
| **CRN** | **Type** |) dropped the**wrappers so header text matches the source weight (| CRN | Type |). - Spurious spaces between CJK/Hangul text and brackets — a stray space a producer inserted between an ideograph/Hangul syllable and an adjacent bracket (
(Xa, Za) 的→(Xa, Za)的,消灯 (0%)→消灯(0%),관찰되었다 (Fig. 3)→관찰되었다(Fig. 3)) is dropped; full-width CJK brackets are left alone. - Line-end hyphen joins kept space-free — a word split across a line break by a hyphen no longer gains a stray space at the join.
Changed
- Dependencies / CI —
taiki-e/install-action2.81.8 → 2.81.9 (#717); CI hardening for single-OS flake isolation and network-setup retries (macOS Homebrew, crates.io publish-check). The page renderer is now exercised by a dedicated CI tier (rendering,test-support,cjk-render-fallback), which previously compiled to zero tests under the default feature set.
Thanks
- RayVR (Ray V. Roberts) — contributed bundled-fallback substitution of Adobe predefined CIDFonts and the §11 transparency compositing surface with the optional
lcms2ICC backend (#730, #674). - @regularkevvv (Kevin Castro) — reported and fixed the cross-document font-cache
/ToUnicodekey collision that could serve one document's font to another in a long-lived process (#733). - Norbert Preining — fixed invalid inline annotation
/APappearance streams, the leaked pyo3Py<Self>receiver parameter in the generated.pyi, and UTF-16BE/LE bookmark-title decoding (#713, #728, #729). - Mike Beschastnov — hardened the OutputIntents scan to skip a damaged ICC profile instead of aborting (#712, #731).
Installation
Rust (crates.io)
cargo add pdf_oxidePython (PyPI)
pip install pdf_oxideJavaScript/WASM (npm)
npm install pdf-oxide-wasmCLI (Homebrew)
brew install yfedoseev/tap/pdf-oxideCLI (Scoop — Windows)
scoop bucket add pdf-oxide https://github.com/yfedoseev/scoop-pdf-oxide
scoop install pdf-oxideCLI (Shell installer)
curl -fsSL https://raw.githubusercontent.com/yfedoseev/pdf_oxide/main/install.sh | shCLI (cargo-binstall)
cargo binstall pdf_oxide_cliMCP Server (for AI assistants)
cargo install pdf_oxide_mcpPre-built Binaries
Download archives for Linux, macOS, and Windows from the assets below. Each archive includes both pdf-oxide (CLI) and pdf-oxide-mcp (MCP server).
Platform Support
| Platform | Architecture | Archive |
|---|---|---|
| Linux | x86_64 (glibc) | pdf_oxide-linux-x86_64-*.tar.gz |
| Linux | x86_64 (musl) | pdf_oxide-linux-x86_64-musl-*.tar.gz |
| Linux | ARM64 | pdf_oxide-linux-aarch64-*.tar.gz |
| macOS | x86_64 (Intel) | pdf_oxide-macos-x86_64-*.tar.gz |
| macOS | ARM64 (Apple Silicon) | pdf_oxide-macos-aarch64-*.tar.gz |
| Windows | x86_64 | pdf_oxide-windows-x86_64-*.zip |
Changelog
See CHANGELOG.md for full details.