npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@docture/loader-tesseract

v0.1.1

Published

TesseractJsLoader — OCR text and geometry for docture, powered by tesseract.js (WASM, no native binary).

Readme

@docture/loader-tesseract

DocumentLoaderTesseract is a docture DocumentLoader that reads a document by recognizing its pixels, backed by tesseract.js (Tesseract compiled to WASM; no native binary, no system install).

pnpm add @docture/loader-tesseract

OCR needs pixels, so this loader needs a way to get them: a rasterizer for a PDF, or nothing at all for an image that is already a PNG or JPEG.

import { createExtractor } from "@docture/core";
import { DocumentLoaderPdfJs } from "@docture/loader-pdfjs";
import { DocumentLoaderTesseract } from "@docture/loader-tesseract";

const extractor = createExtractor({
  loaders: [
    new DocumentLoaderPdfJs(),        // PDFs: the text layer, when there is one
    new DocumentLoaderTesseract(),  // PNG/JPEG: recognize the pixels
  ],
  strategies: [/* … */],
});

await extractor.extract("receipt-photo.jpg", Receipt);

OCR is the slowest and least accurate way to read a document, roughly 0.5 to 1.5 s per page against a text layer's fraction of a millisecond, and it guesses where a text layer knows. Reach for it when there is no text layer to read.

Wiring it next to a text-layer loader

Worth being precise about, because the obvious reading is wrong. The pipeline picks the first loader that accepts the media type and does not throw. A text-layer loader does not throw on a scan. It succeeds, reporting form: "scanned" and no text, so simply listing OCR after it is not a fallback chain, and OCR never gets a turn.

Two wirings that do work:

By media type. The example above. The two loaders cover disjoint inputs, so each document has exactly one candidate: PDFs to the text layer, photographs and scans-as-images to OCR.

As a real fallback, by making "no text layer" a thrown error, which is what hands the document to the next loader:

import { createExtractor, defineLoader } from "@docture/core";
import { NapiCanvasRasterizer } from "@docture/raster-canvas";

const pdfjs = new DocumentLoaderPdfJs();
const textLayerOrBust = defineLoader({
  name: pdfjs.name,
  capabilities: pdfjs.capabilities,
  async load(document, ctx) {
    const loaded = await pdfjs.load(document, ctx);
    // A scan is not a failure for pdf.js. It is one for us.
    if (loaded.form === "scanned") throw new Error("pdfjs: no text layer to read");
    return loaded;
  },
});

const extractor = createExtractor({
  loaders: [
    textLayerOrBust,
    new DocumentLoaderTesseract({ rasterizer: new NapiCanvasRasterizer() }),
  ],
  strategies: [/* … */],
});

Now a digital PDF is read instantly and a scanned one is OCR'd, with no branching in your code, and result.loader plus result.attempts say which happened and why.

OCR or a vision model

Both turn a scan into structured data, and they are not rivals so much as different bets.

| | OCR (DocumentLoaderTesseract) | Vision (vision({ model })) | |---|---|---| | Cost | free after the download | per page, per token | | Determinism | same bytes, same text | sampled | | What it gives you | text with positions and confidence | fields, straight from an image | | Handwriting, stamps, bad photos | poor | much better | | Works offline | yes | no |

OCR also feeds everything downstream that expects text: a deterministic parser, a cheap text-only model, a classifier. A scan becomes an ordinary document.

Capabilities

text: true, geometry: true, images: true. It reads words with positions, and because it necessarily has the pixels, page.image() works too, so a vision strategy can fall back to looking at the page without any extra composition.

mimeTypes depends on how you built it. With no rasterizer there is no honest way to read a PDF, so it is not advertised. The pipeline routes PDFs to another loader instead of choosing this one and then failing:

new DocumentLoaderTesseract().capabilities.mimeTypes;
// ["image/png", "image/jpeg"]

new DocumentLoaderTesseract({ rasterizer }).capabilities.mimeTypes;
// ["image/png", "image/jpeg", "application/pdf"]

The rasterizer is a constructor argument, not something looked up at run time, so a missing one is visible where the loader is built.

Options

| Option | Default | | |---|---|---| | rasterizer | none | how pages become pixels, required for anything that is not already PNG/JPEG | | languages | ["eng"] | traineddata languages, e.g. ["eng", "deu"]; ~5 MB each, downloaded once | | dpi | 300 | render resolution for the OCR pass; below ~200 accuracy drops sharply | | maxPages | all | cap recognition; useful for a cheap classification pass | | pageSegmentation | "auto" | "auto", "singleColumn", "singleBlock", "singleLine", "sparse" | | minConfidence | 0 | drop words below this confidence, 0 to 1 | | lineTolerance | 0.5 | vertical band for grouping words into lines, as a fraction of median word height | | autoRotate | false | detect and correct page skew first; worth it for photos and hand scans | | concurrency | 1 | pages recognized at once; each lane is a worker thread with its own engine (~100 MB) | | langPath | the CDN | directory or base URL to read <lang>.traineddata from | | cachePath | ~/.cache/docture/tesseract | where downloaded traineddata is cached | | parameters | none | raw Tesseract variables, for example { tessedit_char_whitelist: "0123456789.," } |

Notes

  • Coordinates are PDF points, top-left origin, y increasing downward, the same as every other loader. Pixel boxes are converted using the rasterizer's reported points-per-pixel, so a 300-DPI scan of an A4 page reports a 595×842 pt page.

  • Confidence rides along: every span carries confidence from 0 to 1, which a text layer cannot offer. result metadata also reports meanConfidence for the pages read.

  • Line grouping uses core's linesFromWords rather than Tesseract's own block structure. Two reasons: an OCR'd page and a text-layer page then group identically, so a parser written against one works on the other; and on a table Tesseract's layout analysis routinely puts the right-hand Amount column in its own block, which divorces a row's total from its description. Banding by y puts the row back together.

  • Page segmentation defaults to "auto" (Tesseract's --psm 3) rather than tesseract.js's own default of --psm 6. At --psm 6 layout analysis finds one uniform block and silently drops things like a page heading. There is a test for exactly that.

  • maxPages caps the OCR pass, not the render. The Rasterizer contract exposes no page count to iterate against, so pages are rendered in one call and recognition is what gets limited. Recognition is the part that costs seconds per page.

  • An image input has no intrinsic page size, so it is treated as a page scanned at dpi. page.image() then returns the bytes you passed in rather than re-encoding them.

  • The language model is downloaded on first use (~5 MB from the jsdelivr CDN) and cached outside the working directory. To run fully offline, vendor the traineddata and point langPath at it:

    new DocumentLoaderTesseract({ rasterizer, langPath: "./tessdata", cachePath: "./tessdata" });
  • dispose() terminates the worker threads. Call it, and call it once. It is idempotent, and a later load() simply starts a fresh engine.

  • tesseract.js is loaded with await import() at first use, so constructing the loader is free and a pipeline that never OCRs a page pays nothing.

Tests

The default run is offline and takes milliseconds. The conformance suite and the OCR specifics need the real engine and the network on first use, so they are behind a flag:

pnpm test                 # offline: capabilities, refusals, image sizing
RUN_OCR=1 pnpm test       # + loader conformance and OCR behaviour, against real recognition

License

MIT. tesseract.js and its models are Apache-2.0.