Tesseract OCR Free code examples in C#, Python, Java and JavaScript
Open-source OCR — no API key, no network, no per-page cost.
WHAT THIS PAGE COVERS
Pre-processing a scan so the output is usable, which matters more than the engine.
eng
pytesseract
Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.
The NuGet package carries the native engine, but NOT the language data. tessdata is a separate download and the directory path is a constructor argument — getting it wrong is the usual first error.
dotnet add package Tesseract
# Language data, separately. Put it in ./tessdata next to the exe.
mkdir tessdata
curl -L -o tessdata/eng.traineddata \
https://github.com/tesseract-ocr/tessdata/raw/main/eng.traineddata
TesseractEngine is expensive and NOT thread-safe. One per process behind a lock, or one per worker — never one per request, which is the shape that looks fine until it is under load.
using Tesseract;
// "eng" must match a file called eng.traineddata in ./tessdata.
using var engine = new TesseractEngine(@"./tessdata", "eng", EngineMode.LstmOnly);
using var image = Pix.LoadFromFile("invoice.png");
using var page = engine.Process(image);
Console.WriteLine(page.GetText());
Console.WriteLine($"mean confidence: {page.GetMeanConfidence():P0}");
GREYSCALE, DESKEW, THRESHOLD, UPSCALE. Leptonica ships inside the package and does all four — which means the thing that most improves OCR quality needs no extra dependency at all.
using var original = Pix.LoadFromFile("invoice.png");
// Greyscale, then Otsu thresholding for a clean black-and-white page.
using var grey = original.ConvertRGBToGray();
using var binary = grey.BinarizeOtsuAdaptiveThreshold(
2000, 2000, 0, 0, 0.0f);
// Deskew. FindSkew returns the angle Leptonica detected.
using var deskewed = binary.Deskew(out var skew);
Console.WriteLine($"corrected {skew.Angle:F2} degrees");
using var engine = new TesseractEngine(@"./tessdata", "eng", EngineMode.LstmOnly);
using var page = engine.Process(deskewed);
Console.WriteLine(page.GetText());
PageSegMode is the second biggest lever after pre-processing. The default assumes a full page; a receipt, a single label and a screenshot each want something else.
using var engine = new TesseractEngine(@"./tessdata", "eng", EngineMode.LstmOnly);
// SingleBlock — a uniform block of text (most scanned documents)
// SingleLine — one line, e.g. a label or a form field
// SparseText — text with no particular order, e.g. a screenshot
using var page = engine.Process(image, PageSegMode.SingleBlock);
// Per-word confidence, so you know which parts to distrust.
using var iterator = page.GetIterator();
iterator.Begin();
do
{
var word = iterator.GetText(PageIteratorLevel.Word);
var confidence = iterator.GetConfidence(PageIteratorLevel.Word);
if (!string.IsNullOrWhiteSpace(word) && confidence < 60)
{
Console.WriteLine($"suspect: {word} ({confidence:F0})");
}
}
while (iterator.Next(PageIteratorLevel.Word));
TWO INSTALLS. pytesseract is a wrapper around a binary you must also install — pip alone gives you a module that raises TesseractNotFoundError on first use. Insist on Tesseract 5; v3 was pattern matching, v4 and v5 are neural.
# The engine itself
sudo apt install tesseract-ocr tesseract-ocr-hin # Debian / Ubuntu
brew install tesseract tesseract-lang # macOS
winget install UB-Mannheim.TesseractOCR # Windows
# The wrapper
pip install pytesseract pillow opencv-python
tesseract --version # expect 5.x
Three lines, and this is the whole API for the common case. If it raises TesseractNotFoundError the binary is not on PATH — on Windows, point pytesseract.tesseract_cmd at the exe.
import pytesseract
from PIL import Image
# On Windows the binary is usually not on PATH:
# pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
text = pytesseract.image_to_string(Image.open("invoice.png"), lang="eng")
print(text)
GREYSCALE, DESKEW, THRESHOLD, UPSCALE TO 300 DPI. Tesseract wants black text on white at roughly 300 DPI, and a phone photo is none of those. This is worth more than any engine swap.
import cv2
import numpy as np
def prepare(path: str):
image = cv2.imread(path)
grey = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Deskew: find the dominant text angle and rotate it flat.
coords = np.column_stack(np.where(grey < 200))
angle = cv2.minAreaRect(coords)[-1]
if angle < -45:
angle += 90
height, width = grey.shape
matrix = cv2.getRotationMatrix2D((width / 2, height / 2), angle, 1.0)
grey = cv2.warpAffine(grey, matrix, (width, height),
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE)
# Adaptive threshold beats a global one on uneven lighting.
grey = cv2.adaptiveThreshold(
grey, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 31, 15)
# Tesseract wants ~300 DPI. Upscale small scans.
if max(grey.shape) < 1800:
grey = cv2.resize(grey, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
return grey
print(pytesseract.image_to_string(prepare("invoice.png"), lang="eng"))
--psm is the page segmentation mode and the default assumes a full page of text. A single line, a receipt column or a form field each need a different one, and the wrong mode is the second most common cause of bad output.
# 6 = a uniform block of text (most scanned documents)
# 7 = a single line (a label, a field)
# 11 = sparse text, no order (a screenshot, a diagram)
config = "--psm 6 --oem 1" # oem 1 = the LSTM engine only
text = pytesseract.image_to_string(
prepare("invoice.png"), lang="eng", config=config)
# Hindi and English together — order matters, the first is preferred.
bilingual = pytesseract.image_to_string(
prepare("notice.png"), lang="hin+eng", config=config)
image_to_data gives a per-word confidence. Anything below about 60 is a guess, and knowing WHICH words are guesses is the difference between an OCR pipeline and a random text generator.
data = pytesseract.image_to_data(
prepare("invoice.png"),
lang="eng",
output_type=pytesseract.Output.DICT,
)
suspect = [
(word, int(confidence))
for word, confidence in zip(data["text"], data["conf"])
if word.strip() and int(confidence) < 60
]
print(f"{len(suspect)} low-confidence words:", suspect[:10])
Tess4J is a JNA wrapper, so it carries native binaries and needs the Visual C++ redistributable on Windows. Language data is a separate download, and the datapath must point at the folder CONTAINING tessdata.
# Gradle
implementation("net.sourceforge.tess4j:tess4j:5.13.0")
mkdir -p tessdata
curl -L -o tessdata/eng.traineddata \
https://github.com/tesseract-ocr/tessdata/raw/main/eng.traineddata
setDatapath takes the PARENT of tessdata, not tessdata itself. That off-by-one directory is the most common Tess4J error, and the message only says it could not initialise.
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import java.io.File;
public class Read {
public static void main(String[] args) throws Exception {
ITesseract tesseract = new Tesseract();
// The folder CONTAINING tessdata, not tessdata itself.
tesseract.setDatapath("./");
tesseract.setLanguage("eng");
tesseract.setOcrEngineMode(1); // LSTM only
String text = tesseract.doOCR(new File("invoice.png"));
System.out.println(text);
}
}
GREYSCALE, THRESHOLD, UPSCALE TO ~300 DPI. Tess4J bundles ImageHelper for exactly this, and it is worth more than any parameter on the engine.
import net.sourceforge.tess4j.util.ImageHelper;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
BufferedImage source = ImageIO.read(new File("invoice.png"));
// Upscale toward 300 DPI — small scans are the usual problem.
BufferedImage scaled = ImageHelper.getScaledInstance(
source, source.getWidth() * 2, source.getHeight() * 2);
// Greyscale, then a hard black-and-white threshold.
BufferedImage grey = ImageHelper.convertImageToGrayscale(scaled);
BufferedImage binary = ImageHelper.convertImageToBinary(grey);
String text = tesseract.doOCR(binary);
System.out.println(text);
setPageSegMode is the lever after pre-processing. Multiple languages go in one string joined by plus signs, and the order is a preference rather than a filter.
// 6 = a uniform block of text (most scanned documents)
// 7 = a single line (a label, a form field)
// 11 = sparse text, no order (a screenshot)
tesseract.setPageSegMode(6);
// Hindi first, English as the fallback. Both .traineddata files
// must be present in the tessdata folder.
tesseract.setLanguage("hin+eng");
String text = tesseract.doOCR(binary);
getWords returns a confidence per word. Below about 60 is a guess — and an OCR pipeline that cannot say which words it doubts is one that quietly invents invoice totals.
import net.sourceforge.tess4j.ITessAPI;
import net.sourceforge.tess4j.Word;
import java.util.List;
List<Word> words = tesseract.getWords(
binary, ITessAPI.TessPageIteratorLevel.RIL_WORD);
words.stream()
.filter(word -> word.getConfidence() < 60)
.forEach(word -> System.out.printf(
"suspect: %s (%.0f)%n", word.getText().trim(), word.getConfidence()));
tesseract.js is pure JavaScript — it carries a WebAssembly build of the engine, so there is no binary to install and it runs in the browser as well as Node. That convenience costs speed against the native binding.
npm install tesseract.js
# Optional, for the pre-processing step below.
npm install sharp
recognize() downloads the language data on first use and caches it. The first call is slow for that reason, not because the page is hard.
import Tesseract from "tesseract.js";
const { data } = await Tesseract.recognize("invoice.png", "eng", {
logger: (message) => console.log(message.status, message.progress),
});
console.log(data.text);
GREYSCALE, NORMALISE, THRESHOLD, UPSCALE. Tesseract wants black on white at roughly 300 DPI. A phone photo of a page is grey on grey at 72 — fix that and the engine stops being the problem.
import sharp from "sharp";
async function prepare(path) {
const image = sharp(path);
const { width = 0 } = await image.metadata();
return image
.greyscale()
.normalise() // stretch the histogram
.threshold(150) // hard black and white
.resize({ width: Math.max(width * 2, 1800) }) // toward 300 DPI
.png()
.toBuffer();
}
const { data } = await Tesseract.recognize(await prepare("invoice.png"), "eng");
console.log(data.text);
recognize() spins up a worker, loads the language data and tears it all down again. For more than one page that is most of the runtime — create the worker once.
import { createWorker } from "tesseract.js";
const worker = await createWorker("eng");
// psm 6: a uniform block of text, which is most scanned documents.
await worker.setParameters({ tessedit_pageseg_mode: "6" });
for (const page of ["page-1.png", "page-2.png", "page-3.png"]) {
const { data } = await worker.recognize(await prepare(page));
console.log(page, data.text.slice(0, 80));
}
await worker.terminate();
data.words carries a confidence per word. Below about 60 is a guess — surface those rather than presenting the whole transcript as though it were read correctly.
const { data } = await worker.recognize(await prepare("invoice.png"));
const suspect = (data.words ?? [])
.filter((word) => word.confidence < 60)
.map((word) => [word.text, Math.round(word.confidence)]);
console.log(`${suspect.length} low-confidence words`, suspect.slice(0, 10));
QUESTIONS
Which languages are the Tesseract OCR examples in?
C#, Python, Java and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.
Which package do I install for Tesseract OCR?
The samples use pytesseract. The install step on each language tab gives the exact command.
Which Tesseract OCR model do these examples use?
eng, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.
Where is the official Tesseract OCR documentation?
https://tesseract-ocr.github.io/ — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.
ALSO IN IMAGE & VIDEO
The authority
Official Tesseract OCR documentation
Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.
https://tesseract-ocr.github.io/