cd /news/ai-tools/scanned-pdfs-vs-text-pdfs-parsing-ac… · home › topics › ai-tools › article
[ARTICLE · art-158777] src=spidra.io ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Scanned PDFs vs text PDFs: Parsing accuracy and cost

Spidra reported that on 26 October 2026 it parsed 84 pages in all three of its Parse modes and found scanned pages cost the same 1 credit each as text pages in auto and OCR mode but took about 2 seconds of Parse time per page versus 0.09 seconds for text pages. On the 84 synthetic English pages, auto mode averaged 0.0% omitted words at 300, 200 and 150 DPI but lost 2.8% at 100 DPI, including a financial table whose thousands separators all returned as decimal points, while fast mode refused every scan for free. Hidden text layers leaked into output: a faulty layer's errors came out word for word on one page, and a stamp-only layer's text appeared in 2 of 12 auto outputs and none of the OCR ones.

by read19 min views2 publishedOct 27, 2026
Scanned PDFs vs text PDFs: Parsing accuracy and cost
Image: Spidra (auto-discovered)

Somewhere in your document pile there's a folder of scans, and you don't yet know whether it's a rounding error or half your volume. Before you build an ingestion pipeline around it, there are two questions: will Parse read those pages accurately, and what will they cost next to the text PDFs sitting beside them?

We ran an experiment to find out. On 26 October 2026, we parsed 84 pages in all three Parse modes, every one with a right answer known in advance. Every number here is for those pages on that day. The method is the part you can reuse. If you only need to parse one PDF, our single-document guide is the shorter path, and the Parse docs list every option.

What we found, in three sentences #

Scans cost the same as text pages, 1 credit each in auto and OCR mode, but they took about 2 seconds of Parse time per page instead of 0.09, because AI reads them. On our 84 synthetic English pages, auto mode averaged 0.0% omitted words at 300, 200 and 150 DPI, then lost 2.8% at 100 DPI, including a financial table whose thousands separators all came back as decimal points. Fast mode refused every scan for free, and hidden text layers didn't fool auto the way we expected but did leak into the output: a faulty layer's errors came out word for word on one page, and a stamp-only layer's text appeared in 2 of 12 auto outputs and none of the OCR ones.

A scanned PDF is not one thing #

A PDF page can hold its text in three ways. A born-digital page carries its own text, so a parser can read the characters straight out of the file. A scanned page is a picture of text with nothing to read, so the words have to be recognised from pixels. And a searchable scan is both: a picture, plus a hidden text layer that a tool added afterwards. OCRmyPDF, for one, exists to add exactly that layer to scans.

That third kind is the awkward one, because Parse's modes are built around the first two. According to the docs, auto reads each page's own text and uses AI only for scanned pages. fast never uses AI: a scanned page comes back with an error and isn't charged. And ocr uses AI on every page. Parse charges 1 credit per page, scans included.

That's a prediction we can test. Text pages should cost 1 credit in all three modes. Scans should cost 1 in auto and ocr and nothing in fast. And the searchable scans are the open question: the docs say auto reads a page's own text when it has one, and a searchable scan has some. If the layer is wrong, or nearly empty, what then?

A corpus where the right answer is known #

Six page types: plain prose, a page of dense small print, an invoice, two columns, headings with bullet lists, and a financial table. Each one exists as a born-digital PDF and again as scans of falling quality. Tesseract's documentation says Tesseract works best at 300 DPI or higher, and has sections on noise and skew (we checked it in October 2026), so that's our ladder: 300 DPI clean, 200 DPI with a slight skew and some noise, 150 DPI skewed, noisy and blurred, and 100 DPI with all three at their worst. The code that builds it is near the end.

Two more versions look just like the 150 DPI scan, but aren't. In the first, the scan carries an invisible text layer with the kind of errors a bad OCR pass leaves behind (l for 1, m for rn). In the second, the only text on the page is a one-line stamp. Both can happen when a scan passes through other tools: an OCR pass that gets things wrong, or a stamping tool that adds a line of text.

The script writes the reference text as it draws each page, so the answer key is exact. We checked it anyway, against the text layer of every born-digital page, and all 2,331 reference words were found (100.00%). Here is the same dense page in every version, so you can see what "scan quality" means here:

One limit belongs here and not at the end. The pages are synthetic, English and typeset. Handwriting, stains, other scripts and photographed pages are not in the corpus, so this experiment says nothing about them. Each page was also parsed once per mode, so a gap between two modes on one page can be the mode or can be run-to-run variation. Where it matters below, we say so.

Four numbers per page #

Omitted words is the share of the reference's words that never appear in the output, in any order. Extra words is the share of output words that aren't in the reference, which is where invented text would show up. Numbers kept is the share of the reference's numbers that appear exactly. And CER, the character error rate from jiwer, which does punish reading order.

Omission leads for a reason. A table or a two-column page read in a different order can score a terrible CER while every word is present, and a missing word is the failure that's hardest to notice later. We compare words, not formatting, so Markdown marks, table alignment rows, backslash separators and the punctuation around a word are ignored: security. and security match, but 138,525 and 138.525 don't. We had to learn that list the hard way, and we say how near the end.

The whole experiment, on one scanned page #

Here it is in miniature, on a single 150 DPI scan from the corpus (swap in any scanned PDF of your own). The file is uploaded once, then parsed in each mode by its file ID:

import os
from spidra import Spidra, SpidraError

client = Spidra(api_key=os.environ["SPIDRA_API_KEY"])
file_id = client.estimate_parse("corpus/pdf/scan150/body-1.pdf").file_id  # uploads once, free

print("mode | pages | credits | read by | error")
for mode in ("auto", "fast", "ocr"):
    try:
        doc = client.parse(file_id=file_id, mode=mode, include_pages=True)
        page = doc.pages[0]
        print(mode, doc.pages_parsed, doc.credits_used, page.method, page.error or "ok", sep=" | ")
    except SpidraError as e:
        print(mode, "failed:", e)

Output:

mode | pages | credits | read by | error
auto | 1 | 1 | ocr | ok
fast failed: No page of this document could be read.
ocr | 1 | 1 | ocr | ok

Two modes read the scan with AI for 1 credit each. The third didn't fail one page: it failed the whole job, because a one-page scan leaves nothing it can read, and it charged nothing.

The experiment runs this for all 85 files. Each one is uploaded once and parsed three times in a random order, so no mode is systematically favoured by the time of day or a warm connection. (Up once and reusing the file ID is also what keeps 255 parses from being 255 uploads.)

Text PDFs: the same page in all three modes #

Mode Omitted words Extra words Numbers kept CER Credits per page Parse seconds Wall seconds
auto 0.0% 0.0% 100.0% 0.0% 1.00 0.09 0.9
fast 0.0% 0.0% 100.0% 0.0% 1.00 0.09 0.8
ocr 0.0% 0.3% 100.0% 0.4% 1.00 1.98 2.6

All three modes read all 12 text pages without dropping a word. ocr mode cost the same: 1 credit per page, but it reported ocr as the method for all 12 pages, so every page was read with AI, and that took about 22 times as long: 1.98 seconds of Parse time against 0.09. It also added words to one page. On a text invoice, the output grew a table header row, Description | Amount, that isn't anywhere on the page. That's the only output of the 255 in which those words appear, and it accounts for the 0.3% of extra words. On these pages, auto did the same job in a fraction of the time.

Scans: where words start to go missing #

Scan quality AUTO: omitted AUTO: numbers kept OCR: omitted OCR: numbers kept Pages over 5% omitted (AUTO/OCR) FAST
300 DPI 0.0% 100.0% 0.0% 100.0% 0 / 0 All 12 failed, 0 credits
200 DPI 0.0% 100.0% 0.0% 100.0% 0 / 0 All 12 failed, 0 credits
150 DPI 0.0% 100.0% 0.3% 100.0% 0 / 0 All 12 failed, 0 credits
100 DPI 2.8% 93.5% 1.3% 99.5% 2 / 2 All 12 failed, 0 credits

The tall bars are fast, which fails every scan, and are the only reason the scale goes to 100. Look at the other two: auto and ocr are flat until 100 DPI. Above that, the only non-zero page-type cells were small: auto on the 300 DPI prose pages (0.3%) and ocr on the two-column pages at 150 DPI (1.8%). The losses at 100 DPI are concentrated in two page types:

Page type AUTO: at 100 DPI OCR: at 100 DPI
body 0.0% 0.0%
dense 3.5% 6.4%
fin 13.0% 0.9%
invoice 0.0% 0.0%
report 0.0% 0.0%
twocol 0.0% 0.3%

Small print is where 100 DPI hurts: one dense page lost 6.5% of its words in both modes, and the other lost 6.2% in ocr and only 0.6% in auto. The financial table is the one to remember. In auto mode, one table page lost 25.9% of its words, and every one of the 14 mismatched numbers was the same mistake: a thousands separator read as a decimal point. 138,525 came out as 138.525. Nothing was missing, but each of those numbers was wrong by a factor of 1,000, and only 22 of that page's 36 numbers were right (61%). The same page in ocr mode got 35 of 36 right (97%), with one digit misread.

Before you read that as ocr beating auto at 100 DPI, look at the other pages. The two modes disagreed on three of 12 pages, and the disagreements went both ways: auto was better on two pages, a dense one and a two-column one, and ocr was better on the table. Both read scans with AI, we parsed each page once per mode, and 12 pages can't separate a mode from run-to-run variation.

What fast did is simpler and fully consistent. All 72 scan-like pages failed the same way, in about 0.7 seconds on average, for no credits:

SpidraJobFailedError: No page of this document could be read.

It's a failed job, not a failed page. A page inside a document that has other readable pages is different, as the mixed document below shows.

Hidden text layers leak into scanned output #

Here is where our prediction broke. We expected auto to trust a page's text layer, faulty or not. It didn't. auto read all 24 of the trap pages with AI, reported ocr as the method for every one, and took about 2 seconds per page, the same as a plain scan. fast, which never uses AI, refused all 24 with the same message as a scan. As far as Parse is concerned, a hidden layer didn't turn these pages into text pages.

The layers still got into the output, in two different ways.

The faulty layer reached the output on one page of 12, in both modes. On the dense small-print page, 16.2% of the words came out as the layer's errors, character for character: Regi0n, 5hipment, week1y, standord. All 54 of the words that exist only in the faulty layer appeared in the output of that page. The same image with no layer, parsed the same way, produced none of them. The other 11 faulty pages were untouched, which is why the average looks small (1.3% omitted) and the single-page result doesn't.

The stamp reached the output in auto and not in ocr. The stamp is invisible in the image, so the only place it exists is the text layer. On two of the 12 stamp pages, auto appended it after the page's text. ocr never did:

auto: ...support recovery customer recovery update.

CONFIDENTIAL - Page 1 of 1
ocr:  ...support recovery customer recovery update.

That's a mode-dependent failure, and a mild one: nothing is missing; there's a line too many. But a page read as an image can still carry its hidden layer into your output, so check for it if your documents pass through stamping or OCR tools. We can't tell you from outside why it happened on two pages and not ten.

The mixed document, one text page between a scan and another text page, behaved as the docs say:

Page AUTO: read by, omitted FAST: read by, omitted OCR: read by, omitted
1 (text) text, 0.0% text, 0.0% ocr, 0.0%
2 (scan) ocr, 0.0% page error, 100.0% ocr, 0.0%
3 (text) text, 0.0% text, 0.0% ocr, 0.0%
Credits charged 3 2 3

auto switched method page by page and charged 3 credits. fast read the two text pages, flagged the scan with a page-level error ("This page is a scan or image. Use auto or OCR mode to read it."), and charged 2.

What it cost, in credits and seconds #

Pages Mode Credits per page Parse seconds Wall seconds
Text PDF auto 1.00 0.09 0.9
Text PDF fast 1.00 0.09 0.8
Text PDF ocr 1.00 1.98 2.6
Scans, 300 to 100 DPI auto 1.00 1.88 to 2.10 2.5 to 2.8
Scans, 300 to 100 DPI fast 0.00 0.00 0.7 to 0.8
Scans, 300 to 100 DPI ocr 1.00 1.97 to 2.33 2.7 to 3.0
Scans with a hidden text layer auto 1.00 1.87 to 2.09 2.6 to 2.8
Scans with a hidden text layer fast 0.00 0.00 0.7 to 0.8
Scans with a hidden text layer ocr 1.00 1.80 to 1.96 2.5 to 2.7

The prediction held on cost: 1 credit a page in auto and ocr, text or scan, and fast charged only for pages it could read. The whole run, 255 parses, used 188 credits: 87 in auto, 87 in ocr and 14 in fast.

The difference you pay for is time. On our connection, a text page took about 0.9 seconds start to finish and a scan about 2.7. Sequentially, 10,000 pages is about 2.5 hours of text and about 7.5 hours of scans, for the same 10,000 credits. Whether that matters depends on how many of your requests you can run at once, which we didn't test.

Which documents fit #

Document Mode we'd start with What we measured Where we'd stop trusting it
Born-digital text PDFs auto 0.0% omitted words, 1 credit, 0.09 s Other languages and layouts, which we didn't test
Clean scans, 150 DPI or better auto 0.0% omitted at 300, 200 and 150 DPI, 1 credit, about 2 s Handwriting, stains and photographed pages, which we didn't test
Poor scans, around 100 DPI auto orocr , then check the numbers 1.3% to 2.8% omitted, 93.5% to 99.5% of numbers kept Dense small print and any table of figures
Folders that mix both auto Switched method page by page, 1 credit a page None seen
Anything with a hidden text layer auto orocr , then diff Faulty layer leaked on 1 page of 12, stamp on 2 of 12 Don't assume the layer is ignored

On these pages, then: a clean scan costs what a text page costs and reads as cleanly, and what you give up is time. The risk sits below about 150 DPI, in numbers, and in hidden text layers.

The scans are made by code you can read #

These excerpts are from our corpus script. They lean on its imports and helpers, so they won't run pasted on their own. First the ladder, and the function that applies it to a rendered page:

LEVELS = {
    "scan300": (300, 0.0, 0, 0.0, 90),
python
def degrade(img, level, seed):
    dpi, skew, noise, blur, quality = LEVELS[level]
    rng = np.random.default_rng(seed)
    if skew:
        img = img.rotate(skew, resample=Image.BICUBIC, fillcolor=255)
    if blur:
        img = img.filter(ImageFilter.GaussianBlur(blur))
    pixels = np.asarray(img, dtype=np.float32)
    if noise:
        pixels = pixels + rng.normal(0, noise, pixels.shape)
    img = Image.fromarray(np.clip(pixels, 0, 255).astype("uint8"))
    buffer = io.BytesIO()
    img.save(buffer, "JPEG", quality=quality, dpi=(dpi, dpi))
    return buffer.getvalue()  # the JPEG itself, embedded as is, like a scanner's PDF

Then the scan itself, as an image-only PDF page. Given layer_lines it also gets the invisible text layer, which is how both trap versions are made. The first trap's errors come from a small confusion table:

def make_scan(text_pdf, level, seed, out, layer_lines=None):
    """An image-only PDF page. With layer_lines, the page also gets an invisible text layer."""
    jpeg = ROOT / "work" / "page.jpg"
    jpeg.write_bytes(degrade(render(text_pdf, LEVELS[level][0]), level, seed))
    c = canvas.Canvas(str(out), pagesize=letter)
    c.drawImage(str(jpeg), 0, 0, width=W, height=H)
    if layer_lines:
        text = c.beginText(36, H - 40)
        text.setTextRenderMode(3)  # invisible
        text.setFont("Helvetica", 9)
        for line in layer_lines:
            text.textLine(line)
        c.drawText(text)
    c.save()
CONFUSE = {"l": "1", "O": "0", "o": "0", "e": "c", "m": "rn", "i": "l", "a": "o", "s": "5"}

def ocr_like_errors(lines, rate, seed):
    rng = random.Random(seed)

    def swap(ch):
        return CONFUSE[ch] if ch in CONFUSE and rng.random() < rate else ch

    return ["".join(swap(ch) for ch in line) for line in lines]

The scorer was fixed three times after the results came back #

We tested the scorer on known mistakes before the run, and real output still exposed three flaws. Each was a formatting rule we'd missed, and each inflated a number:

  • Table alignment rows.ocr mode writes Markdown tables with alignment markers like:--- , and we counted each as a word. On text pages inocr mode, extra words fell from 2.1% to 0.3%.
  • Backslash separators. On one invoice, three outputs used\ between columns where others use| . We counted 11 extra words on a page where nothing was extra.
  • Punctuation. One two-column page at 100 DPI scored 7.0% omitted words when all 372 words were there: 26 sentences had lost their final period, and we countedsecurity. andsecurity as different words. After the fix, omitted words at 100 DPI fell from 3.5% to 2.8% inauto and from 1.4% to 1.3% inocr .

None of the fixes touched numbers kept, credits or timings. Changing a scorer after seeing results is something to be suspicious of, so we kept every raw output and re-scored all 255 from the saved files. The fixes only make the scorer ignore formatting: the comma-to-period change that turned 138,525 into 138.525 is still counted as an error, and row ten of the test table proves it. These excerpts are from the scoring script, and the same caveat applies:

def normalise(text):
    """Compare words, not formatting: drop Markdown marks, bullets and table rows."""
    text = re.sub(r"<!--.*?-->", " ", text)
    text = re.sub(r"[|#*]", " ", text)
    text = re.sub(r"(?<!\S)[-:\\\u2022]+(?!\S)", " ", text)
    return re.sub(r"\s+", " ", text).strip()

def words(text):
    """Words without the punctuation around them, so 'security.' and 'security' match."""
    return [w for w in (t.strip(".,;:!?()[]\"'") for t in normalise(text).split()) if w]
python
def metrics(reference, output):
    ref, out = normalise(reference), normalise(output)
    ref_words, out_words = words(reference), words(output)
    matched = sum((Counter(ref_words) & Counter(out_words)).values())
    numbers_ref = Counter(w for w in ref_words if any(ch.isdigit() for ch in w))
    numbers_out = Counter(w for w in out_words if any(ch.isdigit() for ch in w))
    kept = None
    if numbers_ref:
        kept = sum((numbers_ref & numbers_out).values()) / sum(numbers_ref.values())
    return {
        "omission": 1 - matched / len(ref_words),  # reference words that never appear
        "extra": (len(out_words) - matched) / len(ref_words),  # words not in the reference
        "numbers": kept,  # of the reference's numbers, the share that appear exactly
        "cer": jiwer.cer(ref, out) if out else 1.0,  # sensitive to reading order, unlike omission
    }

Here is the scorer fed known mistakes, using the invoice page's reference text. Rows seven to nine are the ones that would have caught our three flaws:

Output Omitted Extra Numbers kept CER
Identical to the reference 0.0% 0.0% 100% 0.0%
10% of the words deleted 8.9% 0.0% 95% 8.0%
Same words, lines in reverse order 0.0% 0.0% 100% 68.1%
Every digit changed 39.3% 39.3% 0% 22.5%
Every word said twice 0.0% 100.0% 100% 100.3%
Same words as a Markdown table 0.0% 0.0% 100% 0.0%
Same words as an aligned table 0.0% 0.0% 100% 0.0%
Same words, backslash as column separator 0.0% 0.0% 100% 0.0%
Same words, sentence punctuation dropped 0.0% 0.0% - 1.0%
A comma turned into a period in every number 3.6% 3.6% 91% 0.5%
Nothing returned 100.0% 0.0% 0% 100.0%

Reversed lines cost 68% CER and no omissions, which is the point of having both. A reformatted table costs nothing, and a changed number costs something.

Test it on your own scans #

Take a handful of pages you already know the answer to, from the stack you're worried about: a few born-digital, a few scanned at different quality, and one document that mixes them. Run each one through all three modes with the snippet above, up it once and parsing it by file ID, and score what comes back on the four numbers. The estimate is free and fast is a free test, so the first look costs very little.

── more in #ai-tools 4 stories · sorted by recency
── more on @spidra 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/scanned-pdfs-vs-text…] indexed:0 read:19min 2026-10-27 · —