OCR resume parser: what to do when the CV is a scan
Published
If you are looking for an OCR resume parser, you have almost certainly just met the failure it exists to fix: a PDF came in, your extractor returned an empty string, and the candidate record came out blank with no error anywhere. The short answer is that OCR gets you text off a picture of a page, and that is only the first of two problems — the second is turning that text into a candidate record. Most of the pain in this area comes from tools that solve one of those two and are described as if they solved both.
This article is the map: how to tell a scanned CV from a normal one, what Tesseract and Docling each actually give you, and where a parsing API takes over. We build ResumeJSON, a paid resume parsing API, so read this as written by an interested party. Every claim about someone else's tool below was read from that project's own pages as of September 2026.
Why a scanned CV is a different document
A PDF is two different things wearing one extension. One has a text layer — the characters are in the file, and a library like pdf.js, pdfplumber or smalot/pdfparser reads them out. The other is a picture of a page: a flatbed scan, a phone photo run through a scanning app, an export that rasterised everything. There are no characters in that file at all, only pixels.
The dangerous part is that the second kind does not fail. Open it with any normal extractor and you get back "" — not an exception, not a warning, just an empty string that looks exactly like a CV with nothing in it. If your intake form is open to the public, a meaningful share of what arrives is scans, and those candidates quietly become empty rows.
A cheap, reliable test is a character-count threshold rather than a check for exactly zero, because a scanner usually leaves a scrap behind:
text = extract_text(pdf_bytes)
if len(text.strip()) < 200:
# Treat as a scan: this needs OCR or a vision read, not a retry.
...Zero is the wrong threshold. A scanning app stamps a page number, a watermark or its own banner into the output, so a strict == 0 check sends those files down the text path to be parsed from twelve characters — and they come back as a confident blank, which is the exact failure you were trying to catch. A real CV runs to roughly four thousand characters and the shortest plausible one is still several hundred, so the gap between "a scrap" and "a document" is wide, and a couple of hundred characters sits comfortably inside it.
Tesseract: the OCR engine everything else wraps
When somebody says "OCR" in this context they usually mean Tesseract, the open-source engine maintained at tesseract-ocr/tesseract under the Apache 2.0 licence. Its README is precise about what it is:
Tesseract 4 adds a new neural net (LSTM) based OCR engine which is focused on line recognition, but also still supports the legacy Tesseract OCR engine of Tesseract 3 which works by recognizing character patterns.
It has "unicode (UTF-8) support", recognises "more than 100 languages 'out of the box'", can be trained on more, and writes "plain text, hOCR (HTML), PDF, invisible-text-only PDF, TSV, ALTO and PAGE". The command line is one line:
tesseract resume.png out -l engWhat Tesseract does not do is anything resume-shaped. It returns the characters on the page in reading order, and that is the whole contract. There is no name field, no employer, no date range — the README is not hiding that, it simply is not a resume tool. It is also a native dependency: you are installing a binary and language data into your image, not adding a line to requirements.txt or package.json.
What each option actually returns
| Tool | What it takes | What it gives back | Resume fields? |
|---|---|---|---|
| Tesseract | Images, PDF pages | Text, hOCR, TSV, ALTO, PAGE | No |
| Docling | PDF, DOCX, images and more | Structured document: layout, reading order, tables | No |
| pyresparser | PDF, DOCX with a text layer | A fixed dict of resume fields | Yes, but no OCR and unmaintained |
| A parsing API | The original file | Typed candidate JSON | Yes |
Two columns do the work. "What it gives back" is the gap you will be filling yourself, and for both open-source rows that gap is the entire field-extraction step. "Resume fields?" is the question searchers are really asking, and OCR alone never answers it.
Docling: OCR plus document structure
Docling is the strongest open-source piece of this puzzle, and it is worth knowing about even if you end up buying the parsing step. Its own README lists, among its features:
🔍 Extensive OCR support for scanned PDFs and images
and
📑 Advanced PDF understanding incl. page layout, reading order, table structure, code, formulas, image classification, and more
It describes itself as an SDK "for parsing PDF, DOCX, HTML, and more, to a unified document representation for powering downstream workflows such as gen AI applications". So Docling covers more than Tesseract does: it runs OCR and gives you back a document with structure rather than a flat wall of text, which matters on CVs, where two columns and a sidebar are normal and a naive read interleaves them into nonsense.
It is also alive: PyPI shows docling 2.129.0 released 18 September 2026, and the project notes that Python 3.9 support was dropped in version 2.70.0, so plan on 3.10 or higher. Our fuller comparison is in Docling alternative: when you need resume fields, not text.
What Docling still does not do is decide that "Monzo — Staff Engineer, March 2022 to Present" is a company, a title, a start date of 2022-03, and a role that is still current. That step — the one your ATS actually consumes — is yours to build and maintain on top, and it is where the months go. See Open source resume parser: what works, and when to buy instead for what that costs.
Where a parsing API takes over
An API earns its place when the scanned file is a normal part of intake rather than an exception, because then OCR is no longer a side project — it is a dependency, a container image, a language-data decision and a quality problem you own forever.
For what it is worth, the approach we take is not OCR at all. A PDF is read for its text layer first; when there is essentially none, the original bytes are looked at as a document instead of being run through a character recogniser, and the response says which happened — meta.read tells you whether the answer came from text or from looking at the page, and meta.source still says pdf, because a scan is still a PDF to whoever sent it. Uploading a JPEG, PNG or WebP of a CV is a first-class input on the same endpoint, not a separate pipeline. Uploads are accepted up to 20 MB, and the published plans are on our docs page.
The practical consequence is that there is no separate OCR charge and no OCR flag to set — but the more important consequence is the failure mode. Send the original file, not your extracted text. A parser that receives the bytes can fall back to looking at the page; one that receives your empty string cannot, and it will answer with an empty candidate exactly as confidently.
Who should stay with OCR they run themselves
This is the section most comparison pages skip, so here it is plainly.
- The documents cannot leave your network. If your compliance position is that candidate CVs are not sent to a third party, that settles it. Run Tesseract or Docling locally and write the field extraction yourself.
- You need the text, not the candidate. Search indexing, redaction, archival, a retrieval pipeline of your own — OCR is the right tool and a resume endpoint is the wrong one.
- The documents are not all CVs. Contracts, invoices, forms. Docling handles the whole set with one pipeline; a resume endpoint will refuse every one of them, correctly.
- Volume is small and the layouts are uniform. A few hundred scans from one source, one recruiter reading the output: Tesseract plus a few regexes will genuinely do, and paying anything is silly.
Where the scans arrive from the open internet, in every template and half a dozen languages, and nobody on the team wants to own a page-segmentation problem for the next three years, that is the point where a parse-per-request API is cheaper than an OCR stack — not because the OCR is hard to start, but because it is hard to stop maintaining.
If you want to see what comes back for a scanned page, the free parser takes an upload without a signup, and the fields are the same ones the API returns.