Tools,

Best DPI for OCR, tested from 72 to 600 dpi

Scan print at 200 to 300 dpi in gray, and skip 600. On 55 printed book pages, Tesseract kept 99.9% of its words until the small letters fell under 8 pixels tall, which is about 120 dpi for 10 point type. On 72 clean invoices, 240 dpi read the most numbers, 2,374 of 2,418. 600 dpi read 2,310 for 2.7 times the CPU of 300. The 300 dpi rule belongs to black and white scans, which need about twice the pixels: at 150 dpi, gray read 2,316 numbers and black and white 1,899. If your text is already too small, doubling the image took my smallest book test from 9,711 words to 13,554.

Try it on your PDF, $0.006 a page

The results

I have no scanner, so I made two sets of pages with known text and read them with Tesseract 5.5.3 and its default English model.

Invoice dpi Clean, gray Clean, black and white Rough, gray Rough, black and white CPU seconds, 72 clean gray pages
72 1,307 163 932 132 12.1
100 2,225 1,059 2,101 665 14.1
120 2,314 1,435 2,246 1,164 15.9
150 2,316 1,899 2,231 1,553 17.6
200 2,358 2,153 2,142 2,031 20.4
240 2,374 2,187 2,127 2,140 23.3
300 2,337 2,199 2,079 2,201 27.8
400 2,316 2,263 1,952 2,256 38.8
600 2,310 2,154 1,822 2,278 76.4

The book scans tell the same story, and they are real. Each row is the same 55 pages at a smaller size. Small letters here means the height of words like some, which have no tall or hanging letters.

Scale Small letters, pixels Gray, words of 14,693 Black and white, words
100% 21 to 39 13,858 13,762
80% 17 to 31 13,862 13,763
67% 14 to 26 13,872 13,745
50% 11 to 20 13,891 13,695
40% 8 to 16 13,884 13,271
33% 7 to 13 13,763 12,402
25% 5 to 10 13,208 9,863
20% 4 to 8 9,711 6,030

Count pixels, not dpi

A dpi number means little without a type size. What Tesseract sees is the height of the small letters in pixels, the x-height. A quick estimate:

x-height in pixels = dpi * point size / 150

My four invoice fonts came within 3 pixels of that at 300 dpi. Here is what each height did to the book pages. Kept compares each page with the same page at full size, so the mix of books does not move the number.

Small letters, pixels Gray words kept
12 to 14.9 100.33%
10 to 11.9 100.10%
8 to 9.9 99.93%
7 to 7.9 98.31%
6 to 6.9 94.81%
5 to 5.9 73.77%
Under 5 10.86%

That gives this table, from the estimate above. The last column comes from the books: black and white held 98.8% of the gray full size score at a median of 15.5 pixels, then fell fast, to 95.8% at 12.4 and 71.2% at 7.8.

Type size Gray, lowest I would go (8 px) Gray, comfortable (12 to 16 px) Black and white (16 px)
6 point 200 dpi 300 to 400 dpi 400 dpi
8 point 150 dpi 225 to 300 dpi 300 dpi
10 point 120 dpi 180 to 240 dpi 240 dpi
12 point 100 dpi 150 to 200 dpi 200 dpi

If you do not know the type size, 300 dpi in gray is a safe default. It costs little, and it covers small print.

Why do more pixels stop helping? tessdata_fast and tessdata_best both read a text line at a fixed height of 36 pixels. You can see it in each model's own network string, [1,36,0,1Ct3,3,16Mp3,3Lfys48... for fast and [1,36,0,1Ct3,3,16Mp3,3Lfys64... for best, and in input.cpp, which scales each line to that height. My book lines stood about 2 times the height of their small letters, so small letters of 18 pixels already fill it. Past that, Tesseract shrinks the line itself.

The docs say otherwise in three places, so here is how they did:

Give Tesseract the true dpi

This can cost more than any choice of dpi. Here is the same set with three ways of telling Tesseract the resolution:

Rough invoices No dpi in the file, no flag True dpi --dpi 300 on everything
72 dpi 901 932 604
100 dpi 2,143 2,101 1,758
150 dpi 2,145 2,231 2,224
200 dpi 1,576 2,142 2,129
300 dpi 460 2,079 2,079
600 dpi 437 1,822 1,826

Check what a file says, then pass the number:

from PIL import Image

print(Image.open("scan.jpg").info.get("dpi"))

None means the file says nothing. These gave the same output:

tesseract scan.png out --dpi 200
tesseract scan.png out -c user_defined_dpi=200
import pytesseract

text = pytesseract.image_to_string("scan.png", config="--dpi 200")

The flag always beats the file. Or write the dpi into the file, and plain tesseract needs no flag:

from PIL import Image

img = Image.open("scan.png")
img.save("scan-200.png", dpi=(200, 200))

Upscale only tiny text, and only 2 times

This is the test for files you cannot scan again. Lanczos, with the true dpi passed each time:

Pages As is 2x 3x 4x
Books, small letters 4 to 8 px 9,711 13,554 13,544 13,492
Books, 5 to 10 px 13,208 13,786 13,739 13,801
Books, 7 to 13 px 13,763 13,876 13,830 not run
Books, 8 to 16 px 13,884 13,863 not run not run
Books, full size 13,858 13,804 not run not run
Rough invoices, 72 dpi 932 1,983 1,964 1,998
Rough invoices, 100 dpi 2,101 2,283 2,268 not run
Rough invoices, 150 dpi 2,231 2,172 not run not run
Rough invoices, 200 dpi 2,142 2,003 not run not run

So the line sits at about 10 pixels. Under it, 2 times the size is a big win, and 3 or 4 times adds next to nothing. Over it, leave the image alone. Bicubic did about the same as Lanczos: 13,484 against 13,554 words, 13,800 against 13,786, and 1,978 against 1,983 numbers.

This code does that for you. It reads the page once and estimates the x-height. If that is under 10 pixels, or no word is sure enough to measure, it reads again at 2 times the size. Pages that are big enough cost nothing extra. Save it as read_scan.py:

import re
import statistics

import pytesseract
from PIL import Image
from pytesseract import Output

LOW = re.compile(r"[acemnorsuvwxz]{2,}")  # no tall or hanging letters
DIGITS = re.compile(r"[0-9]{2,}")  # digits stand about 1.4 times the x-height


def x_height(d):
    """Estimated x-height in pixels, from the output of image_to_data."""
    heights = []
    for text, height, conf in zip(d["text"], d["height"], d["conf"]):
        if conf >= 60 and LOW.fullmatch(text):
            heights.append(height)
        elif conf >= 60 and DIGITS.fullmatch(text):
            heights.append(height / 1.4)
    return statistics.median(heights) if heights else None


def read_scan(path, dpi):
    """Read a gray scan. dpi is its true resolution. Tiny text gets a second read at twice the size."""
    img = Image.open(path).convert("L")
    d = pytesseract.image_to_data(img, config=f"--dpi {dpi}", output_type=Output.DICT)
    x = x_height(d)
    if x is None or x < 10:
        img = img.resize((img.width * 2, img.height * 2), Image.LANCZOS)
        d = pytesseract.image_to_data(img, config=f"--dpi {dpi * 2}", output_type=Output.DICT)
    return d
import pytesseract
from pytesseract import Output

from read_scan import read_scan, x_height

d = pytesseract.image_to_data("scan.jpg", config="--dpi 100", output_type=Output.DICT)
print(x_height(d))

d = read_scan("scan.jpg", dpi=100)
print(" ".join(t for t in d["text"] if t.strip()))
Pages As is With read_scan Pages doubled
Rough invoices, 72 dpi 932 1,963 71 of 72
Rough invoices, 100 dpi 2,101 2,273 68 of 72
Clean invoices, 100 dpi 2,225 2,313 69 of 72
Rough invoices, 150 and 300 dpi 2,231 and 2,079 The same 0 of 72
Books at 33% 13,763 13,890 27 of 60
Books at 25% 13,208 13,787 55 of 60
Books at 20% 9,711 13,449 59 of 60

I picked the 10 on these same pages, so expect a little less on yours. The word counts are for the 55 printed pages. The doubled count includes the handwritten book.

Shrink a rough, high resolution scan

If your file is big and speckled, make a smaller copy for OCR and keep the original. These are the rough invoices, shrunk with Lanczos, with the new dpi passed:

File As is To 200 or 300 dpi To 150 dpi To 120 dpi
300 dpi 2,079 not run 2,129 2,225
400 dpi 1,952 2,055 at 200 2,111 2,200
600 dpi 1,822 2,027 at 300 2,091 2,217
import pytesseract
from PIL import Image

img = Image.open("rough600.jpg").convert("L")
img = img.resize((img.width // 5, img.height // 5), Image.LANCZOS)
text = pytesseract.image_to_string(img, config="--dpi 120")

That took the 600 dpi set from 79.0 to 15.9 CPU seconds and added 395 numbers. Aim by letter height, not by that 120: on these fonts it leaves small letters of 8 to 10 pixels. For small print, the rough peak sat at 240 dpi, so do not copy the number. Do this only on rough files. On clean ones, 120 dpi read fewer numbers than 240.

If you do not want to build it

PDF OCR to Markdown is our Apify Actor. It renders each scanned page at 300 dpi in gray, turns it upright, and reads it with Tesseract, and it passes the dpi for you. You paste links to PDFs or images and get Markdown back, with tables as Markdown tables.

I did not run these tests on it, so no number above describes it.

What I did not test

Questions people ask

What is the best DPI for OCR?

For gray scans of normal print, 200 to 300 dpi. On 72 clean invoices, 240 dpi read the most numbers, 2,374 of 2,418. If you do not know the type size, 300 dpi in gray is a safe default.

Is 600 dpi better than 300 dpi for OCR?

Not in my tests. On clean invoices, 600 dpi read 2,310 numbers against 2,337 for 300, and it took 2.7 times the CPU. On rough scans it read 1,822 against 2,079.

What is the minimum DPI for OCR?

It depends on the type size. In gray, book pages kept 99.9% of their words with small letters 8 to 10 pixels tall, about 120 dpi for 10 point type. Black and white needs about 16 pixels, which is 240 dpi for 10 point and 300 dpi for 8 point.

Should I upscale an image before OCR?

Only if the small letters are under about 10 pixels tall. Doubling lifted the smallest book test from 9,711 to 13,554 words. Tripling added nothing more, and doubling text that was already big enough read the same or worse.

How do I set the DPI in Tesseract?

Pass the true value with --dpi, as in tesseract scan.png out --dpi 200. The setting -c user_defined_dpi=200 gave the same output. In pytesseract, use config="--dpi 200". A file with its dpi in the metadata needs no flag.

What does Estimating resolution as mean in Tesseract?

The image has no usable dpi, so Tesseract works at 70 dpi and prints a guess from the size of the text. On 300 dpi invoices the guesses ran from 381 to 484, and it read 460 numbers of 2,418. With --dpi 433, the median guess, the same pages read 2,072.

Also in this set: how to improve Tesseract accuracy, pytesseract PDF to text, tessdata_best vs tessdata_fast, Tesseract batch processing, Tesseract digits only, Tesseract psm modes, Image preprocessing for OCR.

How this was made: Tack, our AI personality, ran these tests on October 31, 2026, and copied every result above from those runs. Apple M5 Max, Homebrew Tesseract 5.5.3 with its default English model (tessdata_fast 4.1.0), pytesseract 0.3.13, Pillow 13.0.0, Python 3.13, 8 workers, OMP_THREAD_LIMIT=1. CPU figures are one run each. Every Tesseract setting but the dpi stayed at its default. The book pages are the 60 validated Wikisource pages of the tessdata page, minus the 5 handwritten ones, scored the same way, and the full size run gave the same 14,310 words of 15,622 on all 60. Their files are 1,920 pixel wide JPEGs from Wikimedia's DjVu renders. Small letter height for a book is the median box of its confident words with no tall or hanging letters, which can read a pixel high. For the invoices it comes from each font's own x-height: 4.7 to 5.8 pixels at 72 dpi, 19.4 to 24.3 at 300, and 38.8 to 48.6 at 600. The invoices are the 72 made-up pages and the row scorer of the confidence score page, drawn again at 1,200 dpi with blur scaled to stay the same physical size. Rough noise is 10 to 16 gray levels on each pixel, the same at every dpi. Black and white is a fixed cut at 128 with no dithering. Every Python and shell block ran as printed on copies of those files. The scripts and their output are in our private repo. Sources, read October 31, 2026: Improving the quality of the output and the old FAQ (tessdoc), and at Tesseract 5.5.3 the man page, baseapi.cpp, pagesegmain.cpp, input.cpp and publictypes.h. We make the Apify Actors. Tesseract, pytesseract, Pillow and Wikisource are free projects we do not make.