# Changelog of OCR Text Extractor - Image & Scanned PDF to Text (`luxe_tasks/ocr-text-extractor`) Actor

- **URL**: https://apify.com/luxe_tasks/ocr-text-extractor/changelog.md
- **Full Actor documentation**: https://apify.com/luxe_tasks/ocr-text-extractor.md

## Changelog

### 0.1.0 candidate 4.4.3 (Oct 26, 2026)

- Built-in sample page: a run with no files (`files` missing, null or empty, or no input at all) now reads only a bundled synthetic
  one-page PDF (`assets/sample-page.pdf`, made by `scripts/make-sample-page.mjs`; its sha256 is checked at run time) and writes one
  dataset record marked `sample: true`. Sample pages are never charged (no `ocr-page` event); Apify's start fee still applies. Before,
  such a run failed with "files must be a list". No attestation is needed for the sample; the other input fields are still validated.
- A run with any files works exactly as before and never includes the sample. File URLs still need `documentRightsAttestation`
  (default false), and the SSRF, robots.txt, URL-cap and run limits are unchanged.
- Input schema: `files` is no longer required (so an empty run from the Store reads the sample); its description says so.
- README: the Input, Pricing and Output sections mention the sample. The dataset's Results view shows the `sample` column.
- A restarted sample run that had already finished writes no second sample record.
- Start-up log line (wording only): when the requested concurrency is reduced, the line now prints the true number of files that fit
  the formula. At 1024 MB and the default concurrency 3 it said "976 MB fits 1 file(s) at once" although 0 fit; it now says
  "976 MB fits 0 file(s) at once (...; minimum 1)". Effective concurrency and memory limits are unchanged (still 1 file, 679 MB).
- Sample run messages: the final status message now reads "Sample page read (success); sample pages are not charged (Apify's start fee
  still applies)." If the bundled sample file is missing or damaged, a run with no files now fails with a one-line status message instead
  of an error trace (no dataset items, no pages charged). Runs with files are not affected.
- Output schema added (`.actor/output_schema.json`): links to the dataset's Results view, the key-value store's text records and RUN_SUMMARY.
- Source comments and this changelog no longer cite internal review item numbers. No behaviour change.

### 0.1.0 candidate 4.4.2 (Oct 26, 2026)

- Wording only, in this changelog: the 0.1.0 limits note now says which pages are charged when a limit stops a file (pages stopped
  by a limit are not charged; pages already read before a run limit stopped the file are; a `render_memory_limit` file is not charged
  at all), and the 4.4 note says the start-up line's memory source label changed on Apify (numbers unchanged). No code change.

### 0.1.0 candidate 4.4.1 (Oct 25, 2026)

- Wording only: a source comment in the memory module and the 4.4 notes below. No code change (same behaviour as 4.4).

### 0.1.0 candidate 4.4 (Oct 25, 2026)

- Memory units corrected: the platform's memory setting is decimal MB (a 1024 MB run has a container limit of 1,024,000,000
  bytes = 976 MiB). The cgroup reading (bytes / 1,048,576) was already right; the memory environment variables are now converted the
  same way (1024 -> 976 MiB) instead of being used as MiB, so a run without a readable cgroup limit plans the same budget as one with
  it. No numeric change on Apify, where the cgroup limit is read (same concurrency and limits as 4.3; only the start-up line's memory
  source label changes, see below). The 4.3 note below had the units backwards.
- The start-up log line says "fits 0, minimum 1" when not even one file fits the formula (1024 MB runs).
- Start-up log line on Apify: it now says "memory from ACTOR_MEMORY_MBYTES" (4.3 said "memory from cgroup /sys/fs/cgroup/memory.max
  (below ACTOR_MEMORY_MBYTES=1024)"). The converted setting (976 MiB at 1024 MB) now equals the container limit, so it is no longer
  overridden by it; the memory, concurrency and limits in the line are the same as in 4.3.
- Fix for runs where the container limit cannot be read (memory taken from ACTOR_MEMORY_MBYTES / APIFY_MEMORY_MBYTES only): Apify's
  MB are 10^6 bytes, but 4.3 used the number as MiB (2^20 bytes), so a 1024 MB run was planned as 1,073,741,824 bytes against a real
  limit of 1,024,000,000: about 5% too much, roughly the whole 5% headroom. 4.4 plans 976 MiB (49 MiB headroom, renderer limit
  679 MiB), the same as when the limit is read.

### 0.1.0 candidate 4.3 (Oct 25, 2026)

- Decompression-bomb check before rendering now measures LZW and RunLength streams (before, they counted at their stored size), and
  Brotli; filters it cannot measure are refused (`file_bomb`, not charged): unknown filter names, and image formats (JPEG, JPEG 2000,
  CCITT, JBIG2) anywhere but as the last filter of an image. The filter is read the way the PDF renderer reads it (`/F` before
  `/Filter`, LZW EarlyChange).
- Object streams are decoded with the same limits: one that expands to more than 32 MB, or all of them together to more than 64 MB, is
  refused (`file_bomb`); before, such an object stream was skipped and the pages inside it were not checked.
- The container memory limit is read in MiB (bytes / 1,048,576); before it was divided by 1,000,000. (Corrected in 4.4: the platform's
  setting is decimal MB, not MiB; a 1024 MB run's limit of 1,024,000,000 bytes was read as 1024 MB before and 976 MiB now.)

### 0.1.0 candidate 4 (Oct 25, 2026)

- Store description: "English OCR for PNG, JPEG, PDF. Pages are OCR'd as images; PDF text layers aren't copied. Charged per page with
  text; $0.005/run start fee +$0.005/extra GB."
- Memory: the number of files processed at once is reduced so that the worst case (every file rendering a PDF page and running OCR at
  the same time) fits in the run's memory with headroom: 1 file at 1 GB, 2 at 2 GB, 4 at 4 GB (was 5). Memory is read from the
  platform's memory setting or the container limit. The start-up log shows the requested and the effective number.
- PDF renderers and OCR processes of a run now share one memory budget. If together they pass it, the largest renderer is stopped and
  that file ends as `render_memory_limit` (not charged). Each renderer is still limited to 700 MB.
- Decompression-bomb check before rendering, extended: filters given indirectly, as arrays or as chains (Flate, ASCIIHex, ASCII85), page
  objects stored inside object streams and escaped filter names are understood; a page is refused (`file_bomb`, not charged) when its
  content streams and forms together expand to more than 160 MB, as well as when a single stream does. The renderer memory limit stays
  as the backstop for anything the check does not cover.
- Fixed a rare extra page result when the renderer process was slow to shut down, or exited, after the last page.
- File URL mode: a robots.txt that returns 429 is retried once only if Retry-After is 60 s or less; otherwise (or on a second 429) the
  domain is stopped: the first file is `rate_limited`, the rest `skipped_blocked`, no file is requested and nothing is charged. Before,
  a robots.txt 429 counted as "no restrictions", like other 4xx. README states the robots.txt status rules.

### 0.1.0 candidate 3 (Oct 25, 2026)

- Pricing text: $4 per 1,000 pages with readable text (event `ocr-page`) plus a $0.005 start fee per run (Apify's `apify-actor-start`
  event, charged once up to 1 GB of memory and once per extra GB). The Store description now discloses the start fee.
- PDF renderer memory limit: the renderer process is stopped when it uses more than 700 MB of memory; the file ends as
  `render_memory_limit` and is not charged, including pages already read. PDFs with a compressed stream that expands to more than
  160 MB are refused before rendering (`file_bomb`). Both stop small "decompression bomb" files that could exhaust memory.
- Uncharged work is now counted per page: blank or unreadable pages inside files that also have readable pages count toward the run
  limit, which is now 25 files, 500 pages or 10 minutes of work without readable text. A file in progress stops at its next page when
  the limit is reached (remaining pages `uncharged_work_limit`). With several files in progress, at most one extra page per other file.
- Source package: tests, test fixtures, benchmarks, internal docs and local scripts are no longer included in the source archive,
  `apify push` upload or Docker build context.

### 0.1.0 (unreleased)

- First version: local Tesseract OCR for PNG, JPEG and PDF (English).
- Every page, including PDFs with selectable text, is rendered and read with OCR. Reading a PDF's embedded text layer first
  ("text-layer-first") is **not implemented**. If it ships, this README and this changelog will be updated together.
- Limits on PDF rendering (per-page render time, per-file time, drawing operations per page, renderer memory) and per run
  (run time, uncharged work).
  Pages stopped by a limit are never charged; readable pages already read before a run limit stopped the file are kept and charged (a file stopped by `render_memory_limit` is not charged at all).
- Kit synced to actor-kit 0.5.1-final5 (tag kit-v0.5.1-final5): uncertain charges count against the max-charge budget; a restarted
  run marks a possibly-charged file `charge_uncertain` instead of re-charging it; a redirected robots.txt is applied to the original
  host and an undetermined robots answer counts as disallowed; with pay-per-event pricing, a run whose pricing lacks the `ocr-page`
  event stops before any work, with nothing charged.
- English model: the Docker image uses Alpine's `tesseract-ocr-data-eng` 5.5.2-r0 (tessdata 4.1.0, `/usr/share/tessdata`) instead of a
  bundled tessdata_fast file. Local accuracy on the synthetic corpus was the same (mean CER 0.0483 vs 0.0484, 12 pages charged in both).
- Dockerfile: Tesseract and its model are pinned to 5.5.2-r0 and checked at build time; app files are owned by the runtime user.
  `.dockerignore` added (local test output, `node_modules` and `.git` are not copied into the image). The Docker build is untested locally.
- URLs in per-file error messages have their query strings redacted, like everywhere else.
- Files refused before processing (download failed, unsupported type, too large, URL caps) no longer count toward the uncharged-work limit.
- The PDF renderer's start-up no longer counts against the per-page render timeout (it has its own 20 s budget, still within the file limit).
- README: advice on choosing uploads vs file URL mode.
