Tools,

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

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.

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.