Tesseract OCR in Node.js, tested on 55 scans
55 of 55
scans, same text, no npm package
npm audit: critical, no fix
Three lines of npm audit on 2.2.1, the latest (May 2021):
node-tesseract-ocr *
Severity: critical
No fix available
The advisory, GHSA-8j44-735h-w4w2, is OS command injection: the wrapper puts the file name into one shell command. I gave one scan three names:
| Name | Wrapper | execFile |
|---|---|---|
receipt $25.jpg |
File not found | Same text |
scan$(touch pwned).jpg |
Same text, and ran touch pwned |
Same text |
say "hi".jpg |
File not found | Same text |
The same text with execFile, no shell
Install Tesseract (brew install tesseract on a Mac), then
save this as index.mjs:
import { execFile } from "node:child_process";
function ocr(input, args = ["--dpi", "300"]) {
const many = Array.isArray(input);
const piped = many || Buffer.isBuffer(input);
return new Promise((resolve, reject) => {
const child = execFile(
"tesseract",
[piped ? "stdin" : input, "stdout", ...args],
{ maxBuffer: 512 * 1024 * 1024 },
(err, text) => {
if (err) return reject(err);
resolve(many ? text.split("\f") : text);
},
);
if (piped) child.stdin.end(many ? input.join("\n") + "\n" : input);
});
}
console.log(await ocr("scan.jpg"));
13,858 of 14,693 words found, like the command
55 of 55 scans also from a Buffer
Set the dpi to match the command
My scans have no dpi in the file. The config in the wrapper's README
(lang, oem and psm) sets none,
so Tesseract guesses.
40 of 55 scans, the command's text with it
13,855 words found, 3 fewer
With dpi: 300 added, the wrapper matched the command on
every scan too.
Many small images: one call
Pass an array of paths. Tesseract reads the list from stdin and loads its model once:
const texts = await ocr(files, ["--dpi", "300", "--psm", "7"]);
| execFile | Round 1 | Round 2 |
|---|---|---|
| One call per image | 56.6 s | 55.6 s |
| One call for all | 19.6 s | 16.7 s |
All 259 lines came back as the command's text. A missing file rejects the promise. A blank image gives an empty string, so the count holds.
hOCR of many pages needs maxBuffer
hOCR of the 55 scans in one call was 1.9 MB. Node keeps 1 MB of output by default, and the wrapper has no option for more. It failed with:
stdout maxBuffer length exceeded
The function above allows 512 MB, so all of it came through.
PDFs: make gray images first
Tesseract reads no PDF. Two lines of its error:
Error in pixReadStream: Pdf reading is not supported
Error during processing.
Below the first block, make an image of each page at 300 dpi:
import { mkdtemp, readdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
const dir = await mkdtemp(join(tmpdir(), "pdf-"));
await promisify(execFile)("pdftoppm", ["-r", "300", "-gray", "scan.pdf", join(dir, "page")]);
const pages = (await readdir(dir)).sort().map((f) => join(dir, f));
const texts = await ocr(pages);
await rm(dir, { recursive: true });
1,882 of 1,955 words, 10 pages
2.3 s CPU a page
Debian in Docker: same text as apt's
FROM node:22-trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends tesseract-ocr \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY index.mjs scan.jpg ./
CMD ["node", "index.mjs"]
With the first block as index.mjs, it printed apt's text. The same
function matched apt's tesseract (5.5.0) on 5 of 5 scans.
How I tested it
- Pages: 55 proofread Wikisource scans from 11 printed books, JPEG, 1,920 pixels wide, no dpi in the file. The same set as Tesseract in PHP.
- Score: true words found in order, with the scorer of the tessdata test.
- Apple Silicon Mac, Node 22.22.2, node-tesseract-ocr 2.2.1, Tesseract 5.5.4, tessdata_fast English. Docker: Node 22.23.4.
-
Text compared byte for byte with
tesseract FILE stdout --dpi 300. - CPU time of the whole process tree, Node start included, on the Mac's efficiency cores. The wrapper's array: 18.7 s and 17.1 s.
- PDF: the first 10 scans in one PDF, JPEG bytes kept, page size for 300 dpi. pdftoppm 26.03.0.
What I did not test
- Windows, Intel Macs and x86-64 Linux.
- Bun, Deno, and Node versions before 22.
- TSV output, other languages, tessdata_best, and photos.
- tesseract.js, which runs in Node without the command. It has its own test.
Sources
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
Is node-tesseract-ocr safe to use?
Only with file names your own code made. It puts the name into a shell command, and npm audit rates that critical with no fix in 2.2.1. execFile with an array of arguments starts no shell.
What does spawn tesseract ENOENT mean?
Node cannot find the tesseract program in its PATH. Install it with brew install tesseract or apt-get install tesseract-ocr, or start Node with that folder in PATH. The wrapper says /bin/sh: tesseract: command not found for the same thing.
Can it read a PDF?
No. Tesseract reads images only. Make an image of each page first, for example with pdftoppm -r 300 -gray, then read all the pages in one call.