OCR explained: turning scans and photos into searchable text, and getting accurate results

You have a scanned contract, a photo of a whiteboard, or a forty-page PDF that someone printed and scanned back in, and you need the words out of it. Select-all highlights nothing, search finds nothing, and the PDF-to-text tool you tried returned an empty file. Nothing is broken: the file holds a picture of text, and a picture contains no characters until optical character recognition (OCR) puts them there. This guide explains what a PDF actually stores, how a modern OCR engine turns pixels into text, which properties of a scan or photo decide whether the result is right, and how to run the whole process on your own machine — including which of our three text tools to reach for and exactly what each one gives back.

Why “select all” finds nothing in a scan

A PDF page is a list of drawing instructions. A page that was born digital — exported from Word, LaTeX or a browser — draws its text with text operators (Tj, TJ) that reference an embedded font, and the font carries a map from each glyph to a Unicode character. That map is what lets a viewer select, search and copy. A scanned page has none of it: the scanner wrote one image object (/Subtype /Image, usually JPEG-compressed with /DCTDecode, or CCITT or JBIG2 for black-and-white) and an instruction to paint it across the page. Every letter you can see is a patch of darker pixels and nothing more.

A “searchable PDF” — what a scanner’s OCR checkbox or Acrobat’s Recognize Text produces — keeps that image and adds a second, invisible layer: the recognised words, drawn in text render mode 3 (3 Tr, invisible) at the positions where they appear in the picture. Selecting in such a file highlights the image, but what lands on your clipboard is the hidden text underneath. The quality of that text is whatever the OCR engine managed at the time; the image never changes.

Our PDF to Text tool reads the real text layer using pdf.js’s getTextContent() — the same code Firefox uses to let you select text in a PDF. It is instant and exact, keeps line breaks where the PDF marks them, and separates pages with a blank line. It also tells you the truth about a scan: when a single PDF yields no text at all, the tool shows “No selectable text found — this PDF is a scan, so the words are pixels rather than text” and offers a Read it with OCR button. OCR is opt-in there because it downloads a 37 MB engine and takes seconds per page, neither of which you want if you only expected a text-layer dump.

How to tell which case you are in

  • Open the PDF in any viewer and press Ctrl+A (Cmd+A on a Mac). Text pages turn blue line by line; a scan shows one selection rectangle around the whole page, or nothing.
  • Search (Ctrl+F) for a word you can see on the page. No hits on a visible word means no text layer.
  • Drop the file into PDF to Text. A text PDF returns its content in under a second; a scan returns the message above.
  • Zoom to 400%. Real text stays razor-sharp at any zoom; a scan turns into visible pixels or JPEG blur.

How OCR reads a page: find the text, then read it

The engine behind Image to Text and PDF OCRis PP-OCRv6 “small”, an open-source (Apache-2.0) pair of neural networks from the PaddleOCR project, converted to ONNX and run by onnxruntime-web inside the browser. It works in two stages, and knowing what each stage does explains most of the failures you will meet.

Detectionanswers “where is the text?”. The image is shrunk so its long side is at most 960 px and fed to the detector (det.onnx, 9.9 MB), which outputs a probability for every pixel: is this pixel part of a text stroke? Pixels above 0.2 are grouped into connected blobs, each blob is wrapped in its smallest enclosing rotated rectangle, rectangles whose mean probability is below 0.45 are discarded as noise, and the survivors are grown by an “unclip” margin so the box covers full ascenders and descenders. Because the box is a rotated rectangle rather than an upright one, text photographed at an angle is found and straightened before it is read.

Recognitionanswers “what does it say?”. Each box is cropped from the full-resolution image, rectified to a flat strip and resized to exactly 48 px tall (width in proportion, capped at 1280 px). A crop at least 1.5 times taller than it is wide is treated as vertical text and rotated first. The recogniser (rec.onnx, 21.2 MB) then emits, for every 8-pixel column of the strip, a probability over a dictionary of 18,710 symbols — Latin letters with their accents, digits, punctuation, Chinese characters and kana — and a CTC decoder collapses repeats and blanks into a string. Every line gets an average confidence, and a line below 0.5 is dropped rather than shown. That matters later: a missing line usually means the engine was unsure, not that it saw nothing.

Finally the lines are put into reading order. Boxes are sorted by their vertical centre; boxes whose centres lie within 0.6 of the median box height of one another are treated as one line and ordered left to right; lines are joined with newlines. That rule is exactly right for a letter or a receipt and exactly wrong for a two-column layout, where lines from both columns share a vertical centre and are glued together.

Why the first run downloads 37 MB

A server-side OCR site keeps the models on its machines; this one has to bring them to yours. The first time you use either tool the browser fetches the two models and the gzipped onnxruntime core — about 37 MB in total — with a live counter (“Downloading the OCR engine — 12 / 37 MB (cached after this)”). The download starts the moment your pointer reaches the drop zone, so it is usually well under way before you have chosen a file. The site’s service worker stores it in a cache bucket keyed to the model version, separate from the site’s own code, so a site update does not throw it away; once cached, the tools work with the network switched off.

Inference runs on the GPU through WebGPU when the browser exposes navigator.gpu (Chrome and Edge 113 or newer), and otherwise on the CPU through WebAssembly in a single thread. The status line tells you which: “Loading OCR runtime (WebGPU)” or “(CPU)”. Expect roughly two to four seconds per page on a laptop CPU, less with WebGPU. The floor is Safari 16.4 or Chrome 103; an older browser is told “This browser cannot run OCR on your device — it is missing DecompressionStream” (or whichever API is absent) before any download begins.

Which languages and scripts it reads

One model covers 50 languages and there is nothing to select. The dictionary holds the Latin alphabet with the accented and special letters of 46 Latin-script languages — French, German, Spanish, Italian, Portuguese, Dutch, Polish, Czech, Hungarian, Romanian and Turkish among them — plus Simplified Chinese, Traditional Chinese and Japanese (kanji, hiragana, katakana). Mixed pages are fine: Café Müller Straße and 東京都渋谷区 can sit in the same image.

What the dictionary does not contain cannot come out. Korean (Hangul), Russian and every other Cyrillic language, Arabic, Hebrew, Thai, the Indic scripts and Vietnamese (whose stacked diacritics are not in the set) are not covered. Feed the engine Russian and you get either nothing — the confidence falls below 0.5 and the line is dropped — or Latin look-alikes: СОВЕТ may come back as COBET, because those Cyrillic capitals are visually identical to Latin ones. If you need those scripts, this is the wrong tool, and no improvement in scan quality will change that.

What decides accuracy

OCR quality is settled almost entirely before the engine runs. The variables below are the ones that move results, each with a target you can check on your own file.

FactorAim forWhat goes wrong otherwise
Resolution300 dpi scans; text lines at least 30–40 px tall in the imageEach line is resized to 48 px tall before reading; a 12 px line is upscaled four times and has no detail to upscale
Focus and motionStroke edges crisp at 100% zoomBlur merges strokes: rn becomes m, cl becomes d
ContrastDark text on a plain light groundFaint photocopies and grey-on-grey fall under the detector’s 0.2 and 0.45 thresholds and are never boxed
AngleStraight on; small rotation is fineRotation is corrected per box; perspective (a page photographed from one side) is not, so letters stay squashed
CompressionPNG, or a JPEG saved once at high qualityRe-saved JPEGs put ringing around every glyph edge, which reads as extra strokes
TypefacePlain printed or typeset textCursive handwriting, decorative fonts and stencil lettering are outside the training data
LayoutOne column, or crop columns and tables apartReading order merges lines with the same vertical centre, so columns interleave
BackgroundPlain paperPatterns, watermarks and security print create false text regions that decode as junk

The resolution numbers, worked through

Type is measured in points, 72 to the inch, and a scanner in dots per inch, so the two multiply directly. A 10-point line at 300 dpi is 10 ÷ 72 × 300 ≈ 42 px from the top of the ascenders to the bottom of the descenders, with an x-height (the height of a lower-case x) of roughly 20 px. That is close to the recogniser’s native 48 px strip, so almost nothing is lost in the resize. At 150 dpi the same line is 21 px tall with a 10 px x-height and gets doubled; the letters are still separable but e, c and o start to blur together. A 72 dpi screenshot of a full page gives a 10 px line, which is why photographing a laptop screen showing a document works so much worse than photographing the paper. On a phone the same arithmetic applies: a 12-megapixel shot (4032 × 3024) filled by an A4 page is about 345 px per inch of paper, comfortably enough; a photo of the whole desk with the page in one corner is not.

Detection has a resolution limit of its own, and it is relative to the image, not to the paper. Whatever you supply is shrunk to 960 px on its long side before the detector sees it, so a full A4 page becomes roughly 960 × 680 px, a 10-point line is about 11 px tall in that view and 6-point footnotes about 7 px. Small print is the first thing detection loses. The fix is cheap: crop the region of small text and run it on its own, so it gets the whole 960 px. The source image is likewise held at no more than 4000 px on the long side (2600 px on devices reporting 4 GB of memory or less), so a 48-megapixel photo is downsampled before the crops are taken and gains nothing over a 12-megapixel one.

Preparing the input

Phone photos

Fill the frame with the page, hold the phone parallel to the paper rather than tilted, and put the light behind you so your own shadow is not across the text. Tap to focus on the text, not the background. HEIC is fine as it is — Image to Text decodes HEIC and HEIF itself, and HEIC to JPG exists for when you also want a JPG to keep. Then crop: Crop Image opens the picture in an editor where you can rotate and straighten it in the same session, and Add Filters has a Finetune tab with brightness and contrast sliders for a faint or grey shot. Do not sharpen; sharpening adds the same edge halos as JPEG ringing.

Scans

Scan at 300 dpi in greyscale rather than black-and-white — a bilevel scan throws away the anti-aliasing that helps the recogniser tell thin strokes from noise — and save as PNG, TIFF or a single high-quality JPEG. Flatten the page: text falling into the gutter of a bound book comes out curved, and the crop step models rotation, not curvature. If your scanner produces a PDF, drop that PDF straight into PDF OCR; there is no need to convert it to images first. The tool renders each page itself with pdf.js as a PNG (deliberately, to avoid JPEG artefacts) at up to 2000 px on the long side, about 180 dpi for a Letter page — enough for body text, though a page whose embedded scan is only 100 dpi does not improve by being rendered larger. Our scanning guide covers scanner settings in more depth.

PDFs

PDF OCR first checks for a text layer and uses it when it finds at least 40 characters, telling you “This PDF already contained selectable text, so it was read directly”. The 40-character threshold exists because scanners routinely stamp a few characters of header text onto otherwise imageless pages, but it has a side effect: a file with one born-digital cover page followed by thirty scanned pages passes the check and returns only the cover page. Pull the scanned pages out with Split PDF (a range such as 2-31) and run those separately. The same tool is how you process a 300-page scan in instalments rather than one sitting. A password-protected PDF fails with “Could not read these PDFs. They may be corrupted or password-protected — try the Unlock PDF tool first”; Unlock PDF needs the current password and produces a copy without one.

What you get back

ToolTakesMethodReturns
PDF to TextPDFs with a text layer, several at onceReads the text objects; no OCR unless you click Read it with OCR on a scanPlain text in the box, Copy all, Download .txt; several files become extracted_text.zip
PDF OCRScanned or mixed PDFs, several at once, no page limitUses the text layer if it holds 40 or more characters, otherwise renders and recognises every pagePlain text with a blank line between pages, so page n of the text matches page n of the file; .txt or ZIP
Image to TextPNG, JPG, HEIC/HEIF, WebP, GIF, BMP, TIFF, several at onceDetection and recognition on the imagePlain text in reading order; .txt or ZIP

None of the three writes a searchable PDF: the output is text, not a new document with an invisible layer. Structure is flattened. Columns interleave, as described above. A table comes out one row per line with the cells separated by spaces where the cells share a baseline, or as separate lines where a tall cell breaks the 0.6-height rule — check any table row by row against the picture rather than trusting the alignment. Paragraph breaks are not preserved, only line breaks, so text pasted into a word processor needs its hard returns removed before it will reflow.

Checking the result

Print OCR has a short, predictable list of confusions, and you can search for them. 0/O, 1/l/I, 5/S, 8/B and 2/Z swap in either direction, especially inside mixed codes such as invoice and part numbers where context cannot help. rn reads as m and vv as w on blurred input. A comma can become a full stop, so 1,204.99 may come back as 1.204.99. Run a spell-checker over prose; it catches most single-character errors in words. For numbers, re-add any column that has a printed total and compare: a column that no longer sums to its total contains at least one misread digit, and one that does is very probably right.

Remember the confidence cut: a line the engine could not read to 50% average confidence is simply absent, with no marker. Compare line counts against the page — a missing address line or a dropped footnote is easier to spot from the picture than from the text. When a line is missing, crop that region and run it alone; the detector gets more pixels and the recogniser often clears the threshold on the second attempt.

Some inputs are beyond it. Cursive handwriting, forms whose printed box borders run through the answers, heavily stamped or annotated pages, and anything in an unsupported script will not improve with better lighting. For those, OCR is still useful as a search index — the readable half of a page is enough to find the page again — but the transcription itself has to be done by hand.

Verifying that nothing leaves your machine

The documents people OCR are usually the ones they least want on someone else’s server — ID pages, payslips, medical letters, signed contracts — and here the model comes to the document rather than the other way round. You do not have to take that on trust. Open your browser’s developer tools on the Network tab, then use Image to Text. You will see the engine arrive, and then nothing:

/models/ocr/manifest.json                   789 B    sizes of what follows
/models/ocr/det.onnx                        9.9 MB   text detector
/models/ocr/rec.onnx                       21.2 MB   text recogniser
/models/ocr/charset.json                    112 KB   18,710-symbol dictionary
/js/ort/ort-wasm-simd-threaded.jsep.wasm.gz          onnxruntime-web core, gzipped

Every one of those is a download from this site; no request carries your image or its text anywhere, and once the files are cached there are no OCR-related requests at all. The stronger test is to switch the network off after one successful run — airplane mode on a phone, Wi-Fi off on a laptop — reload the page and OCR another image. It works, because there is nothing left to fetch.

Troubleshooting

SymptomLikely causeFix
“No text found in this image” on a page you can readText too small in the image, too faint, or in a script outside the modelCrop to the text, raise contrast, rescan at 300 dpi; check the language list
Output is Latin gibberishCyrillic, Greek or another unsupported script read as look-alike Latin lettersNot fixable here — use an engine trained on that script
Whole lines missing, the rest perfectThose lines fell under the 0.5 confidence cut: small print, a stamp, bad focus in one regionCrop the region and run it alone
Two columns merged into nonsense linesReading order groups boxes by vertical centreCrop each column and run them as separate images
Numbers wrong in a tableDigit and letter confusions, misplaced decimal separatorsRe-add against the printed total; search for 0/O, 1/l, 5/S
First run sits at “Downloading the OCR engine”37 MB on a slow connectionWait once; it is cached across visits and site updates
Several seconds per page and the status says (CPU)No WebGPU, so single-threaded WebAssemblyUse Chrome or Edge 113 or newer on a machine with a usable GPU, or accept the wait — the page stays responsive throughout
“This browser cannot run OCR on your device — it is missing …”Browser predates Safari 16.4 or Chrome 103Update the browser; nothing else helps
PDF OCR returned only the first page of a scanA text layer of 40 or more characters exists somewhere in the file, so OCR was skippedSplit the scanned pages into their own PDF and run those
“Could not read these PDFs. They may be corrupted or password-protected”Encrypted PDFRemove the password with Unlock PDF first — see the passwords guide
Tab runs out of memory on a huge PDF or photoEvery page is rendered at up to 2000 px and every image held at up to 4000 pxSplit the PDF into ranges; resize a 48-megapixel photo to 4000 px on the long side before dropping it in

Do this

  • Check for a text layer first: PDF to Text is instant and exact, and only a scan needs OCR.
  • Scan at 300 dpi in greyscale, or photograph the page flat, filling the frame, with the light behind you.
  • Crop to the text — small print, columns and tables each as their own image.
  • Drop PDFs straight into PDF OCR; split mixed or very long files with Split PDF first.
  • Proof-read numbers against printed totals and search for 0/O, 1/l and 5/S swaps before you rely on the text.
  • Expect plain text, not a searchable PDF, and only Latin, Chinese and Japanese scripts.

Frequently asked questions

Why can't I select or search text in my scanned PDF?

Because the page is a single image, not text. A scanner stores a photograph of the paper; there are no characters in the file for a viewer to select. OCR has to read the pixels first — the PDF OCR tool does that and gives you the text as a .txt file.

What resolution should I scan at for OCR?

300 dpi for ordinary body text. That makes a 10-point line about 42 px tall, close to the 48 px the recogniser works at. 200 dpi is usually still fine; 150 dpi loses small print, and a 72 dpi screenshot of a whole page is unreadable.

Does the OCR tool produce a searchable PDF?

No. Both PDF OCR and Image to Text return plain text that you can copy or download as .txt (or as a ZIP of .txt files for several inputs). They do not write a new PDF with an invisible text layer behind the scan.

Which languages can the OCR read?

Fifty, with nothing to select: English and 46 other Latin-script languages, plus Simplified Chinese, Traditional Chinese and Japanese, including mixed pages. Korean, Russian and other Cyrillic languages, Arabic, Hebrew, Thai, Vietnamese and the Indic scripts are not in the model's character set.

Why is the first OCR run so slow?

The engine — two neural-network models plus the runtime, about 37 MB — has to be downloaded before the first character can be read. It is cached afterwards, survives site updates and works offline, so later runs start immediately.

Tools used in this guide

Every one of these runs in your browser — the files you work on never leave your device.

More pdf guides