github xberg-io/xberg v1.2.7

latest releases: packages/go/v1.2.8, v1.2.8, packages/go/v1.2.7...
one day ago

Added

  • (config): ConcurrencyConfig::max_concurrent_ocr and the --max-concurrent-ocr CLI flag set concurrent Tesseract recognition sessions on their own. Use it when the host has cores to spare but not the memory to run a recognition session on each of them. The value is applied as given: neither the thread budget nor the host's free memory reduces it, since both of those bound only the automatic limit. The first extraction in a process fixes the session count for the rest of that process, because the admission semaphore and the Tesseract handle pool that enforce it are built once and the pool's capacity is fixed when it is constructed; a later extraction that names a different value keeps the first one and logs a one-time WARN naming both numbers. Set the value on the first extraction, or run one process per value. ConcurrencyConfig is not #[non_exhaustive], so the added field breaks any Rust caller that builds the struct by literal without ..Default::default(); such a caller must add the field or the rest pattern. Callers on every other binding are unaffected. (GH#1727)

Changed

  • (pdf): scan detection and fabricated-mapping OCR routing now use the thread budget. Native PDF extraction graded every page for raster scan evidence, then read every page's text again to check its glyph-to-Unicode mapping provenance, and ran both passes one page at a time. The two passes held a single core for the whole document whatever ConcurrencyConfig::max_threads was set to, so no thread budget could shorten them. Both now run across the thread budget, sequentially on wasm32 which has no thread pool, and report the same per-page confidences and the same page lists in the same page order. Measured on a 32-core machine at max_threads = 32, with the extracted output identical in every run: a 4778-page manual from 66.9 s to 63.3 s, and a 676-page book from 24.1 s to 21.9 s. The two passes are a small share of a native extraction, so the wall-clock gain is bounded by that share. (GH#1723)
  • (ocr): concurrent Tesseract recognition follows the thread budget instead of a fixed four. The limit was a compile-time constant that no configuration reached, so raising max_threads could not raise recognition throughput, and recognition is most of the run on a scanned document. The default is now the thread budget, reduced to the number of sessions the host's free memory holds. Set max_concurrent_ocr to 4 to keep the previous behaviour. (GH#1727)
  • (pdf/ocr): the per-page OCR route sizes its batch by what a page costs to hold, not by the limit on returned content size. max_content_size bounds the text an extraction returns and says nothing about the rasters a batch holds while it works, so on an ordinary 150 DPI document the arithmetic pinned the batch at four pages and no stage of the route could use more than four threads whatever the thread budget said. The width now comes from the page box and the render resolution, measured against free memory less the document and a reserve. Each page's own peak is still checked against max_content_size, one page at a time. Peak resident memory rises with the wider batch -- 2,234 MB to 3,583 MB on a 731-page document at a 32-thread budget -- because more rasters are in flight. (GH#1724)
  • (pdf): the embedded-image pass now runs across the thread budget. Configuring OCR switches whole-document embedded-image extraction on, and that pass walked the document one page at a time, so no thread budget shortened it. Pages now decode in parallel (sequentially on wasm32, which has no thread pool), in the same order, and the returned bytes are unchanged. Measured on a 731-page document holding 2,881 images at a budget of 32: the pass falls from 20.1 s to 2.2 s, and the whole extraction from 57.3 s to 41.3 s. Peak resident memory rises with the budget, because each pool thread holds one page's raw pixel buffers and their PNG re-encodes at once, where a single page's were live before: over the pass itself, 1,133 MB before against 1,463 MB at a budget of 8 and 2,186 MB at 32. The pass carries no memory budget of its own, so the ceiling is the thread budget, which is min(cpu_cores, 8) until ConcurrencyConfig::max_threads raises it. The pass also no longer decodes and PNG re-encodes a full-page image on a page that already carries native text when OCR is the only reason the pass is running at all: should_skip_pdf_image_ocr excludes exactly that image from OCR and drop_ocr_only_images then drops it from the result unread, so the decode was pure waste. That image still appears in the result with its dimensions, bounding box, page number and alt text intact and an empty data; a document where any other consumer -- image extraction, captioning, QR codes, inline-image OCR, page rasters, or styled HTML rendering -- would actually read the bytes is unaffected. (GH#1732)
  • (ocr): candle-deepseek-ocr reads device tensors back in one transfer instead of one element at a time. The mixture-of-experts gate copied its score matrix per expert, per row, per layer, per generated token, and the SAM relative-position lookup read its index inside a nested loop per attention layer, per crop. Both are now a single bulk read. Measured on an A100 at BF16: 62.71 s to 8.09 s per page (7.8x), output byte-identical, peak memory unchanged. A 22-page scanned document that previously exceeded the 600 s extraction ceiling now completes in 322 s. (GH#1711, GH#1714)
  • (ocr): candle-deepseek-ocr resizes the relative-position table and scatters the image embeddings as device operations instead of per-element loops. The resize sampled its axis one position at a time and concatenated one tensor per output position, on each of the four global-attention layers of the batched 640 px local-crop pass; it is now candle's bilinear resize over a one-row image, which is the half-pixel sampling the reference implementation uses. The scatter gathered rows one at a time and stacked one tensor per sequence position, once per page; it is now two gathers and a select. Measured on an NVIDIA L4 at BF16 over a full Letter page, five repetitions: 47.610 s to 47.546 s per page, which sits inside the spread of the unchanged arm, so this is not a speedup on that workload. Both arms decode the same 2,052 characters. Peak GPU memory falls from 8,818 MiB to 8,722 MiB. (GH#1719)
  • (ocr): the per-page OCR-image render/encode size check now runs through one shared helper instead of three separate copies. GH#1724 and GH#1731 each fixed a route that summed a whole page batch against security_limits.max_content_size -- a limit its own error text names as a per-image bound -- rejecting every page in the batch once the sum crossed it. Both fixes, and the mixed native/OCR route's own single-backend path, now call one validate_png_encode_pages_individually helper that charges each page on its own, so a new call site cannot reintroduce the batch-summing shape by calling the lower-level batch-peak function directly. No behaviour change on any of the three existing routes. (GH#1748)
  • (pdf): a /Font dictionary shared across pages no longer redoes TrueType cmap donation on every page that touches it. share_truetype_cmaps clones and mutates each undonated font with Arc::make_mut, and the font-set caches deliberately store each dictionary's pre-donation fonts (#1725 made that order-independent), so every cache hit re-ran that clone for the same font. Donation among fonts within one dictionary's own resolved set is now cached alongside the pre-donation set, under the same key, so it depends only on that dictionary's own resources and not on which page or Form XObject was read before it. Donation whose donor lives in a different dictionary (a Form XObject donating to its page, say) is unaffected and still runs on every load_fonts call. (GH#1746)
  • (pdf): fabricated-mapping OCR routing no longer reads every page's text a second time. The main text pass and the provenance pass each read every page of a native PDF: the main pass to assemble content, the provenance pass (GH#1723) to grade each page's glyph-to-Unicode mapping. Neither carried its read into the other, so a 731-page document spent 14.2 s of a 24.6 s extraction on the two passes together. The main pass now keeps each page's fabricated-character counts from the raw spans it already reads, and the provenance pass consumes those counts instead of reading the page again; a document with default-off optional-content layers still reads separately, since the main pass reads layer-filtered spans there and the two would otherwise disagree. Retaining the counts holds page text already held for content assembly, not rasters, so peak memory is unaffected. fabricated_text_pages, scan confidences and extracted content are unchanged. (GH#1744)
  • (pdf): scan detection no longer decodes an embedded image's pixels just to classify its codec. Grading a page for raster scan evidence decoded every embedded image on the page in full -- the same decode the embedded-image extraction pass repeats moments later when OCR is configured -- only to read whether the result was JPEG or CCITT and how large its bounding box was. Both are already known from the cheap Phase 1 handle enumeration that walks the content stream without decompressing anything, so scan detection now reads bounding box and filter chain from there instead. The embedded-image extraction pass, the only remaining caller that needs decoded pixels, is unaffected; classification output (image area ratio, codec class) is unchanged for every image that decodes; an image the decoder rejects now counts toward the page's image coverage where it was silently dropped before, and images under the 8 x 8 px extraction floor stay excluded. (GH#1732)

Fixed

  • (pdf): layout detection no longer fails partway through a long document. The layout pass charged every page raster it had already produced against security_limits.max_content_size, so the running total crossed the 100 MiB default after about twelve standard pages, whatever the page size. Layout detection then failed for the whole document while extraction carried on, and the caller got a successful result with no layout hints and a warning naming the page's pixel dimensions, which reads as a page-size limit and is not one. Each batch is now charged on its own, so the limit bounds the work in flight rather than the length of the document. The layout pass still retains every chunk's raster, and security_limits.max_content_size no longer bounds that retained set; only security_limits.max_pages does, and it is unset by default. (GH#1721)
  • (ocr): the auto device preference reports when it cannot reach the accelerator, instead of running the whole job on the CPU in silence. A failed CUDA or Metal init discarded its error and returned the CPU device with no log line naming the cause, so the run looked like a hang: the GPU stayed at 0%, several cores were busy in the forward pass, and the last log line was whatever ran before it. A warning now names the accelerator and the underlying error. GLM-OCR, PaddleOCR-VL and TrOCR already resolved the device once per engine, inside the engine pool's cold start; candle-deepseek-ocr now does the same, instead of resolving -- and warning -- once per page. A probe that fails is retried on the next page rather than remembered, so a transient accelerator failure does not pin the process to the CPU for the rest of its life. (GH#1722)
  • (pdf): a row-padded image buffer is rejected instead of panicking. ImageBuffer::from_raw rejects a buffer that is too small and accepts one that is too large, keeping the extra bytes. A decoder that pads each scanline to an alignment boundary produces exactly that, and the mismatch surfaced inside the PNG encoder as its own size assertion, so extraction panicked instead of returning an error the caller could act on. Measured on a 229x265 RGB image whose buffer held 182,320 bytes against the 182,055 the image needs: one byte of padding per row. The exact length is now checked before construction for all three pixel formats, returning the same recoverable error an undersized buffer already returned. to_png_bytes in xberg-native-pdf already carried this guard; it was never applied to this call site. (GH#1735)
  • (pdf): a page's extracted text no longer depends on the order pages are read. Reading a document's pages concurrently could return different text on different runs. Sequentially, a page whose /Font dictionary is written inline under the same resource names as an earlier page's decoded through that earlier page's fonts, so where the two fonts differed the later page read wrong. Extraction now returns the same text every time, whatever order the pages are read in, and each page decodes through the fonts its own resources name. (GH#1725)
  • (ocr): OCR with layout detection no longer drops every page of a batch. The route that OCRs pages a layout pass has already rendered charged the whole batch's render-and-encode peak against security_limits.max_content_size, a ceiling that bounds one image. Batch width on that route is the resolved thread budget, so from five US Letter pages up at the default 150 dpi the sum crossed the 100 MB default and the OCR run was refused outright; the automatic OCR fallback then returned the document's native text with a warning, which reads as a successful extraction that is missing its OCR'd text. Each page is now charged on its own, as the limit's name says. The batch's own footprint is bounded by the thread budget alone and not by max_content_size, so peak live bytes on this route rise with a wider budget. (GH#1731)
  • (pipeline): embedded images populated without OCR are no longer dropped from images. The GH#1703 fix (1.2.6) dropped every embedded image's bytes after extraction unless the caller had asked for image extraction, captioning, QR codes, inline-image OCR or page rasters. It ran on every extraction, including ones with no OCR configured at all, so a Markdown inline SVG data URI, a Jupyter output or attachment image, or an ODT/DOCX/PPTX embedded picture came back with images empty even though nothing had read those bytes for OCR. The drop now runs only when OCR was configured to run on embedded images, which is the case GH#1703 addressed. (GH#1703)
  • (ocr): candle-glm-ocr resolves its layout model once per process, not once per page. Each page re-ran the Hugging Face path resolution and checksum verification of the 131 MB PP-DocLayout-V3 model; that is now cached by directory, the same shape the layout engine's sibling models use. Measured on 22 pages: 82.71 s to 6.24 s. A failed resolution is not cached, so the next caller retries. (GH#1718)
  • (pdf): a document whose text-plausibility check could not judge any page now says so. implausible_text_pages: [] meant either that every page was checked and passed or that no page held enough prose to be checked at all (contracts, invoices, forms, agenda packets), and a caller could not tell the two apart. A document where no page could be judged now carries a processing warning naming how many pages were examined; the warning is suppressed when OCR was already forced. Extraction behaviour and OCR routing are unchanged. (GH#1709)
  • (pdf): a numbered heading that wraps twice is no longer closed after its second line. The paragraph grouper's heading-wrap exemption only recognised a heading that had absorbed exactly one wrap (visual_line_count(&current_lines) == 2), so a heading spanning three or more visual lines was cut before its own last line: the orphaned line was then welded to the body paragraph beneath it, with no signal left to separate the two on a two-column page. The exemption no longer counts wraps; it now applies at every line while the paragraph is still nothing but the heading and its accepted continuations, so a heading is closed at the first line that genuinely fails to continue it, however many wraps came before. (GH#1740)
  • (pdf): a numbered heading set as one TJ array with a kern for the tab no longer absorbs the other column's line at the top of a two-column page. When a producer places a heading's marker and title in one TJ array with a kern standing in for the tab ([(3.)-1329.5(Title )] TJ), the kern becomes a space-only span that is always regular weight regardless of the surrounding bold context, which broke the reading-order heading-run detector's clustering right at the marker/title boundary. The narrower run this produced no longer covered the marker's column position, letting an unrelated span from the other column land inside the heading. The same page set with the marker in its own text object (no kern) already read correctly; it now reads the same way regardless of how the producer set the marker. (GH#1738)
  • (config): the configured thread budget is no longer silently dropped when two extractions start together. init_thread_pools built the process-wide Rayon pool outside the call_once fence that guards it, so a second concurrent caller could be released -- with the atomics already set -- before the pool existed. If that caller reached its own parallel work before the first caller's build_global() finished, it silently installed Rayon's default pool first, and the configured max_threads was never applied for the life of the process. The pool now builds inside the same fence, so no caller observes the installed limits before the pool they describe actually exists. (GH#1750)
  • (pdf): the dense two-column repair no longer lets a table's own grid vote for the page's gutter. On a two-column page that also carries a table, detect_split_x counted a table row's internal cell gaps as gutter evidence -- a 5-column table's rows outvoted the page's real two-column lines and placed the split inside a column, and the same votes fed the hanging-label snap, so a numeric table column straddling the true gutter could be mistaken for a stack of hanging clause numbers and pull an already-correct split back into the table. Both now skip a line whose inked spans open four or more internal gaps, the shape of a table row of five or more columns; a two-column line with a hanging number on each margin opens three, so three would exclude the very lines that carry the gutter. Separately, both_sides_are_columns (gating the split's widened-corridor rescue) required both sides of a candidate gutter to classify as prose, stricter than the per-band reorder gate it feeds, which already accepts one non-prose (table) side when the two sides do not pair up row for row; it now applies the same test. A third gap remained even with both of those in place: corridor_is_hanging_label_indent read a table's own narrow edge column (cells stacked hard against the gutter's left wall) as a hanging clause number, so the widened corridor rescue still refused the page's real gutter and left the split inside the table. It now also requires an inked span on the far side of the corridor, on the same line, before counting a candidate -- a hanging label always has its clause text there; a table's edge cell never does, since the row's other cells sit on the label side and whatever text starts past the corridor belongs to an unrelated, unpaired line. Fixes the reporter's own page 1 (a five-column table with a narrow last column, plus a centred page number in the gutter); a table filling a whole column with no row-pairing across the gutter (the reporter's page 4) is fixed too. There the wrong split is crossed by only one line -- an introductory paragraph above the table, not the table itself -- because every table row's own evidence that the split sits inside it is its internal cell gap, never a span crossing the split, so the count that decides whether to widen the corridor search never reached its threshold and the search that would have found the true gutter never ran. That count now also includes a line whose split evidence is its own internal, multi-column cell gap rather than a literal crossing, so the search runs and the already-fixed corridor guards find the true gutter the same way they do on page 1. (GH#1742)

Zig

Add to your build.zig.zon:

.dependencies = .{
    .xberg-zig = .{
        .url = "https://github.com/xberg-io/xberg/releases/download/v1.2.7/xberg-zig-v1.2.7.tar.gz",
        .hash = "xberg-1.2.7-iV1Grh5GVRmMv8Z6KJNAbV-yZE22P0_XuSR0XxI5iyzW",
    },
},

Don't miss a new xberg release

NewReleases is sending notifications on new releases.