@familis/scribe-tesseract
v0.3.0
Published
Tesseract.js OCR adapter for @familis/scribe
Readme
@familis/scribe-tesseract
Local OCR adapter for @familis/scribe, backed by Tesseract.js and Sharp. It accepts page bitmaps,
prepares PNG input, reuses OCR workers, and returns normalized positioned tokens.
Tesseract.js does not read PDF files directly. Use a PDF adapter such as
@familis/scribe-pdfium to render pages first.
Installation
pnpm add @familis/scribe @familis/scribe-tesseractLanguage data
languageDataPath is required. Point it to an explicitly configured Tesseract.js-compatible
language-data location. A local directory is recommended for production:
/opt/familis/tessdata/
├── eng.traineddata.gz
└── fra.traineddata.gzBy default the adapter reads gzipped <lang>.traineddata.gz files. For a directory of plain
<lang>.traineddata files, the layout tessdata is distributed in, set compressed: false:
/opt/tessdata/
├── eng.traineddata
└── fra.traineddataconst ocr = await createTesseractEngine({
languageDataPath: "/opt/tessdata",
compressed: false,
});When languageDataPath is a local directory, the adapter checks that every requested language file
exists and is readable before it starts a worker. A missing file rejects recognize with OcrError,
whose message names the file and the configured path. Remote locations are left to Tesseract.js: a
file it cannot fetch also rejects with OcrError, whose cause carries the Tesseract.js report.
The adapter never selects a remote language-data source or automatically detects a language. Every document profile must declare its languages.
Usage
import { createScribe } from "@familis/scribe";
import { createPdfiumEngine } from "@familis/scribe-pdfium";
import { createTesseractEngine } from "@familis/scribe-tesseract";
const scribe = createScribe({
pdf: await createPdfiumEngine(),
ocr: await createTesseractEngine({
languageDataPath: "/opt/familis/tessdata",
concurrency: 1,
}),
});
try {
const result = await scribe.parse(bytes, profile, { ocr: "auto" });
console.log(result.data);
} finally {
await scribe.close();
}Options
| Option | Required | Description |
| --------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| languageDataPath | Yes | Local path or explicitly configured Tesseract.js language-data location. |
| compressed | No | Whether language files are gzipped (.traineddata.gz). Defaults to true. |
| cachePath | No | Writable directory for the language-data cache. Omitted, no cache is used. |
| workerPath | No | Custom Tesseract.js worker script location. |
| corePath | No | Custom Tesseract.js core/WASM location. |
| concurrency | No | Number of OCR workers per language set. Defaults to 1. |
| logger | No | Receives Tesseract.js progress messages. |
| upscale | No | Lanczos upscaling of low-DPI bitmaps, or false. Defaults to { targetDpi: 300, maxFactor: 4 }. |
| preprocess | No | { threshold, sharpen } image adjustments before recognition. All off by default. |
| pageSegMode | No | Tesseract page segmentation mode (PSM). Defaults to Tesseract's PSM.AUTO. |
| minWordConfidence | No | Drops words whose confidence (0–1) is below this value. Defaults to 0, keeping all. |
| dropPunctuationOnly | No | Drops words with no letter or digit, such as \| or '. Defaults to false. |
Without cachePath, Tesseract.js reads languageDataPath directly and writes nothing. Setting it
makes Tesseract.js keep a copy of each language it loads there and read it back on later starts.
That only pays off with a remote languageDataPath: with a local directory, the cache copies one
local file to another.
const ocr = await createTesseractEngine({
languageDataPath: "/opt/tessdata",
logger(message) {
console.log(message.status, message.progress);
},
});Worker reuse and concurrency
Workers are pooled by normalized language set. Requests for ["fra", "eng"] and
["eng", "fra"] reuse the same pool.
concurrency controls the number of workers in each pool. Each application process or container
creates its own pools, so total OCR concurrency is:
application processes × language sets used × configured concurrencyStart with concurrency: 1 and increase it only after measuring CPU and memory consumption. Use an
application-level queue or request limit when many documents can arrive simultaneously.
Image preparation
The adapter accepts the core PageBitmap contract:
interface PageBitmap {
data: Uint8Array;
width: number;
height: number;
format: "gray8" | "rgba8";
dpi?: number;
}Sharp prepares each bitmap in this order before encoding it as PNG:
- Upscale. A bitmap whose
dpiis belowupscale.targetDpi(300 by default) is enlarged with a Lanczos kernel bytargetDpi / dpi, capped atupscale.maxFactor(4 by default). A 96 DPI bitmap is enlarged by 3.125 to 300 DPI; a 50 DPI one by 4, to 200 DPI. A bitmap at or above the target, or withoutdpi, keeps its size. preprocess. Sharpening and thresholding, when configured, run on the upscaled pixels.
The resulting density is written into the PNG metadata and passed to Tesseract as
user_defined_dpi: the bitmap dpi times the upscale factor, or 300 DPI when dpi is unknown.
Tesseract also runs with preserve_interword_spaces enabled, so multi-word values keep their
spacing.
Tesseract reads best at about 300 DPI of glyph detail, and a low-resolution input, such as an
embedded 96 DPI image, reaches it far below that. A proper resampling kernel recovers much of what
thresholding cannot: a high cutoff breaks strokes that the plain upscale reads correctly. Set
upscale: false to send every bitmap at its own size:
const ocr = await createTesseractEngine({
languageDataPath: "/opt/tessdata",
upscale: { targetDpi: 300, maxFactor: 4 },
});targetDpi must be a finite number above 0, and maxFactor a finite number of at least 1.
OCR tokens are normalized to top-left coordinates between 0 and 1 of the input bitmap, whatever
the upscale, with confidence values between 0 and 1.
The standard PDFium adapter supplies the requested render density. The core pipeline renders OCR pages in grayscale at 300 DPI, which upscaling leaves untouched, and skips visually blank pages before invoking this adapter.
Preprocessing and page segmentation
By default the bitmap reaches Tesseract as rendered, and Tesseract picks its own page layout. Two options change that:
import { createTesseractEngine, PSM } from "@familis/scribe-tesseract";
const ocr = await createTesseractEngine({
languageDataPath: "/opt/tessdata",
preprocess: {
threshold: 160, // binarize: gray levels >= 160 become white, the rest black
sharpen: false,
},
pageSegMode: PSM.SINGLE_BLOCK,
});preprocess.thresholdbinarizes the image before PNG encoding. It helps with low-resolution bitmap text, such as a 96 DPI header embedded in a PDF, where gray anti-aliasing blurs digits together. It must be an integer from0to255;nullor omitted leaves the image untouched. Too high a cutoff thins or breaks light strokes, too low a cutoff merges them, so tune it against your own documents.preprocess.sharpenapplies a mild sharpen. It runs before thresholding when both are set.pageSegModetells Tesseract how the image is laid out.PSM.AUTOsuits full pages;PSM.SINGLE_BLOCKorPSM.SINGLE_LINEsuit a cropped region holding one block or one line of text.PSMis re-exported, so there is no need to import Tesseract.js directly.
These settings apply to every page the engine recognizes. Measure them on representative documents before enabling them: they help degraded bitmap text and can hurt clean scans.
Word filtering
Every word Tesseract returns becomes a token by default, however unsure Tesseract is of it. On
degraded input, that lets noise through: a stray q from a speck, a | from a table border, a lone
apostrophe. Such tokens join selected text, split lines and anchors, and pull down the confidence of
the spans they touch. Two options drop them before the tokens are returned:
const ocr = await createTesseractEngine({
languageDataPath: "/opt/tessdata",
minWordConfidence: 0.3,
dropPunctuationOnly: true,
});minWordConfidencedrops words whose confidence, scaled to0–1, is below the given value. It must be a number from0to1; the default0keeps every word.dropPunctuationOnlydrops words that contain no Unicode letter or digit, such as|,',—or.. Words mixing symbols with letters or digits, such asN°or1/2, are kept.
The page-level confidence stays Tesseract's own, computed over every word before filtering.
recognize reports the number of words it dropped as droppedTokenCount, 0 when none were, and
the core pipeline surfaces it in each page diagnostic.
Languages
Languages come from the active profile:
const profile = defineProfile({
id: "multilingual-document",
version: "1",
languages: ["fra", "eng"],
schema,
fields,
});All declared language files must be available beneath languageDataPath using the layout expected
by Tesseract.js.
Direct engine usage
const result = await ocr.recognize(bitmap, {
languages: ["fra"],
signal,
});
console.log(result.tokens, result.confidence);
await ocr.close();close() terminates every initialized scheduler and worker. It is idempotent.
Errors and cancellation
- Initialization and recognition failures produce
OcrError. - Missing languages produce
OcrError. - An already aborted operation, or an abort while awaiting recognition, produces
AbortError. - Calling the engine after
close()producesDisposedError.
An abort rejects the caller immediately. Tesseract.js work already executing inside a worker may finish internally before that worker accepts another job.
A missing local language file is checked again on every call, so data deployed after the failure is picked up without a restart, and no worker is started until it is there.
Other initialization failures, such as corrupt or unreachable language data, happen inside a worker.
Tesseract.js offers no way to terminate a worker whose language data failed to load, so its thread
stays idle and keeps Node.js from exiting on its own. To keep that to one failed start per language
set, the engine remembers the failure: later calls for the same languages reject with the same
OcrError without starting new workers. Fix the configuration and create a new engine to retry.
Container deployment
No system tesseract-ocr executable is required. Install dependencies inside the target Linux
image so Sharp selects the correct platform binary, and copy language data into the image. With
local language data, leave cachePath unset: no writable directory is needed.
ENV TESSDATA_PATH=/opt/familis/tessdata
COPY resources/tessdata/ /opt/familis/tessdata/Do not copy node_modules from macOS or Windows into a Linux image.
Runtime support
- Node.js 22.12 or newer.
- Current stable Bun.
- Server runtimes only; browser and edge use are not supported in v1.
License
MIT
