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. 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 https://spidra.io/blog/how-to-scrape-pdf-extract-json-api is the shorter path, and the Parse docs https://docs.spidra.io/features/parse 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 https://github.com/ocrmypdf/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 https://github.com/tesseract-ocr/tessdoc/blob/main/ImproveQuality.md 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 https://pypi.org/project/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: python modes.py 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. Uploading once and reusing the file ID https://spidra.io/blog/pdf-url-vs-upload-vs-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 or ocr , 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 or ocr , 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: dpi, skew in degrees, noise sigma, blur radius, jpeg quality 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: python 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 in ocr 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 counted security. and security as different words. After the fix, omitted words at 100 DPI fell from 3.5% to 2.8% in auto and from 1.4% to 1.3% in ocr . 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: python 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, uploading 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.