4 hours ago
kreuzberg

v1.2.4

Added

  • (llm): LlmConfig exposes liter-llm's provider response-byte cap. liter-llm's ClientConfigBuilder::max_response_bytes bounds every HTTP response body read from a provider, but xberg's LlmConfig never forwarded it, so downstream products had no way to bound provider responses. LlmConfig::max_response_bytes mirrors it: it bounds response bodies on every non-streaming call and the error body read on a failed request (a successful streaming response keeps its own existing frame bounds), and defaults to None (unbounded), matching liter-llm. Some(0) is rejected by LlmConfig::validate rather than reaching liter-llm's own builder. (xberg-io/xberg-enterprise#1568, xberg-io/xberg-enterprise#1861)

Fixed

  • (pdf): a page-sized background rectangle no longer seeds a whole-page table. Many Office-to-PDF producers draw a white rectangle over the entire page before anything else. That rectangle passed the table-primitive filter (its < 1000 pt bound does not exclude an A4 or Letter page) and, because clustering unions any two primitives whose boxes intersect, pulled every rule on the page -- the real table's borders, the footer rule, a figure's frame, strokes inside a drawing -- into one cluster with the page as its box. The cluster fallback then built a table over the whole page: the heading above the real table became its first row, torn at the table's own column rule, and the first words of the prose below it became its last row, with those words missing from or doubled in the paragraphs that followed. A rectangle covering at least 90 % of the page's MediaBox in both dimensions is now treated as page furniture and dropped before clustering, so the real table's rules cluster on their own and the page comes out as it does without the background. A full-page-width rule (one dimension at page scale) is deliberately still a primitive. (GH#1656)
  • (pdf): a single-column page of hanging-number headings is no longer read as two columns. Some producers (Distiller among them) emit the tab between a heading's number and its title as a space-only span in a different font, and leave space-only spans for blank lines. The dense-two-column repair counted those spans as column population and let a blank line's two whitespace spans and a footer split across both margins vote for a gutter, so five headings' tabs plus two lines of nothing met the six-line quorum and the page was emitted column-major: 6, 6.1, 6.1.1 as bare numbers and their titles as separate unnumbered headings. Whitespace-only spans no longer count toward the per-side density gate, the gutter vote, the whitespace corridors or the row-pairing guard, and a per-line gap wider than a quarter of the page width is no longer accepted as gutter evidence (a blank line's or a footer's gap; every real gutter measured here is under 10 %). (GH#1655)
  • (ocr): a configured security_limits now reaches the Tesseract image decode. GH#1554 routed ExtractionConfig::security_limits onto OcrConfig before each OCR call, but the Tesseract backend's config_to_tesseract had no field to carry it into and built a fresh default ExtractionConfig for the processor, so every Tesseract decode -- standalone images, embedded images, scanned pages and the targeted page fallback alike -- ran under the 100 MiB default no matter what the caller set. A 600 DPI A4 scan was refused with an error naming 104857600 bytes while both configuration objects said 5 GiB. The internal TesseractConfig now carries the limits (outside the cache key: they gate whether a decode runs, never what it produces), the processor prefers them over its synthetic config, and the standalone-image and PDF embedded-image routes inject the caller's limits like the other routes already did. A limit set directly on OcrConfig is honoured when ExtractionConfig carries none; ExtractionConfig still wins when both are set. (GH#1651)
  • (go): Register* no longer leaks its cgo.Handle when the C vtable allocation fails. Every Register* wrapper in the Go binding created the handle before allocating the C vtable, and the allocation-failure branch returned without deleting it, keeping the bridge and the caller's implementation reachable for the life of the process. The branch is only reachable when a small malloc fails, so this is a resource leak on an error path rather than an exploitable defect; it is fixed in the generator (alef 0.91.6) and regenerated here. Reported by @OvOhao (GHSA-q5pq-8g86-v9j9).
  • a table's multi-word cell no longer bleeds a trailing word into the next column, a right-aligned amount column split by digit-width drift is folded back together, and a legitimately sparse but independently-headed column (or a sparse first data row) no longer gets the whole table rejected. Reconstructing a table from word boxes decided each word's column independently by nearest x-position, so a wide cell's own trailing word (e.g. a long description's last word) could resolve to a neighboring column's anchor instead of its own cell's. Column membership for a data row is now decided once per merged cell cluster and applied to every word the cluster contains; a drift-split right-aligned numeric column (mutually exclusive per row, no independent header of its own) is folded into one column; and a section caption sharing a table region with its real header row is dropped. This cell-clustering and column-assignment logic lives in the shared table_core module, so it also affects native-PDF word-position table reconstruction, not just OCR. Separately, pdf's shared table post-processor no longer treats an independently-headed but infrequently-populated column (e.g. a bank statement's DEPOSIT, populated on a minority of transaction rows) as noise, and no longer folds a sparse first data row (one missing an optional numeric field) into a bogus multi-row header merge with the row after it -- and that header-shortcut no longer stops one row early on a genuine two-row text header, which also fully populates a digit-free first row. The punctuation cell-merge gap this widens for a lone dash/colon glyph is now conditioned on the punctuation genuinely bridging two neighboring words on both sides, so an isolated punctuation cell near a real column boundary (a - placeholder, a :) no longer fuses two columns' content into one cell. (GH#1649)
4 hours ago
xberg

v1.2.4

Added

  • (llm): LlmConfig exposes liter-llm's provider response-byte cap. liter-llm's ClientConfigBuilder::max_response_bytes bounds every HTTP response body read from a provider, but xberg's LlmConfig never forwarded it, so downstream products had no way to bound provider responses. LlmConfig::max_response_bytes mirrors it: it bounds response bodies on every non-streaming call and the error body read on a failed request (a successful streaming response keeps its own existing frame bounds), and defaults to None (unbounded), matching liter-llm. Some(0) is rejected by LlmConfig::validate rather than reaching liter-llm's own builder. (xberg-io/xberg-enterprise#1568, xberg-io/xberg-enterprise#1861)

Fixed

  • (pdf): a page-sized background rectangle no longer seeds a whole-page table. Many Office-to-PDF producers draw a white rectangle over the entire page before anything else. That rectangle passed the table-primitive filter (its < 1000 pt bound does not exclude an A4 or Letter page) and, because clustering unions any two primitives whose boxes intersect, pulled every rule on the page -- the real table's borders, the footer rule, a figure's frame, strokes inside a drawing -- into one cluster with the page as its box. The cluster fallback then built a table over the whole page: the heading above the real table became its first row, torn at the table's own column rule, and the first words of the prose below it became its last row, with those words missing from or doubled in the paragraphs that followed. A rectangle covering at least 90 % of the page's MediaBox in both dimensions is now treated as page furniture and dropped before clustering, so the real table's rules cluster on their own and the page comes out as it does without the background. A full-page-width rule (one dimension at page scale) is deliberately still a primitive. (GH#1656)
  • (pdf): a single-column page of hanging-number headings is no longer read as two columns. Some producers (Distiller among them) emit the tab between a heading's number and its title as a space-only span in a different font, and leave space-only spans for blank lines. The dense-two-column repair counted those spans as column population and let a blank line's two whitespace spans and a footer split across both margins vote for a gutter, so five headings' tabs plus two lines of nothing met the six-line quorum and the page was emitted column-major: 6, 6.1, 6.1.1 as bare numbers and their titles as separate unnumbered headings. Whitespace-only spans no longer count toward the per-side density gate, the gutter vote, the whitespace corridors or the row-pairing guard, and a per-line gap wider than a quarter of the page width is no longer accepted as gutter evidence (a blank line's or a footer's gap; every real gutter measured here is under 10 %). (GH#1655)
  • (ocr): a configured security_limits now reaches the Tesseract image decode. GH#1554 routed ExtractionConfig::security_limits onto OcrConfig before each OCR call, but the Tesseract backend's config_to_tesseract had no field to carry it into and built a fresh default ExtractionConfig for the processor, so every Tesseract decode -- standalone images, embedded images, scanned pages and the targeted page fallback alike -- ran under the 100 MiB default no matter what the caller set. A 600 DPI A4 scan was refused with an error naming 104857600 bytes while both configuration objects said 5 GiB. The internal TesseractConfig now carries the limits (outside the cache key: they gate whether a decode runs, never what it produces), the processor prefers them over its synthetic config, and the standalone-image and PDF embedded-image routes inject the caller's limits like the other routes already did. A limit set directly on OcrConfig is honoured when ExtractionConfig carries none; ExtractionConfig still wins when both are set. (GH#1651)
  • (go): Register* no longer leaks its cgo.Handle when the C vtable allocation fails. Every Register* wrapper in the Go binding created the handle before allocating the C vtable, and the allocation-failure branch returned without deleting it, keeping the bridge and the caller's implementation reachable for the life of the process. The branch is only reachable when a small malloc fails, so this is a resource leak on an error path rather than an exploitable defect; it is fixed in the generator (alef 0.91.6) and regenerated here. Reported by @OvOhao (GHSA-q5pq-8g86-v9j9).
  • a table's multi-word cell no longer bleeds a trailing word into the next column, a right-aligned amount column split by digit-width drift is folded back together, and a legitimately sparse but independently-headed column (or a sparse first data row) no longer gets the whole table rejected. Reconstructing a table from word boxes decided each word's column independently by nearest x-position, so a wide cell's own trailing word (e.g. a long description's last word) could resolve to a neighboring column's anchor instead of its own cell's. Column membership for a data row is now decided once per merged cell cluster and applied to every word the cluster contains; a drift-split right-aligned numeric column (mutually exclusive per row, no independent header of its own) is folded into one column; and a section caption sharing a table region with its real header row is dropped. This cell-clustering and column-assignment logic lives in the shared table_core module, so it also affects native-PDF word-position table reconstruction, not just OCR. Separately, pdf's shared table post-processor no longer treats an independently-headed but infrequently-populated column (e.g. a bank statement's DEPOSIT, populated on a minority of transaction rows) as noise, and no longer folds a sparse first data row (one missing an optional numeric field) into a bogus multi-row header merge with the row after it -- and that header-shortcut no longer stops one row early on a genuine two-row text header, which also fully populates a digit-free first row. The punctuation cell-merge gap this widens for a lone dash/colon glyph is now conditioned on the punctuation genuinely bridging two neighboring words on both sides, so an isolated punctuation cell near a real column boundary (a - placeholder, a :) no longer fuses two columns' content into one cell. (GH#1649)
8 hours ago
gpui-kit

v0.6.2

GPUI Kit v0.6.2

GPUI Kit v0.6.2 brings mobile application support for iOS and Android, three new components (Carousel, Empty, and InputGroup), a public editor search API, Markdown inline plugins, and motion sequencing. This release also cuts per-frame work in TextView, List, Plot, and Chart, and removes the dock's tiles canvas.

Mobile Applications

GPUI Kit applications can now be hosted by an external iOS or Android platform. Mobile builds keep GPUI and component initialization while leaving the desktop platform selector out, and gpui_kit::is_mobile() exposes the target check. On mobile, tooltips stay hidden and HoverCard opens and closes on tap. New mobile guides cover simulator setup, Swift UIView embedding, lifecycle and keyboard layout, and touch interactions. (#3045)

  • Touch text selection: a long press in Input, Textarea, Editor, or a selectable TextView selects the word under the finger and shows draggable selection handles with an iOS-style edit menu (Cut, Copy, Paste, Select All). Double tap selects a word in editable text, Select All reaches past the viewport, and the menu returns after a scroll. (#3058, #3073, #3076, #3075, #3077)
  • iOS-like scroll bounce through the new ScrollBounce wrapper in gpui-base, covering lists, scrollable divs, touch release, momentum, and reduced motion. (#3067)
  • Scrollbars support touch dragging with a smaller resting thumb on mobile. (#3060, #3111)
  • A long press opens a plot's tooltip on mobile. (#3078)

New Components

  • Carousel: horizontal and vertical snapping, pointer and trackpad gestures, keyboard navigation, controlled selection, looping, optional pagination, and reduced motion. (#2867)
  • Empty: composable empty states built from Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, and EmptyContent, imported from gpui_kit::component::empty. (#3030)
  • InputGroup: inline addons, block toolbars, and textareas sharing one input frame through InputGroup, InputGroupInput, InputGroupTextarea, InputGroupAddon, InputGroupButton, and InputGroupText. (#3042)

Editor and Text Input

  • Editor search API is now public on the editor state: set_search_query, close_search, next_search_match, previous_search_match, the replace methods, and search_session(), so an application can draw its own search bar. Match highlights show without the panel, and the search shortcut reaches the host view. (#2533)
  • Wrapped search matches stay visible with surrounding context. (#3013)
  • Added on_paste to Input, Textarea, and Editor so applications can handle images and files from the clipboard instead of dropping them. (#3087)
  • Languages can register runtime parser factories through LanguageRegistry::register_parser_factory, so a Tree-sitter grammar no longer has to be linked into the binary. (#3039)
  • Fixed text descender clipping. (#3023)

Markdown and Text Rendering

  • Inline plugins through MarkdownPlugin and .plugin(...). Inline math parses by default, and the example adds [@user](mention:user) profile cards. Inline objects inherit surrounding formatting and links and participate in table column sizing. (#3031, #3070)
  • Streamed text fades in with TextView::stream_fade(true), or with custom timing and a word-by-word reveal through TextViewMotion. (#3082)
  • A Markdown image's data: URL is decoded instead of fetched. (#3088)
  • Faster rendering: fewer and cheaper elements per Markdown frame, and a paragraph's shaped text and highlights are kept across frames. (#3090, #3115)
  • Fixed inline-code layout in headings, list alignment, and inline-code fragments wrapping twice. (#3038, #3046)

Motion

  • Sequence chains transitions so a step starts when the previous one ends, each with its own duration, easing, and delay. (#3092)
  • gpui_base::init reads the operating system's reduced-motion preference into App::reduce_motion, which every Base motion primitive already honors. (#3094)

Dock

  • Removed the tiles canvas. Split and Tabs are now the only container shapes; a freeform canvas is a panel the host implements. gpui_component::dock::Panel::title_bar lets a panel that carries its own chrome opt out of the tab group's title bar. See Breaking Changes below. (#3036)
  • Added DockArea::select_panel to display a tab by PanelId wherever it sits, without moving it. (#3063)

Components

  • Button: a ghost button hovers with accent and presses with button_active, matching the outline button and other low-emphasis controls. (#3100)
  • Dialog: a dialog stays inside the window it opens in, and its own controls route to that dialog. (#3093, #3097)
  • Menu: a submenu opens from a scrollable PopupMenu, and shortcut hints paint on the frame the menu opens. (#3095, #3096)
  • Tab: fixed explicit scrolling to newly added tabs, and flex_1() tabs fill the tab bar again. (#3021, #3032)
  • List and Table: dropped the outline from the selected item, row, and cell; keyboard row navigation stops when row_selectable is off. (#3108, #3033)
  • Select: the search query clears when the menu closes. (#3050)
  • Setting: fixed number step and clamping in the delegate, and preserved the selected page while searching. (#3099, #3104)
  • Avatar: the initials fallback draws from an even OkLCH hue ring. (#3026)
  • ColorPicker: fixed duplicate accessibility node IDs. (#3028)
  • Chart and Plot: the hover animates, heavy geometry is cached, grids paint as quads, and tessellated line and area paths are cached. The chart gallery cards were reworked. (#3112, #3071)
  • List: trimmed the per-frame cost of a scrolling list. (#3085)
  • FPS: the readout leaves its own frame out of the readings, grades against the panel, and stops the clock while hidden. (#3025, #3029)

Documentation and Website

Added mobile guides, notification placement docs, and certified AI-facing consumer recipes. The website lets the browser draw emoji and CJK text the bundled fonts lack, skips the system font probe when no font is installed, and fixes the docs sidebar hover background. (#3062, #3109, #3057, #3106, #3044)

Thank you to everyone who contributed code, documentation, testing, and feedback!

What's Changed

New Contributors

Full Changelog: https://github.com/longbridge/gpui-kit/compare/v0.6.1...v0.6.2

1 days ago
quiche

📏 0.30.0

Breaking Changes:

  • PathEvent is now #[non_exhaustive] and adds PmtuUpdated. Rust applications that exhaustively match path events must add a wildcard arm. C applications should handle QUICHE_PATH_EVENT_PMTU_UPDATED when PMTU discovery is enabled.
  • The supported boring range is now >=4.19,<6, so fresh dependency resolution uses 5.x by default while consumers pinned to 4.19 remain supported. Static C applications linking libquiche.a must use a C++ linker driver or otherwise link the C++ runtime. Boring 5 enables post-quantum groups by default, which can split the ClientHello across multiple Initial packets. Applications that need smaller or deterministic ClientHellos can use Config::set_curves_list() or quiche_config_set_curves_list().

Highlights:

  • Added Connection::stream_readable_len() to report the contiguous bytes currently readable from a stream, up to a caller-supplied limit, without copying data.
  • Fixed connection-level flow-control double counting from zero-length non-FIN STREAM frames, which could close long-running bulk transfers with FLOW_CONTROL_ERROR.
  • Fixed handshake callback configuration being lost when early data was accepted during an in-progress 0-RTT handshake.
  • Fixed cleanup of unknown and empty HTTP/3 unidirectional streams, and compressed completed-stream tracking into ranges to reduce retained memory.
  • Updated intrusive-collections to 0.10.3 for upstream undefined-behavior and atomic-link race fixes affecting stream priority queues.

Full changelog at 0.29.3...0.30.0

1 days ago
actix-web

actix-multipart: v0.8.2

  • Fix multipart field parsing hanging at EOF with missing or incomplete boundaries by returning MultipartError::Incomplete.
1 days ago
syn

3.0.6

  • Fix parsing interpolated lifetime at statement start (#2082)
1 days ago
async-compression

async-compression-v0.4.48

Fixed

  • (gzip) reject invalid header before the full 10 bytes arrive (#489)
1 days ago
async-compression

compression-codecs-v0.4.43

Fixed

  • (gzip) reject invalid header before the full 10 bytes arrive (#489)
1 days ago
xberg

v1.2.3

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 OcrBackendCapabilities record per registered backend — its name and the languages it reports — ordered by backend name. An empty supported_languages means the backend does not enumerate its languages, not that it supports none, so ocr_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_elements carry 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_frames now 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 own page_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_backend performs model initialization on Tokio's blocking pool, returns the same Arc for 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_frames now carries one record per page that has hierarchy blocks — page_number, origin_x/origin_y (the MediaBox llx/lly, which may be non-zero and negative), width/height, unit: "point", origin: "bottom_left", and clockwise_rotation — ordered by page number. width/height are 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 /Rotate is 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 whose QTY column 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_async no longer releases its concurrency permit while its inference is still running. The permit was held by the awaiting future rather than by the spawn_blocking task, 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 for embed_texts_async and audio/video transcription. Both bound a spawn_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, TokenizerBackend and 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 an async method'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.ts had 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 ThreadsafeFunction created on the JS thread, await the reply through call_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 crawlberg dependency 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.

1 days ago
kreuzberg

v1.2.3

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 OcrBackendCapabilities record per registered backend — its name and the languages it reports — ordered by backend name. An empty supported_languages means the backend does not enumerate its languages, not that it supports none, so ocr_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_elements carry 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_frames now 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 own page_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_backend performs model initialization on Tokio's blocking pool, returns the same Arc for 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_frames now carries one record per page that has hierarchy blocks — page_number, origin_x/origin_y (the MediaBox llx/lly, which may be non-zero and negative), width/height, unit: "point", origin: "bottom_left", and clockwise_rotation — ordered by page number. width/height are 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 /Rotate is 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 whose QTY column 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_async no longer releases its concurrency permit while its inference is still running. The permit was held by the awaiting future rather than by the spawn_blocking task, 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 for embed_texts_async and audio/video transcription. Both bound a spawn_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, TokenizerBackend and 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 an async method'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.ts had 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 ThreadsafeFunction created on the JS thread, await the reply through call_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 crawlberg dependency 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.