Skip to content

Releases: bzsanti/oxidize-python

v0.16.0

Choose a tag to compare

@github-actions github-actions released this 22 Jul 18:36
0fb3080

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
    OverflowError at construction (u8) or PdfError at validation (>2).
  • Default bit widths are 16-bit coordinates, 8-bit components, 8-bit flags;
    override with with_bits(bits_per_coordinate, bits_per_component, bits_per_flag).
  • validate() enforces the Type 4 constraints (permitted bit widths,
    Decode length/ordering for the colour space, edge flags, DeviceGray
    meshes requiring gray vertex colours). Page.add_mesh_shading validates
    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() and Page.add_conic_shading enforce 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 /ToUnicode was 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 exceed max_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 4 dictionaries with
    packed vertex bytes cross-checked against core's byte-aligned pack fixture,
    /ShadingType 1 with the deterministic PostScript atan prologue, sh
    operators in the decoded content stream, resource-key/internal-name
    separation, and every core validate() precondition as an error path.
  • Full suite: 2210 passed. mypy clean. cargo clippy --all-targets -- -D warnings clean.

v0.15.1

Choose a tag to compare

@github-actions github-actions released this 15 Jul 13:46
d88c5d8

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_threshold band 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

Choose a tag to compare

@github-actions github-actions released this 11 Jul 21:19
6e41747

What's Changed

  • test(images): characterize real image extraction + document external-images limit (#119) by @bzsanti in #129
  • release(0.15.0): upstream oxidize-pdf 4.0.0 + bounded extraction / contextual chunking / rich tables by @bzsanti in #135

Full Changelog: v0.14.0...v0.15.0

v0.14.0

Choose a tag to compare

@github-actions github-actions released this 29 Jun 12:02
0fd8499

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.
  • PdfReader extract/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_config variants),
    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.json configuration for GitHub Copilot agent mode and an
    OpenAI Agents SDK (MCPServerStdio) snippet.
  • examples/openai_agents_quickstart.py — runnable example that connects to the
    oxidize-mcp server 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, RefCellMutex), 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

Choose a tag to compare

@github-actions github-actions released this 26 Jun 21:00
b2b1d5f

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_cid is
    auto-derived as the largest CID across every map when omitted.
    cid_to_unicode_str lets a single CID (e.g. an fi ligature glyph)
    decompose to several characters in the ToUnicode CMap.
  • CidShowElement(cid, adjust) with .with_x_offset(offset) and cid /
    adjust / x_offset accessors. adjust is the post-glyph advance kern
    (TJ convention); x_offset displaces 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 before show_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 /Resources references /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 an fi
    ligature CID), and verifies the end-to-end contract on real bytes —
    Type0/CIDFontType2/Identity-H structure, the CID codes in the TJ
    content stream, and a text-extraction round-trip yielding fix via the
    ToUnicode CMap.
  • New characterization test in tests/mcp_tests/test_tool_convert_pdf.py for
    the max_tokens/rag contract.
  • Full suite green; mypy clean; cargo check/cargo fmt clean.

Compatibility

  • Python 3.10+, cp310-abi3 wheels (unchanged).

Breaking Changes

None to the Python API.

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 21 Jun 19:48
5f275ff

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_text vs convert_pdf vs extract_entities; read_pdf vs
    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_forms read returns text runs rather than AcroForm widgets,
    validate enforces a required-only rule, fill overlays values; secure_pdf
    encrypt may 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.py asserts 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 the Field description 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 the Annotated form). 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

Choose a tag to compare

@github-actions github-actions released this 20 Jun 22:10
fdfbedb

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 the unstable-spi feature 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:

  • AnalysisPipeline builder: 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 against str),
    ClassifyContext, EnrichContext, Element (with a class_label getter so a
    strategy can read classifier labels), DocumentSource.
  • runtime_checkable Protocols ChunkingStrategy, ElementClassifier,
    MetadataEnricher document 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-abi3 wheel 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 after windows-latest rotated 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-present python3.lib forwarder 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

Choose a tag to compare

@github-actions github-actions released this 13 Jun 21:35
f56f67b

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 IncrementalFormFiller and a PdfReader::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 /V and
    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

Choose a tag to compare

@github-actions github-actions released this 10 Jun 21:58
aebd4cc

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 /Font dictionaries and overlapping font-switched
    runs (#302, #305), a DocumentChunker::chunk_text infinite loop (#308), and
    measure_text/get_string_width over-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_shadingsave_graphics_state → build a path → clipend_path
paint_shadingrestore_graphics_state (the PDF q … W n /Sh sh … Q
sequence).

  • Page.add_shading(name, shading) — registers an AxialShading or
    RadialShading under /Resources/Shading/<name>. name must be a valid PDF
    resource name; an invalid name raises PdfError and a non-shading object
    raises TypeError.
  • Page.paint_shading(name) — emits the sh operator, painting the named
    shading into the current clip region. If name was 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 the W / 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 the n operator, 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
(Wn/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

Choose a tag to compare

@bzsanti bzsanti released this 08 Jun 14:18
f21fd50

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:

  1. 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 new DetectedLanguage
    type on DocumentChunk.language, and the DocumentChunker.document_language
    aggregator.
  2. 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.
  3. JsonExporter — structured JSON export for documents and RAG chunks,
    completing the chunk-export surface alongside the token-efficient format.
  4. Ruling-based table detection control (upstream #292) —
    PartitionConfig.prefer_ruling_tables getter 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-detection feature was added to the dependency's feature set
    (compression, signatures, semantic, language-detection,
    default-features = false) to pull in the pure-Rust whatlang detector 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),
    /SMask soft-mask compositing into RGBA on image extraction plus a flate
    compression-ratio guard fix (#286), and the token-efficient chunk serializer
    with the unifying ChunkExporter trait (#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 plus let-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.language stays None.
  • DocumentChunk.languageDetectedLanguage | None. Populated during
    chunk_text only when detection is enabled.
  • DocumentChunker.document_language(chunks) — static method returning the
    dominant DetectedLanguage across chunks, weighted by chunk content length.
    Returns None when no chunk carries a detected language (including the empty
    list).
  • DetectedLanguage — frozen type with code (ISO 639-3, e.g. "eng",
    "spa"), confidence (float in [0.0, 1.0]), and reliable (bool).
    Short or ambiguous text can yield an unreliable detection with an
    effectively-random code; gate routing on reliable.

Chunk exporters

  • TokenEfficientExporterexport_chunks(chunks) serializes RAG chunks to the
    #oxct/1 tabular format (header declared once, one tab-separated row per
    chunk); the static TokenEfficientExporter.parse_chunks(serialized) is its
    exact inverse, reconstructing the DocumentChunk list. Parsing raises on a
    wrong version marker, wrong header, or a row whose column count does not match
    the header.
  • JsonExporterexport(text) for a simple document object and
    export_chunks(chunks) for a structured chunked_document object
    (type, chunk_count, chunks[]). Constructor takes
    pretty_print (default True) and include_chunks (default False);
    JsonExporter.default() mirrors the upstream defaults.

Partition configuration

  • PartitionConfig.prefer_ruling_tables — read-only getter; True by 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 existing without_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.