Releases: bzsanti/oxidize-python
Release list
v0.16.0
Release v0.16.0
Summary
Exposes the advanced shading surface added in oxidize-pdf core 4.1.0
(upstream #407) and bumps the bundled core from =4.1.0 to =4.2.0. Three
new classes — GouraudVertex, FreeFormGouraudShading, ConicShading — and
two new Page methods — add_mesh_shading, add_conic_shading — bring
Type 4 free-form Gouraud triangle meshes and exact conic (angular) gradients
to Python. The core bump additionally ships text-extraction and RAG-chunking
correctness fixes that reach existing callers with no code change.
Added — Type 4 free-form Gouraud mesh shadings (core 4.1.0, #407)
FreeFormGouraudShading emits a Type 4 mesh as a PDF stream per
ISO 32000-1 §8.7.4.5.5: the shading dictionary plus byte-aligned packed
vertex data.
from oxidize_pdf import Color, Document, FreeFormGouraudShading, GouraudVertex, Page
mesh = FreeFormGouraudShading(
"Mesh1",
"DeviceRGB",
[0.0, 100.0, 0.0, 100.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0], # Decode
[
GouraudVertex(0, 10.0, 20.0, Color.rgb(1.0, 0.0, 0.0)),
GouraudVertex(1, 30.0, 40.0, Color.rgb(0.0, 1.0, 0.0)),
GouraudVertex(1, 50.0, 60.0, Color.rgb(0.0, 0.0, 1.0)),
],
)
page = Page.a4()
page.add_mesh_shading("Mesh1", mesh)
page.paint_shading("Mesh1")GouraudVertex(flag, x, y, color)— edge flag 0 starts a new triangle;
1/2 share an edge with the previous one. Out-of-range flags raise
OverflowErrorat construction (u8) orPdfErrorat validation (>2).- Default bit widths are 16-bit coordinates, 8-bit components, 8-bit flags;
override withwith_bits(bits_per_coordinate, bits_per_component, bits_per_flag). validate()enforces the Type 4 constraints (permitted bit widths,
Decodelength/ordering for the colour space, edge flags, DeviceGray
meshes requiring gray vertex colours).Page.add_mesh_shadingvalidates
both the resource name and the mesh before registering.
Added — exact conic (angular) gradients (core 4.1.0, #407)
ConicShading emits a Type 1 function-based shading whose /Function is a
real Type 4 PostScript calculator (angle around a centre → colour ramp), so
conic gradients are resolution-independent rather than a mesh approximation.
from oxidize_pdf import Color, ColorStop, ConicShading, Page, ShadingPoint
conic = ConicShading(
"Cone1",
ShadingPoint(50.0, 50.0),
[0.0, 100.0, 0.0, 100.0], # domain
[ColorStop(0.0, Color.red()), ColorStop(1.0, Color.blue())],
)
page = Page.a4()
page.add_conic_shading("Cone1", conic)
page.paint_shading("Cone1")- Stops must be strictly ascending and (with 2+ stops) span [0.0, 1.0];
validate()andPage.add_conic_shadingenforce this. with_matrix([a, b, c, d, e, f])sets the shading-to-target transform.
Both shading kinds coexist with the existing axial/radial gradients in the
same /Resources/Shading dictionary and are painted with the pre-existing
paint_shading(name). The resource key is the name argument of the
add_*_shading call; the shading's internal name is descriptive metadata
and never reaches the emitted PDF.
Changed — core bumped to 4.2.0
Correctness fixes from core 4.2.0 that reach Python callers directly:
- A lone space decoded from
/ToUnicodewas discarded as garbage
(core #438). The extractor fell back to the raw code and emitted the byte
as a literal ASCII character, corrupting word boundaries in subsetted-font
documents. Whitespace-only decodes are now accepted; the shared predicate
also stops the CID path accepting C1 control codes as genuine text. - Chunk budgets were decided on a sum of per-element token counts
(core #435). Under a BPE counter a chunk could be approved on a cost that
was never measured and exceedmax_tokens. Every budget decision now
measures the text it is about to emit; oversize fragments are flagged
rather than passed off as within budget. - Paragraphs ran together across a font change in extraction
(core #436). A change of font size or weight now ends the paragraph, so a
heading set in the same block as its body no longer merges into one run.
Compatibility
- No breaking changes. All additions are new classes/methods.
- Wheels remain
cp310-abi3(Python 3.10+). - MSRV unchanged (1.88).
Verification
- 38 new tests asserting real PDF bytes:
/ShadingType 4dictionaries with
packed vertex bytes cross-checked against core's byte-aligned pack fixture,
/ShadingType 1with the deterministic PostScriptatanprologue,sh
operators in the decoded content stream, resource-key/internal-name
separation, and every corevalidate()precondition as an error path. - Full suite: 2210 passed.
mypyclean.cargo clippy --all-targets -- -D warningsclean.
v0.15.1
Release v0.15.1
Summary
Bumps the bundled oxidize-pdf core from =4.0.0 to =4.1.0. This is a
core-only release: no Python API changes, no new bindings. It ships three
correctness fixes and one security patch that reach existing Python callers
without any code change on their side.
Two of the fixes are silent-failure bugs — they returned plausible-looking
wrong output rather than raising — so upgrading is recommended for anyone
extracting text from third-party PDFs.
Fixed — page trees with indirect /Kids or /Count (core #415)
A /Pages node storing /Kids or /Count as an indirect reference
(N G R) instead of inline is spec-legal per ISO 32000-1 §7.3.10 and is
emitted by iText. On those documents PdfReader.page_count returned 0 and
PdfReader.extract_text() returned empty text — with no error raised. One
level of indirection now resolves consistently across the page-tree flat
index, page_count(), and PDF/A page-walk validation.
Fixed — LZW EarlyChange code-width boundary (core #415)
The LZW decoder widened the code one entry too late (2^width instead of
2^width - 1 under the default EarlyChange=1), desyncing streams that grew
past 511/1023/2047 dictionary entries and failing with invalid code.
Corrected per ISO 32000-1 §7.4.4.2.
Fixed — column detection no longer shreds tokens (core #422, #417)
Affects the opt-in ExtractionOptions(detect_columns=True) path only; the
default extraction path was never affected.
- Column blocks now require their wide gaps to align horizontally across rows.
Previously, normal-leading lines that each happened to contain a wide gap at
a different X were merged into a false column block and reordered
column-major, which split tokens apart — CNPJ identifiers in label/value
forms were a reported case (#422). - Line grouping is now head-anchored, and a multi-line column block only forms
when its rows are at least one line height apart. Previously a fixed
newline_thresholdband keyed to the previous fragment merged tight-leading
prose into one pseudo-line and then reordered it as a table (#417).
Real tables and genuine multi-column layouts are unaffected.
Security — quick-xml DoS advisories (core #416)
The core bumped quick-xml 0.39 → 0.41, clearing RUSTSEC-2026-0194 and
RUSTSEC-2026-0195: a quadratic duplicate-attribute check and an unbounded
namespace-declaration allocation, both reachable through untrusted XMP
metadata in a PDF. The unused serialize (serde) feature was dropped; XMP
parsing uses only the streaming pull-parser.
Not included
Core 4.1.0 also added ConicShading and FreeFormGouraudShading /
GouraudVertex with Page::add_mesh_shading / add_conic_shading. These are
not exposed in the Python bridge yet — it still binds AxialShading and
RadialShading only. Wrapping the new shading types is deferred to a future
minor release.
Compatibility
No breaking changes. No Python API additions or removals. Wheels remain
cp310-abi3 (Python 3.10+).
v0.15.0
v0.14.0
Release v0.14.0
Summary
Hardens the built-in MCP server against denial-of-service from a single large
or malicious PDF (issue #115), and bumps the bundled oxidize-pdf core from
=3.0.1 to =3.0.4. The core bump makes PdfDocument Send, which lets the
bridge release the GIL during heavy PDF work — so a multi-client MCP deployment
now runs concurrent operations in parallel instead of serializing on one core.
Also documents first-class use from GitHub Copilot and the OpenAI Agents
SDK.
No breaking change to the existing Python API. All new limits are configurable
and ship with generous defaults.
Added — MCP resource caps (issue #115)
Configurable limits, enforced before any heavy work, returning an error with
code RESOURCE_LIMIT:
OXIDIZE_MAX_PAGES(default 10000) — documents with more pages are rejected
before extraction begins.OXIDIZE_MAX_OUTPUT_BYTES(default 10 MB) — caps the serialized size of a
tool's JSON response.OXIDIZE_MAX_SESSION_BYTES(default 10 MB) — bounds the content a single
stateful PDF-creation session may accumulate.
The page-count gate and output cap apply to extract_text, read_pdf,
extract_entities, and convert_pdf. get_session_store now honours
OXIDIZE_MAX_SESSIONS (previously a hardcoded 100).
Added — GIL release for concurrent PDF work (issue #115)
Heavy Rust operations now run inside Python::detach, releasing the GIL so
concurrent MCP calls execute in parallel:
- Standalone path/bytes ops:
validate_pdf,compare_pdfs,
detect_pdf_corruption,PdfAValidator.validate_bytes,split_pdf,
merge_pdfs. PdfReaderextract/chunk methods:extract_text,extract_text_from_page,
extract_text_chunks,metadata,get_page,to_markdown,to_contextual,
chunk,chunk_page,partition,rag_chunks(and the_with_profile/
_with_source/_with_source_and_configvariants),
extract_text_with_options,extract_fragments_with_options,
extract_fragments_from_page,extract_plain_text,
extract_plain_text_lines,get_page_content_streams.
rag_chunks_with_pipeline is intentionally left GIL-held (it runs a
user-supplied analysis pipeline that may re-enter Python).
Added — Copilot & OpenAI Agents SDK integration
- README:
.vscode/mcp.jsonconfiguration for GitHub Copilot agent mode and an
OpenAI Agents SDK (MCPServerStdio) snippet. examples/openai_agents_quickstart.py— runnable example that connects to the
oxidize-mcpserver over stdio and exposes the 12 tools to an agent.
Changed — upstream bump to oxidize-pdf 3.0.4
Picks up the core change that makes PdfDocument Send
(Rc<ResourceManager> → Arc, RefCell → Mutex), required to release the
GIL in the reader methods.
Security
Mitigates MCP DoS (premortem scenario): a crafted PDF (huge page tree,
unbounded extraction output, or session-content flood) is now rejected at the
gate or bounded, and GIL release prevents one heavy request from freezing all
concurrent sessions.
v0.13.0
Release v0.13.0
Summary
Minor release that bumps the bundled oxidize-pdf core from =2.16.3 to
=3.0.1 and surfaces the new upstream CID-keyed positioned-glyph-run write
path (issue #358) into the Python bridge. This lets callers draw a pre-shaped
glyph run — addressing glyphs directly by id, with per-glyph kerning and offset
— while keeping the result extractable as text.
No breaking change to the existing Python API: the upstream 3.0.0 breaking
changes touch only low-level font modules the bridge never used.
Added — CID-keyed positioned glyph runs (issue #358)
A pre-shaped glyph run (e.g. produced by a HarfBuzz-style shaper) can now be
embedded as an Identity-H Type0/CIDFontType2 font where the CID equals the
glyph id, drawn as a TJ array, and accompanied by a ToUnicode CMap so the
text stays searchable/extractable. The embedded font is subset to the used
glyph ids.
New surface:
CidMapping(cid_to_gid=..., cid_to_unicode=..., cid_to_unicode_str=..., max_cid=...)— keyword dicts populate the underlying maps;max_cidis
auto-derived as the largest CID across every map when omitted.
cid_to_unicode_strlets a single CID (e.g. anfiligature glyph)
decompose to several characters in theToUnicodeCMap.CidShowElement(cid, adjust)with.with_x_offset(offset)andcid/
adjust/x_offsetaccessors.adjustis the post-glyph advance kern
(TJconvention);x_offsetdisplaces a glyph without consuming advance
(GPOS mark attachment / diacritics).Document.add_cid_keyed_font(name, data, mapping)— registers a CID-keyed
font on a path kept separate from the Unicode-keyed embedding path. Only
TrueType/SFNT (CIDFontType2) fonts are supported.Page.set_custom_font(name, size)— selects the active custom font for
subsequent drawing (required beforeshow_cid_array).Page.show_cid_array(elements, x, y)— writes the positioned glyph run.
Changed — upstream bump to oxidize-pdf 3.0.1
- 3.0.0 introduces the CID glyph-run API above and a subset-by-used-GIDs
embedding path for it. - 3.0.1 fixes font loss when a merged page's
/Resourcesreferences/Font
indirectly (/Font 1 0 R) rather than inline.
The two upstream 3.0.0 breaking changes (removal of the non-functional
truetype_subsetting glyph subsetter; CidMapping becoming #[non_exhaustive])
do not affect the bridge — neither symbol was used in bridge source.
Fixed — MCP convert_pdf parameter description
The max_tokens parameter description claimed it applied to format='rag', but
the rag path calls rag_chunks() with a fixed internal budget and ignores it.
The description now states max_tokens applies to format='chunks' only. A
characterization test pins that rag output is independent of max_tokens.
Tests
- New
tests/test_issue_358_cid_glyph_run.py: builds a CID-keyed font with
CIDs distinct from their GIDs, draws a positioned run (including anfi
ligature CID), and verifies the end-to-end contract on real bytes —
Type0/CIDFontType2/Identity-Hstructure, the CID codes in theTJ
content stream, and a text-extraction round-trip yieldingfixvia the
ToUnicodeCMap. - New characterization test in
tests/mcp_tests/test_tool_convert_pdf.pyfor
themax_tokens/ragcontract. - Full suite green;
mypyclean;cargo check/cargo fmtclean.
Compatibility
- Python 3.10+,
cp310-abi3wheels (unchanged).
Breaking Changes
None to the Python API.
v0.12.0
Release v0.12.0
Summary
Minor release focused entirely on the bundled MCP server: it raises the
Tool Definition Quality of all 12 tools so AI agents (and Glama's Tool
Definition Quality Score) get precise, self-describing tool definitions. No
change to the PDF library API and no upstream bump (oxidize-pdf stays at
=2.16.3).
Every MCP tool now ships per-parameter descriptions, behavioural annotations,
and a description that states its purpose, when to use it (and the alternative
tool when not), its side effects, and its JSON return shape. Free-form mode
parameters are now typed enums.
Changed — MCP tool definitions
Reworked the definition of every tool (read_pdf, extract_text,
extract_entities, convert_pdf, analyze_pdf, manipulate_pdf,
annotate_pdf, manage_forms, secure_pdf, create_pdf, add_pdf_content,
save_pdf) along six quality dimensions:
- Parameter semantics — every parameter carries an
Annotated[..., Field( description=...)]description: units (PDF points), 0-based page indices,
bottom-left coordinate origin, defaults, and which parameters apply to which
mode. Previously the generated schema had no parameter descriptions. - Behavioural transparency — each tool declares MCP
ToolAnnotations
(title,readOnlyHint,destructiveHint,idempotentHint,
openWorldHint=False), and descriptions now disclose file writes/overwrites,
session mutation, and the JSON shape returned. - Purpose & usage — descriptions distinguish overlapping tools
(extract_textvsconvert_pdfvsextract_entities;read_pdfvs
analyze_pdf) and name the alternative tool for excluded cases. - Contextual completeness — valid modes are enumerated, the
create→add→save session workflow is documented, and honest limitations are
stated (manage_formsreadreturns text runs rather than AcroForm widgets,
validateenforces a required-only rule,filloverlays values;secure_pdf
encryptmay drop non-text elements).
Typed mode parameters (minor behaviour change)
operation, check, content_type, compliance_level, page_size, and
annotation_type are now Literal types, surfaced as JSON-schema enums. An
unknown value is now rejected by schema validation (an MCP ToolError) before
the tool body runs, instead of returning an INVALID_* JSON error body. Valid
inputs are unaffected.
Tests
- New
tests/mcp_tests/test_tool_definition_quality.pyasserts the wire-level
schema contract for every tool (non-tautological parameter descriptions,
annotations with read-only flags, enum-typed mode parameters, sibling
cross-references). - The three existing invalid-mode-value tests were migrated to the
pytest.raises(ToolError)pattern to match the new enum validation. - Full suite green; mypy clean.
Compatibility
- Python 3.10+ — optional tool parameters are declared as
name: Optional[T] = Field(default=None, description=...)rather than
name: Annotated[Optional[T], Field(...)] = None. On Python 3.10 the
latter form drops theFielddescription from the generated schema for
None-typed parameters (reproduced with FastMCP 3.4.2 / pydantic 2.13.4 on
CPython 3.10.20; required and non-None-default parameters are unaffected and
keep theAnnotatedform). Verified that all 12 tools expose descriptions
for every parameter on 3.10, and across the 3.10–3.13 × ubuntu/macos/windows
CI matrix.
Breaking Changes
None to the PDF library API. The only behavioural change is stricter,
schema-level validation of MCP tool mode parameters (described above), which
affects only previously-invalid inputs.
v0.11.0
Release v0.11.0
Summary
Minor release that pulls in upstream oxidize-pdf v2.16.3 and wires its new
experimental Analysis SPI into the Python bridge, enriches RagChunk
metadata, adds document-source stamping, and switches the native extension to a
single abi3 (stable ABI) wheel.
Upstream 2.16.x exposes a Service Provider Interface (SPI) that lets a consumer
plug in custom chunking, classification, and metadata-enrichment logic without
forking the MIT core. This release surfaces that surface in
oxidize_pdf.experimental (semver-exempt, matching the upstream contract), plus
the always-on metadata that the new pipeline produces. It is a non-breaking,
additive change at the stable API level — every previously callable method
preserves its signature.
Upstream
oxidize-pdf=2.15.0→=2.16.3. Pinned exact equality preserved.
Added theunstable-spifeature alongside the existing set (compression, signatures, semantic, language-detection,default-features = false).- 2.16.0 introduced the experimental Analysis SPI and the enriched
RagChunk
metadata. 2.16.1–2.16.3 are bug fixes only (xref-stream double-decode #341,
bounded-memory lenient parse #339, deterministic extraction/XMP #329/#331/#334)
inherited transparently with no bridge API change.
Added
Experimental Analysis SPI (oxidize_pdf.experimental, semver-exempt)
Plug custom analysis logic into RAG chunk generation:
AnalysisPipelinebuilder:with_chunking,with_classifier,with_enricher,
with_source,with_max_tokens.PdfReader.rag_chunks_with_pipeline(pipeline)runs the configured pipeline.- Support types:
ChunkGroup,ClassLabel(compares againststr),
ClassifyContext,EnrichContext,Element(with aclass_labelgetter so a
strategy can read classifier labels),DocumentSource. runtime_checkableProtocolsChunkingStrategy,ElementClassifier,
MetadataEnricherdocument the callback shapes a provider must implement.
The module name signals that this surface may change between releases (same
semver-exempt contract as the upstream unstable-spi feature).
Enriched RagChunk metadata (always on, not SPI)
21 new getters on RagChunk: heading_path, dominant_font[_size],
is_bold/is_italic, min_confidence, content_types (ContentTypeFlags),
char/word/sentence_count, language/language_confidence/language_reliable
(ISO 639-3), chunk_id chain, page_span, page_regions (PageRegion with
ElementBBox), table_rows/table_cols, source (DocumentSource), extra.
Document-source stamping
DocumentSource(filename=..., doc_hash=...).PdfReader.rag_chunks_with_source[_and_config]— auto-fills
title/author/creation_date/total_pages from the document info dict.
Build & Packaging
Single abi3 wheel (stable ABI)
The native extension now builds against PyO3's abi3-py310 (Python limited API):
- One
cp310-abi3wheel per platform covers Python ≥ 3.10 instead of a wheel
per minor version. - Fixes the Windows / Python 3.13 link failure (
LNK1181: cannot open input file 'python313.lib') seen afterwindows-latestrotated to the VS 18 / MSVC
14.51 image whose Python 3.13.13 toolcache omits the version-specific import
library. abi3 links against the always-presentpython3.libforwarder instead. - Verified: full CI matrix (3.10–3.13 × ubuntu/macos/windows) green; 2281 tests pass.
Breaking Changes
None. All stable-API method signatures are preserved. The experimental module is
explicitly semver-exempt.
v0.10.0
Release v0.10.0
Summary
Minor release that pulls in upstream oxidize-pdf v2.15.0 and exposes its new
incremental form-filling capability in the Python bridge.
Upstream 2.15.0 adds IncrementalFormFiller: it fills AcroForm fields on an
already-serialized PDF by appending an ISO 32000-1 §7.5.6 incremental update.
The original bytes are preserved verbatim — only the modified field objects and
the /AcroForm dictionary are rewritten in a new revision (partial cross-
reference section, chained /Prev, regenerated /ID), and a form reader
recovers each field's /V after re-parsing (upstream #318). The bridge already
filled fields at document-construction time (Document.add_text_field and the
form-builder surface), but had no way to fill an existing template PDF read from
disk. This release adds that path.
This is a non-breaking, additive change at the API level. Every previously
callable method preserves its signature; the new class is purely additive. The
two upstream text-extraction fixes (#319) are inherited transparently through the
version bump with no bridge API change: a single malformed content-stream
operator no longer discards a whole page (best-effort recovery), and text drawn
inside a Form XObject invoked with Do is now extracted — 277 files in the
upstream 9051-PDF corpus recover previously-dropped text.
Upstream
oxidize-pdf=2.14.0→=2.15.0. Pinned exact equality preserved; the
feature set is unchanged (compression, signatures, semantic, language-detection,default-features = false).- Upstream 2.15.0 added
IncrementalFormFillerand aPdfReader::trailer()
accessor (#318), and fixed text extraction to be best-effort on malformed
operators and to recurse into Form XObjects (#319).
Added
Incremental form filling (IncrementalFormFiller)
Fill AcroForm fields on an existing PDF without rewriting it:
IncrementalFormFiller(base_bytes: bytes)— wrap the bytes of an
already-serialized PDF (e.g. a form template)..fill(field_name: str, value: str) -> bytes— set one field's/Vand
return the updated PDF (base bytes + appended incremental revision)..fill_many(fields: list[tuple[str, str]]) -> bytes— set several fields in a
single appended revision. Field names are fully qualified
(e.g."address.street"); duplicate names collapse to the last value.
Unknown field names raise (FieldNotFound surfaces the offending name);
malformed base bytes and encrypted documents raise as well.
Behavior change inherited from upstream (#319)
Content-stream parsing is now best-effort. ContentParser.parse and
ContentParser.parse_strict (which share the same tokenizer) no longer raise on
unparseable bytes: they return the operators recovered before the first
unrecoverable byte. Pure garbage yields an empty list; a valid prefix followed by
garbage preserves the valid operators. parse_strict is retained as a
compatible alias of parse — upstream exposes no separate strict mode.
Compatibility
Fully backward compatible. The full suite is 2058 passing (single run,
mcp_tests excluded). New coverage: 7 tests for IncrementalFormFiller (the
verbatim-preservation contract, /V (value) written into the appended revision,
the second cross-reference section, multi-field fill in one revision, and the
unknown-field / malformed-bytes error paths); the content-parser suite was
updated to the best-effort contract (empty list on garbage, valid prefix
preserved before an unrecoverable tail). mypy and
cargo clippy --all-targets -D warnings are clean.
v0.9.0
Release v0.9.0
Summary
Minor release that pulls in upstream oxidize-pdf v2.14.0 and exposes its new
gradient-rendering capability in the Python bridge.
Upstream 2.14.0 makes axial and radial shadings actually render: a shading
now emits a real PDF /Function (Type 2 exponential for two colour stops, Type
3 stitching for more) together with the required /ColorSpace, instead of the
previous placeholder /Function 1 integer with no paint operator (upstream
#297). The bridge already exposed the shading definition types
(AxialShading, RadialShading, ShadingManager), but they were a dead end:
there was no way to attach a shading to a page nor to paint it. This release
adds the missing paint path.
This is a non-breaking, additive change at the API level. Every previously
callable method preserves its signature; the new methods are purely additive.
The text-extraction quality fixes from upstream (word scramble in dense and
multi-column documents #302/#305, the chunk_text infinite-loop guard #308, and
the non-ASCII WinAnsi glyph measurement corrections #309/#313) are inherited
transparently through the version bump with no bridge API change.
Upstream
oxidize-pdf=2.13.0→=2.14.0. Pinned exact equality preserved; the
feature set is unchanged (compression, signatures, semantic, language-detection,default-features = false).- Upstream 2.14.0 added real gradient rendering (#297) and fixed: word scramble
from unresolved indirect/Fontdictionaries and overlapping font-switched
runs (#302, #305), aDocumentChunker::chunk_textinfinite loop (#308), and
measure_text/get_string_widthover-measuring non-ASCII WinAnsi glyphs
(#309, #313).
Added
Gradient rendering (Page)
The canonical bounded-gradient idiom is: register a shading, then bound it with
a clip and paint it —
add_shading → save_graphics_state → build a path → clip → end_path →
paint_shading → restore_graphics_state (the PDF q … W n /Sh sh … Q
sequence).
Page.add_shading(name, shading)— registers anAxialShadingor
RadialShadingunder/Resources/Shading/<name>.namemust be a valid PDF
resource name; an invalid name raisesPdfErrorand a non-shading object
raisesTypeError.Page.paint_shading(name)— emits theshoperator, painting the named
shading into the current clip region. Ifnamewas never registered the
operator is still emitted but references an undefined resource (no/Shading
dict is written); conforming viewers skip the paint.Page.clip()/Page.clip_even_odd()— emit theW/W*clip-path
operators, intersecting the clipping region with the current path using the
non-zero winding or even-odd rule. Bound an arbitrary-shaped gradient region.Page.end_path()— emits thenoperator, terminating a clip path
(W n/W* n) without filling or stroking.
Rectangular gradient regions can also be bounded with the existing
Page.set_clipping_path; the new clip / end_path pair covers
arbitrary-shaped clips.
Compatibility
Fully backward compatible. All 2038 pre-existing tests pass unchanged; 12 new
tests cover the added surface: exact content-stream operators in document order
(W → n → /Sh sh), resource registration (/ShadingType 2 axial,
/ShadingType 3 radial), a real /FunctionType 2 exponential function for a
two-stop axial gradient (the #297 proof), two shadings coexisting on one page,
the unregistered-name contract, and the invalid-name / non-shading-object error
paths. mypy and cargo clippy -D warnings are clean; the full suite is 2050
passing.
v0.8.0
Release v0.8.0
Summary
Minor release that pulls in upstream oxidize-pdf v2.13.0 and surfaces its new
RAG/AI-pipeline capabilities in the Python bridge:
- Per-chunk and document-level language detection (upstream #293) — opt-in
ISO 639-3 detection for chunked text, exposed through
DocumentChunker.with_language_detection(True), the newDetectedLanguage
type onDocumentChunk.language, and theDocumentChunker.document_language
aggregator. - Token-efficient chunk serialization (upstream #291) — the new
TokenEfficientExporter, a compact, fully round-trippable tabular format for
RAG chunks that roughly halves the serialized-token count versus JSON. JsonExporter— structured JSON export for documents and RAG chunks,
completing the chunk-export surface alongside the token-efficient format.- Ruling-based table detection control (upstream #292) —
PartitionConfig.prefer_ruling_tablesgetter plus
PartitionConfig.without_ruling_tables()to opt out of vector-grid table
reconstruction in the partition pipeline.
This is a non-breaking, additive change at the API level. Every previously
callable method preserves its signature; the new capabilities are opt-in. The
image-extraction fixes from upstream (#286: /SMask alpha compositing and the
flate compression-ratio false-positive) are inherited transparently through the
version bump with no bridge API change.
Upstream
oxidize-pdf=2.12.0→=2.13.0. Pinned exact equality preserved. The
language-detectionfeature was added to the dependency's feature set
(compression, signatures, semantic, language-detection,
default-features = false) to pull in the pure-Rustwhatlangdetector that
backs the new language APIs.- Upstream 2.13.0 added: ruling-based (vector-grid) table detection wired into
the partition pipeline (#292), per-chunk/document language detection (#293),
/SMasksoft-mask compositing into RGBA on image extraction plus a flate
compression-ratio guard fix (#286), and the token-efficient chunk serializer
with the unifyingChunkExportertrait (#291).
Toolchain
- MSRV raised to Rust 1.88 (from 1.77), tracking upstream 2.13.0's own MSRV
bump. The 2025 ecosystem migration to edition 2024 pluslet-chains made the
previously declared 1.77 unbuildable through the dependency tree.
Added
Language detection (RAG chunks)
DocumentChunker.with_language_detection(enabled: bool)— builder that turns
on per-chunk language detection. Disabled by default; when off,
DocumentChunk.languagestaysNone.DocumentChunk.language—DetectedLanguage | None. Populated during
chunk_textonly when detection is enabled.DocumentChunker.document_language(chunks)— static method returning the
dominantDetectedLanguageacross chunks, weighted by chunk content length.
ReturnsNonewhen no chunk carries a detected language (including the empty
list).DetectedLanguage— frozen type withcode(ISO 639-3, e.g."eng",
"spa"),confidence(floatin[0.0, 1.0]), andreliable(bool).
Short or ambiguous text can yield an unreliable detection with an
effectively-random code; gate routing onreliable.
Chunk exporters
TokenEfficientExporter—export_chunks(chunks)serializes RAG chunks to the
#oxct/1tabular format (header declared once, one tab-separated row per
chunk); the staticTokenEfficientExporter.parse_chunks(serialized)is its
exact inverse, reconstructing theDocumentChunklist. Parsing raises on a
wrong version marker, wrong header, or a row whose column count does not match
the header.JsonExporter—export(text)for a simple document object and
export_chunks(chunks)for a structuredchunked_documentobject
(type,chunk_count,chunks[]). Constructor takes
pretty_print(defaultTrue) andinclude_chunks(defaultFalse);
JsonExporter.default()mirrors the upstream defaults.
Partition configuration
PartitionConfig.prefer_ruling_tables— read-only getter;Trueby default,
matching upstream. When enabled, bordered tables are reconstructed from the
PDF's drawn grid (primary path) and per-page graphics are extracted only for
pages that have a drawn grid, so table-free documents pay no extra cost.PartitionConfig.without_ruling_tables()— builder that disables the
ruling-based detector; only the spatial detector runs and no page graphics are
extracted. Chains with the existingwithout_tables/with_*builders.
Compatibility
Fully backward compatible. All 2038 existing tests pass unchanged; 20 new tests
cover the added surface (language detection round-trips against real
English/Spanish corpora, token-efficient export/parse round-trip,
chunked_document JSON shape, and the ruling-tables flag). mypy and
cargo clippy -D warnings are clean.