Added
- (ocr): the registry reports each backend's declared languages.
list_ocr_backends()returned
only names, so a caller validating an OCR language against the installed backends had to maintain
a second language table of its own.list_ocr_backend_capabilities()returns one
OcrBackendCapabilitiesrecord per registered backend — its name and the languages it reports —
ordered by backend name. An emptysupported_languagesmeans the backend does not enumerate its
languages, not that it supports none, soocr_backend_supports_language()is provided for
callers that need a decision rather than a list. (GH#1643) - (ocr): mixed-page PDF extraction reports one coordinate frame per OCR'd page. Public
ocr_elementscarry the OCR backend's raster coordinates, but the per-page processed raster size
lived only in the page-local result and was discarded before the elements reached the final
document. A multi-page consumer was left with the document-level
ocr_processed_image_width/ocr_processed_image_height, which is not authoritative for pages of
differing size, preprocessing, or rotation, and so could not normalize a box safely.
metadata.additional.ocr_page_coordinate_framesnow carries one record per OCR page that has
public elements —page_number,width,height,unit: "pixel",origin: "top_left"— ordered
by page number and joined to an element through the element's ownpage_number. No record is
emitted for a page whose raster dimensions are absent or invalid. (GH#1645) - (ner): Rust callers can share xberg's process-wide GLiNER backend cache.
text::ner::gline::get_or_init_backendperforms model initialization on Tokio's blocking pool,
returns the sameArcfor the same model and thread budget, and leaves failed initializations
retryable. - (pdf): every hierarchy page now advertises the coordinate frame its boxes live in. Hierarchy
bounding boxes are emitted in raw PDF user space, but nothing named that space, so a consumer
could not tell whether a box needed translating by a non-zero MediaBox origin or rotating for a
page's/Rotate.metadata.additional.pdf_page_coordinate_framesnow carries one record per
page that has hierarchy blocks —page_number,origin_x/origin_y(the MediaBoxllx/lly,
which may be non-zero and negative),width/height,unit: "point",
origin: "bottom_left", andclockwise_rotation— ordered by page number.width/heightare
the MediaBox extent and are deliberately not swapped for a 90/270 rotation, because they
describe raw user space rather than the displayed frame; this is the opposite convention to
ocr_page_coordinate_frames, which reports an already-rotated raster, so the two must not be
assumed to agree for the same page. A page is omitted entirely, rather than reported with a
default, when its/Rotateis present but not a valid multiple of 90 or its MediaBox yields no
usable extent. (GH#1653, GH#1654)
Fixed
- (ocr): an isolated blank quantity cell in an invoice table can be recovered from its own
pixels. A table whoseQTYcolumn has strong integer support — at least two recognized
whole-number quantities — but exactly one blank cell now gets a single targeted retry that
re-reads only that cell's bounded pixel region in single-line segmentation mode. The retry never
derives a quantity from a price or total column, and any table outside this narrow shape takes
the existing fast path unchanged. A header split across two words (UNIT/PRICE,
LINE/TOTAL) is also reassembled into one header cell when the fragment sits nearer a
data-supported column than any standalone column of its own. This OCR quality change ships
without a corpus-wide A/B measurement. - (reranker): a cancelled
rerank_asyncno longer releases its concurrency permit while its
inference is still running. The permit was held by the awaiting future rather than by the
spawn_blockingtask, and a blocking task cannot be cancelled — dropping its handle merely
detaches it. A caller that timed out or dropped therefore returned the permit immediately while the
model load and inference it was bounding continued, so repeated abandoned calls could exceed the
configured reranker concurrency and keep several models resident after every caller had returned.
(GH#1641) - (embeddings, transcription): the same cancelled-permit leak fixed above for the reranker is now
fixed forembed_texts_asyncand audio/video transcription. Both bound aspawn_blocking
inference call with a concurrency permit held by the awaiting future rather than by the blocking
task, so a caller that dropped its future released the permit while the ONNX embedding batch or
Whisper inference it was bounding kept running. Transcription is timeout-wrapped by default
(transcription.timeout_ms), so an expiry there was a live, designed-in path to the same
concurrency breach, not a hypothetical one. (GH#1641) - (pdf): a wrapped numbered heading set in a heading-size font no longer loses its second line.
follows_section's two continuation exemptions only recognized a wrap via a hanging indent or via
matching right edges, both of which a no-indent heading's short last line fails by construction. A
third exemption,heading_continuation_at_margin, now recognizes a wrap by measuring the heading's
own line against the width of the body column beneath it, the same signal the paragraph-merge pass
already uses for a body-sized heading. Independently, the paragraph-gap detector could still cut a
continuation these exemptions accepted, because a heading's own line pitch legitimately exceeds the
body's and was read as a blank-line break; the gap check is now suppressed at exactly the boundary a
continuation exemption already accepted. (GH#1650) - (packaging): the Helm chart is published again. The chart publish was gated such that a failed
Docker build leg skipped it entirely, while the container images themselves published and the
release still reported success. No chart was published for 1.1.4, 1.1.5, 1.2.0, 1.2.1 or 1.2.2, so
helm install --version <current>could not resolve. The chart for 1.2.2 has been published
retroactively, and a release now fails loudly if its chart is missing rather than shipping without
one. - (node): a JavaScript plugin registered through the Node binding now actually runs. Every
generated Node trait bridge --OcrBackend,PostProcessor,Validator,Renderer,
TokenizerBackendand the rest -- was structurally unable to call back into JavaScript. It
invoked the JS callable directly from whatever Tokio worker thread the pipeline happened to be
on, with no N-API handle scope; it never awaited a returned promise, so anasyncmethod's
result was the string"[object Promise]"; and it coerced whatever came back to a string before
parsing it as JSON, which fails for a plain object as well. The published.d.tshad always
declared the working contract (processImage(imageBytes: Uint8Array): Promise<ExtractedDocument>),
so no plugin could have been written against the shipped behaviour. Bridges now hold a
ThreadsafeFunctioncreated on the JS thread, await the reply throughcall_async_catch(a JS
throw surfaces as a plugin error instead of aborting the process), and decode the value without
the string round-trip. A handle reference that would otherwise be released off its owning JS
thread is leaked rather than freed, since freeing it there corrupts the V8 heap. (GH#1636)
Changed
-
(pdf): a page whose MediaBox is not rooted at the origin no longer reports the wrong page
size. The native engine's per-page text result took its width and height from the MediaBox
upper-right corner instead of its extent, so a page with MediaBox[10 -100 622 692]reported
622x692 rather than the true 612x792. That figure feeds hierarchy reading-order computation, so
a non-zero MediaBox origin could mis-order content, not merely mis-scale it. Pages rooted at
(0, 0)— the overwhelming majority — are unaffected, since the two expressions agree there.
(GH#1653) -
Upgraded the
crawlbergdependency to 1.7.0 (from 1.6.4). xberg re-exports crawlberg's
crawl types --CrawlConfig,MapResult,SitemapUrl-- so the pin is part of this crate's
public surface. 1.7.0 makes no Rust API changes; its breaking changes are confined to
crawlberg's own Java and Swift bindings, which xberg does not consume.
Zig
Add to your build.zig.zon:
.dependencies = .{
.xberg-zig = .{\n .url = \"https://github.com/xberg-io/xberg/releases/download/v1.2.3/xberg-zig-v1.2.3.tar.gz\",\n .hash = \"xberg-1.2.3-iV1GrgLORRm2zXRsxaZHWV8cdrQNfPRQsQCDMrPmDJrM\",\n },\n},\n```\n