Tesseract digits only: what the whitelist did on a scanned invoice
To make Tesseract read only numbers, add
-c tessedit_char_whitelist=0123456789., and read one
number at a time with --psm 7. On my scanned test
invoice, the whitelist did not add a single right number: 31 of 36
with it, 31 of 36 without. With the point and comma left out of the
list, it got 11. What it did do is stop other characters coming back,
and once that hid a mistake. A short check after OCR caught every row
with a wrong number.
How to set it
In Python, with pytesseract, on an image of one number:
import pytesseract
from PIL import Image
cell = Image.open("cell.png") # one number, cut out of the page
text = pytesseract.image_to_string(
cell, config="--psm 7 --dpi 300 -c tessedit_char_whitelist=0123456789.,"
).strip()
The same on the command line:
tesseract cell.png stdout --psm 7 --dpi 300 -c tessedit_char_whitelist=0123456789.,
Add any other mark your numbers use, such as - or
$. Set --dpi to your scan's real resolution:
pytesseract drops it, and on this invoice, read as a whole page, that
took Tesseract from 24 numbers right to 0 (see
how to improve Tesseract accuracy).
Tesseract also ships a ready made digits config. In my
copy (5.5.3 from Homebrew) it holds
tessedit_char_whitelist 0123456789-., with no comma. Use
it by adding the word digits at the end of the config:
config="--psm 7 --dpi 300 digits".
What it did on 36 numbers
The test page is a plumbing invoice we made, so the true value of every number is known: 36 numbers in a ruled table (quantity, unit price and amount on 12 rows), scanned at 300 dpi, tilted 0.8 degrees, with noise, blur and JPEG grain. I cut each cell out along the table's lines, added a white margin, and read it once with each setting. A number counts only when the text matches exactly.
| Setting | Numbers right |
|---|---|
| psm 7, no whitelist | 31 of 36 |
psm 7, whitelist 0123456789., |
31 of 36 |
psm 7, the digits config |
30 of 36 |
psm 7, whitelist 0123456789 (no point, no comma)
|
11 of 36 |
psm 6, whitelist 0123456789., |
30 of 36 |
psm 3, whitelist 0123456789., |
20 of 36 |
psm 8, whitelist 0123456789., |
3 of 36 |
| psm 7, cell enlarged 2x, with and without the whitelist | 25 and 24 of 36 |
| Whole page, no cutting, psm 3, without and with the whitelist | 24, then 11 of 36 |
| Our Actor's cell pass: each unsure cell read 3 ways, then a math check | 33 of 36, all 3 misses flagged |
It did not fix a misread, and it hid one
With psm 7, the same 5 numbers came out wrong with and without the whitelist:
| True value | No whitelist | Whitelist 0123456789., |
|---|---|---|
| 44.83 | 4483 | 4483 |
| 8.50 | 850 | 850 |
| 59.50 | 5950 | 5950 |
| 1 (a quantity) | | | nothing |
| 109.30 | 109.4) | 109.40 |
The whitelist cannot add a decimal point that Tesseract did not see.
It only limits which characters may come back. That is why the last
row matters: without it, "109.4)" is plainly broken. With it, the
answer is 109.40, a clean number that is wrong. The
tesseract command gave the same text as pytesseract on
the 3 cells I checked.
Keep the point and the comma in the list
With only 0123456789, every number with a decimal lost
its point: 78.64 came back as 7864, and 1,138.00 as 113800. That took
the count from 31 to 11. The digits config keeps the
point but has no comma, so 1,138.00 came back as 1138.00, which is why
it got 30.
Use it only on cells that hold numbers
A whitelist does not make Tesseract skip words. It makes it answer in digits. On the whole page, the title line, a shop name and "Invoice 4471", came back as ".4471", and the first row of the table as "100134 67113.82 682.92". The count fell from 24 to 11 at psm 3, and from 21 to 11 at psm 4.
The description cells did the same. "Copper pipe 3/4in" came back as "34", "Ball valve 1/2in" as "120", "PVC elbow 2in" as "2", and "Supply hose 16in" as "16". The other 8 came back empty. So find the number cells first, then use the whitelist on those alone.
psm 7 for one number
The page segmentation mode mattered far more than the whitelist. On a cut-out cell, psm 7 (one line of text) got 31. psm 6 (one block) got 30. psm 3 (find the layout) got 20: it returned nothing for 11 of the 12 quantities. psm 8 (one word) got 3.
Making the cell twice as big did not help on this scan: 24 without the whitelist and 25 with it. It read 550.48 as 350.48 and 5.94 as 3.94, wrong numbers that look right.
Check the numbers after OCR
A whitelist cannot tell you a number is wrong. An invoice can. This is the check I ran on the psm 7 whitelist readings. Each row is [quantity, unit price, amount] as text:
import re
NUM = r"\d{1,3}(,\d{3})+(\.\d+)?|\d+(\.\d+)?"
def flag_rows(rows):
"""Returns (row number, reason) for each row that needs a second look."""
bad = []
for i, (qty, price, amount) in enumerate(rows, 1):
if not all(re.fullmatch(NUM, v) for v in (qty, price, amount)):
bad.append((i, "not a number"))
elif not (re.search(r"\.\d\d$", price) and re.search(r"\.\d\d$", amount)):
bad.append((i, "no decimal point"))
elif abs(float(qty) * float(price.replace(",", "")) - float(amount.replace(",", ""))) > 0.005:
bad.append((i, "qty times price is not amount"))
return bad
It flagged rows 4, 5 and 9, exactly the 3 rows with a wrong number, and none of the 9 good rows. On the true values it flagged nothing. Row 9 shows why the decimal test is there: 7 times 850 is 5950, so the math alone says that row is fine.
What I did not test
- Other invoices, other fonts, and clean pages that were never scanned.
-
The tessdata_best model, and
tessedit_char_blacklist. - How long each setting takes.
- Handwritten numbers and phone photos.
For many files, or from your code
The last row of the results table is PDF OCR to Markdown, our Apify Actor. It finds tables by their lines, reads each cell with psm 7, reads an unsure cell again at twice the size and in black and white, and keeps a reading two versions agree on, or else the most sure one. Together, that fixed 4 of the 5 misses above and broke 2 numbers that plain psm 7 had right, so it ended at 33 of 36. Its checks flagged all 3 misses. It also has a digits only reread for numbers that lost their point, and on this invoice that reread changed nothing.
- $0.006 a page, plus $0.00005 a run at the default 1 GB of memory. Pages that fail are free.
- The Apify free plan lets you try 20 pages a run and 50 a month.
- Pages are read inside the run. They are not sent to any AI service.
Whole tables rather than single numbers? See PDF table to Markdown. Want a Python library that does the whole job? See PDF to Markdown in Python. Only need the text of a scan? See scanned PDF to text.
How this was made: Tack, our AI personality, ran Tesseract 5.5.3 (Homebrew, eng) through pytesseract 0.3.13 on a Mac on October 30, 2026, and copied every count above from that run. The script and its output are in our research notes. The Actor row used the code of PDF OCR to Markdown build 0.1.21, run on the same Mac. We made the test invoice. We make the Apify Actor. Tesseract and pytesseract are free, open source tools that we do not make.