ocr
Why Your OCR Results Are Garbage: 1nvo1ce vs Invoice
Why OCR turns "Invoice" into "1nvo1ce" — how modern OCR actually recognizes text, the image quality factors that dominate accuracy (resolution, skew, contrast, compression), and a pre-flight checklist to fix it before you scan.
You screenshot a receipt, run it through OCR, and get back 1nvo1ce # 0O47B instead of Invoice #0047B. The text was perfectly legible to you. The software isn’t broken — it’s responding rationally to an image that gives it less to work with than it looks like it does. OCR accuracy is overwhelmingly a function of input quality, not engine quality, and almost every garbled result traces back to one of a short list of fixable problems in the image itself.
How OCR actually reads a page
Modern open-source OCR, the kind behind Tesseract (and the engine behind Image to Text), works in stages:
- Page layout analysis — the image is segmented into regions: blocks of text, images, tables, whitespace. This is also where the software decides reading order — which column comes first, which cell belongs to which row.
- Line and word segmentation — within each text block, the software finds individual lines, then splits those lines into words based on spacing.
- Line recognition — this is the part that changed the most. Tesseract 3 and earlier recognized text character by character, classifying each isolated glyph and then trying to stitch the results into words using a dictionary. Tesseract 4 replaced that with a neural network subsystem — an LSTM (long short-term memory) recognizer, adapted from OCRopus’s line-recognition work and rebuilt in C++ — that reads an entire line image as a sequence and outputs a sequence of characters directly, without ever isolating single glyphs. That’s a meaningfully different technique: it can use the shape of neighboring characters as context, the way a human reader does, instead of judging each letter in isolation. It’s also, per Tesseract’s own documentation, roughly ten times more compute-intensive than the legacy per-character classifier.
Every step in that pipeline can go wrong independently, and the failure mode looks the same from the outside: wrong characters, wrong order, or missing text entirely. But the fix depends on which step actually failed — which is why “just try a different OCR tool” so often doesn’t help. The image is the bottleneck, not the software.
The input quality factors that dominate accuracy
Resolution — the single biggest factor
Tesseract’s own documentation is blunt about this: “Tesseract works best on images which have a DPI of at least 300 dpi.” Below that, accuracy degrades — and it degrades specifically as a function of how tall the characters are in pixels, not how big the file is.
The practical unit here is x-height — the height of a lowercase letter like “x”, excluding ascenders and descenders — measured in pixels. Documented Tesseract behavior: below an x-height of roughly 10 pixels, accuracy craters; below about 8 pixels, the engine effectively treats the strokes as noise and discards them. On the other end, the LSTM recognizer has an upper limit too — accuracy also drops once x-height climbs past roughly 30 pixels, because the line images it was trained on assume text in a specific size range. The sweet spot documented by Tesseract and referenced testing sits around a 20–30 pixel x-height, which is what 10–12pt text renders to at 300 DPI.
This is why a screenshot of small on-screen text is worse than a 300 DPI scan even when the screenshot has more total pixels. A 2400×1200 screenshot of a webpage can still have 11px-tall body text if the browser zoom was at 100% — every letter is a handful of pixels, regardless of how many megapixels the screenshot file claims. A scanned page at 300 DPI puts real, physical distance between strokes: a serif at 10pt has actual pixels to render its shape, not a blur of anti-aliasing. Pixel count and effective resolution for OCR purposes are different numbers, and only the second one matters. The fix, when you’re stuck with small source text, is to upscale the image before running OCR, not after — feed the recognizer a bigger image with more pixels per character, don’t try to fix small text in the output.
Rotation and skew
Line segmentation assumes text runs roughly horizontally. Even a couple of degrees of tilt — the kind you get from a phone photo held slightly off-axis, or a page that wasn’t square on the scanner bed — throws off where the software thinks a line starts and ends, which cascades into bad word boundaries and dropped characters at line edges. Image to Text has 90° rotate buttons for fixing gross orientation (sideways or upside-down pages), but that’s a different problem from a 3-degree skew — for that you need to actually straighten the image, not rotate it in 90° increments, before OCR sees it.
Contrast and lighting
OCR engines binarize the image internally — reduce it to black and white — before recognition. Tesseract does this with an Otsu threshold by default, and Otsu’s algorithm assumes a reasonably bimodal image: text is dark, background is light, with a clean gap between the two brightness clusters. A flat grey scan, a phone photo with a shadow falling across half the page, or glare washing out a corner all break that assumption — the threshold that works for the well-lit part of the image clips text to nothing in the dim part.
Dark mode screenshots are a specific version of this problem: light text on a dark background is the inverse of what the binarizer expects, and while modern Tesseract handles some inverted text automatically, it’s not guaranteed across the board. Inverting the image to black-on-white before OCR is a cheap, reliable fix when you’re not sure.
Compression artifacts
Heavy JPEG compression introduces ringing — faint halos and blockiness around hard edges, which is exactly what character strokes are. At high compression ratios those artifacts blur or fracture thin strokes (the difference between l and 1, or the crossbar on a t) in ways that look fine to your eye at normal zoom but confuse a recognizer working at the pixel level. For anything that’s primarily text — a screenshot, a scanned document you’re re-exporting — save as PNG, which is lossless, instead of JPEG. See PNG vs JPG vs WebP vs AVIF for when each format actually makes sense; text is one of the clearest cases where lossy compression costs you more than it saves. If you’ve already got a JPEG and need to convert it, Image Format Converter or Image Compressor can get you to PNG or a less-aggressive quality setting before you run OCR.
Noise, dithering, and halftones
Scanned newsprint, faxed documents, and old photocopies often use halftone dithering to simulate greyscale with black and white dots — which looks like a solid grey tone to a human eye at distance but is, pixel by pixel, a texture of dots layered right on top of the character strokes. Binarization on a halftoned image tends to either keep the dot pattern (adding visual noise the recognizer has to see through) or lose thin strokes along with it. There’s no clean one-line fix here beyond a higher-quality source scan; light denoising or a moderate blur before binarization can help marginally, but it’s fighting the source material.
Fonts and styling
The LSTM recognizer was trained on a broad but not infinite range of typefaces and styles. Condensed fonts, heavy letter-spacing, small caps, italics, and decorative or script-adjacent display fonts all reduce accuracy because the model has less signal to work from — tightly kerned or condensed letters visually merge, wide letter-spacing breaks word-boundary assumptions, and stylized fonts just don’t look like the training data. None of this is something you fix in the image; it’s a reason to expect a lower baseline accuracy and proofread harder.
Multi-column layouts and tables
This is where layout analysis, not recognition, is the point of failure. A two-column newspaper page or a multi-column PDF requires the software to correctly detect the column boundaries and read top-to-bottom within a column before moving to the next one — get that wrong and you get sentences interleaved from both columns. Tables are worse: Tesseract’s plain-text output does not preserve grid structure. It can usually recognize the text inside each cell, but the relationship between cells — which value belongs to which row and column — is not reconstructed unless you’re using a layout-aware mode built specifically for it. If you need a table’s actual structure, treat OCR text output as raw material to manually re-align, not a finished spreadsheet.
Handwriting
Be direct about this one: classic OCR engines, Tesseract included, are built and trained for printed text. Cursive and handwritten text is a fundamentally different recognition problem — connected strokes, no fixed glyph shapes, huge inter-writer variation — that general OCR pipelines were never designed to solve. Don’t expect usable results from handwriting; that’s a separate technology (handwriting recognition / ICR) most consumer OCR tools, this one included, don’t attempt.
Language data
Each language has its own trained model, and running the wrong one produces confident, plausible-looking nonsense rather than an obvious error — an English model forced through accented French or Cyrillic text will substitute the nearest character shapes it knows and give you a result with no error indication at all. Always match the language selection to the actual text. Image to Text supports English, Spanish, French, German, Portuguese, and Italian, each as a separately downloaded model — pick the one that matches your source before you run it, not after.
The classic confusion table
Some character mix-ups show up constantly enough that they’re worth knowing by name — they’re visual near-twins at the pixel level, and low resolution or compression makes them worse:
| Mix-up | Why it happens |
|---|---|
0 / O | Nearly identical glyph shape; digit zero is sometimes narrower or has a slash, but low-res rendering erases that distinction |
1 / l / I | Three different characters that can all render as a plain vertical stroke, especially in sans-serif fonts at small sizes |
5 / S | Similar curve structure; heavy compression blurs the flat top of the 5 into the curve of an S |
8 / B | Both built from stacked closed loops; degraded resolution merges the gap between them |
rn → m | Two characters that, kerned close together, form a shape almost identical to a single m — a classic segmentation error, not a recognition one |
cl → d | Same mechanism as rn/m: two adjacent glyphs visually merging into a shape that matches a different single character |
Notice that the last two are a different kind of error — they happen at the segmentation stage (deciding where one character ends and the next begins), not at the classification stage. No amount of dictionary correction fixes a segmentation error, because the software isn’t unsure between two candidates — it’s confidently reading the wrong number of characters.
Pre-flight checklist
Before you run OCR, in order:
- Crop to just the text region — Crop Image — so layout analysis isn’t wasting effort on borders, UI chrome, or photo backgrounds.
- Straighten any skew — a few degrees of rotation is enough to hurt line segmentation.
- Boost contrast if the source is a flat scan or a shadowed photo — dark mode screenshots may need inverting to black-on-white.
- Upscale small screenshots before OCR, not after — if the text’s x-height looks under ~20px, enlarge the image first; enlarging the output text afterward does nothing for the recognizer.
- Save as PNG, not JPEG, if the source is a screenshot or anything primarily text.
- Pick the correct language — an OCR result in the wrong language model is wrong with no warning.
- If your source is a scanned or exported PDF, convert its pages to images first with PDF to Images, then run each page through OCR individually rather than trying to OCR a PDF as one flat object.
If you’re unsure whether metadata (orientation EXIF tags, embedded color profiles) is quietly working against you, Image Metadata will show you what’s actually stored in the file before you feed it to anything.
What OCR is actually good for
OCR is good at making a document searchable, giving you a starting point for copy-paste instead of retyping, and producing a rough first draft for data entry that a human then checks. It is not a source of truth. For anything financial, legal, or identifier-heavy — account numbers, dollar amounts, dates, names, ID numbers — proofread the output against the original image every time. The failure modes above (0/O, 1/l/I, rn/m) are exactly the kind of quiet, plausible-looking substitution that slips past a skim-read and into a spreadsheet. Image to Text shows a confidence score per run and flags low-confidence regions for exactly this reason — treat that flag as a checklist, not a formality, and keep the result editable until you’ve actually checked it against the source.