Tools,

Tesseract OCR in Node.js, tested on 55 scans

55 of 55

scans, same text, no npm package

See the code

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:

One scan, three file 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"]);
CPU seconds, 259 line images
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.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one 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.