DEV Community

ThatToolSite
ThatToolSite

Posted on

OCR in the browser with Tesseract.js: no server, no CDN

People OCR some of the most private things they own: ID cards, payslips, contracts, medical letters. Uploading those to a random "image to text" site never felt right, so for ThatToolSite I wanted OCR that runs entirely in the browser, with no server and not even a CDN.

Tesseract.js makes that possible. Here's what it took to make it fast, private and reliable.

Self-host everything (yes, everything)

Out of the box, Tesseract.js fetches its worker script, its WebAssembly core and the language data from a CDN. That works, but it means a third party sees every visitor who uses the tool, and the tool breaks if the CDN does. We serve all of it from our own domain under /ocr:

const worker = await createWorker(lang, 1, {
  workerPath: "/ocr/worker.min.js",
  corePath: corePath(),            // SIMD or plain build, see below
  langPath: "/ocr/lang",           // tessdata_fast, gzipped
  cachePath: "tts-ocr-fast-v1",    // IndexedDB key, bump when data changes
  workerBlobURL: false,
  gzip: true,
});
Enter fullscreen mode Exit fullscreen mode

Our browser test proves it: it reads an image and fails if any request goes to another origin during OCR.

Pick the right engine build for the device

Tesseract ships a SIMD build of its WebAssembly core, which is noticeably faster where the CPU and browser support it, plus a plain fallback. We detect support with a tiny WebAssembly module that uses one SIMD instruction:

const SIMD_PROBE = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123,
  3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11]);

export function hasWasmSimd(): boolean {
  try {
    return typeof WebAssembly === "object" && WebAssembly.validate(SIMD_PROBE);
  } catch {
    return false;
  }
}

export const corePath = (simd = hasWasmSimd()) =>
  `/ocr/${simd ? "tesseract-core-simd-lstm.js" : "tesseract-core-lstm.js"}`;
Enter fullscreen mode Exit fullscreen mode

Keep the download small

Sizes matter when the whole engine runs on a phone:

File Size
WebAssembly core (SIMD or plain) about 2.9 MB
English data (tessdata_fast, gzipped) 1.97 MB
Hindi, Spanish, French, German, Portuguese 0.6 to 1.1 MB each

We use the fast language models, not the "best" ones: they're several times smaller, and for printed text the accuracy difference is small. Language data is cached in IndexedDB, so the second visit starts instantly, and a language is only downloaded when someone picks it. None of this loads until the visitor drops an image; the page itself stays light.

Help Tesseract see the text

Most bad OCR results come from the image, not the engine. Two cheap fixes made the biggest difference:

Scale to the size Tesseract likes. It reads best when letters are roughly 20 to 40 pixels tall. Small screenshots get enlarged up to 2×, and huge phone photos are shrunk so the longest side is at most 4,000 px, which also keeps it quick.

An optional clean-up pass for faint scans and dark-mode screenshots: convert to greyscale, stretch the contrast so the darkest 1% becomes black and the lightest 1% white, and flip light-on-dark images to dark-on-light. It's a few dozen lines of pixel code with no library, and it rescues a lot of "Tesseract read nothing" cases.

Don't block the page

Recognition runs in Tesseract's own Web Worker, and we reuse one worker for every image. Calls are queued, so switching language waits for the current image instead of racing it. Progress events drive a real progress bar, and the test suite checks the page stays responsive while OCR runs.

Be honest about the limits

Tesseract is excellent on printed text and weak on handwriting, and it doesn't keep table layout. The tool says that up front rather than letting people discover it after a bad result.

How we test it

The Playwright suite generates images with known text (including a dark-mode screenshot and a Spanish sentence with accents), runs them through the tool and compares the output. It also checks the worker, core and language files come from our own server.

Try it on something private: Image to text, in your browser. It's free, and the image never leaves your device.

Top comments (0)