> Source: https://txtfetch.com/extract/scanned-pdf > Plain-text twin — every page on txtfetch.com has one. https://txtfetch.com/text --- # Scanned PDFs, OCR'd without a flag. No text layer, skewed feeder scans, low-DPI faxes. txtfetch detects the blank pass itself and retries through OCR, in the same request. the-problem A scanned PDF has no text layer at all. It's a picture of a document wearing PDF packaging. A naive text-extraction pass on it reads back nothing but whitespace. Real scans add their own damage on top. There's skew and rotation from the sheet feeder. Low-DPI captures under ~200 dpi blur character edges past recognition. And fax-style CCITT Group 4 compression hits bitonal pages. The sharpest trap is the mixed document: a digitally-created cover page stapled ahead of a faxed, scanned body. A page-one text pass on that document comes back non-empty. So a system that only checks 'did we get any text' never learns that the rest of the file is a picture. one-request-solution txtfetch always tries the fast path first: a direct text-layer extraction. Only when the \*entire\* document comes back whitespace does it retry the whole file. That retry runs through Tika's ocr\_only PDF strategy with Tesseract underneath. It's the same request and the same { status, extracted\_text } shape, with no ocr=true flag to set. That escalation is deliberately whole-document. So the mixed cover-page-plus-scanned-body case above won't auto-escalate, because the first pass isn't blank. For those documents, or anywhere page-by-page fidelity matters more than the extra latency, pass ?quality=premium. Every page then routes through a vision-capable model instead of the text/OCR fork. So a digital cover page and a scanned body both get read consistently. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@faxed-inspection-report.pdf ``` Python ```python import os import requests with open("faxed-inspection-report.pdf", "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; const file = new Blob([await readFile("faxed-inspection-report.pdf")]); const form = new FormData(); form.append("file", file, "faxed-inspection-report.pdf"); const res = await fetch("https://api.txtfetch.com/v1/extract", { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, body: form, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" ) type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` } func main() { f, err := os.Open("faxed-inspection-report.pdf") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "faxed-inspection-report.pdf") if err != nil { panic(err) } if _, err := io.Copy(part, f); err != nil { panic(err) } writer.Close() req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` Or skip the download. Pass a `url` parameter and txtfetch fetches the document server-side: curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/scans/site-survey.pdf" \ -H "Authorization: Bearer $TXTFETCH_KEY" ``` Python ```python import os import requests r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, params={"url": "https://example.com/scans/site-survey.pdf"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/scans/site-survey.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go package main import ( "encoding/json" "fmt" "net/http" "net/url" "os" ) type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` } func main() { endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract") if err != nil { panic(err) } q := endpoint.Query() q.Set("url", "https://example.com/scans/site-survey.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` what-comes-back That is the shape. This is the text. A real .pdf from the benchmark corpus, word-diffed against the raw parser output. Nothing here was written for this page. Every character comes from a committed fixture. Scanned Invoice #9042 (image-only PDF) `invoice-9042-scan.pdf` · application/pdf · left pane read via Apache Tika, **right pane at premium (VLM)** raw parser output ``` lnvoice #9O42 Bill to: Northwind Tradcrs, 44 Harbor R0w ltem Qty Unit Pricc Total Onsite installatlon 2 $45O.OO $9OO.OO Annual support pian 1 $1,2OO.OO $1,2OO.OO Tota1 due: $2,1OO.OO ``` what txtfetch returns ``` # Invoice #9042 Bill to: Northwind Traders, 44 Harbor Row | Item | Qty | Unit Price | Total | | --- | --- | --- | --- | | Onsite installation | 2 | $450.00 | $900.00 | | Annual support plan | 1 | $1,200.00 | $1,200.00 | Total due: $2,100.00 ``` [PDF extracts no text (the OCR escalation path) →](https://txtfetch.com/fixes/pdf-extracts-no-text) [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.pdf` response-options Mixed text-and-scan PDF? ?quality=premium reads every page through a vision model instead of the whole-document text/OCR fork. A digital cover page and a scanned body both come back accurately. faq **Why is my scanned PDF coming back empty or blank?**: If Tesseract still finds no readable text after the automatic OCR retry, the request returns an extraction_failed error rather than guessing. That can happen with an extremely low-resolution scan, a blank page, or a corrupted image stream. Try ?quality=premium for a second read via a vision model. **Do I need to tell txtfetch a PDF is scanned?**: No, there's no ocr=true parameter for PDFs. txtfetch runs a text pass first. It only escalates to OCR automatically when that pass comes back whitespace across the whole document. **What about a PDF with a digital cover page and scanned pages after it?**: That's the one case whole-document escalation misses. The first pass isn't blank, so OCR never kicks in for the rest. Pass ?quality=premium for these mixed documents. It reads every page through a vision model instead of relying on the text-or-OCR fork. **Does OCR handle skewed or low-quality fax scans?**: Tesseract corrects moderate skew and reads CCITT Group 4 fax-compressed bitonal pages. Accuracy drops, though, on very low-DPI (under ~150 dpi) source scans. See /benchmarks for measured accuracy by document category. go-further - [Read the .pdf guide →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Digital-native PDFs and the text-layer fast path →](https://txtfetch.com/extract/pdf) - [See exactly which of your PDF's pages have real text — free, page by page →](https://txtfetch.com/tools/pdf-to-text) - [Multi-page TIFF faxes need a different read →](https://txtfetch.com/extract/tiff) - [Measured OCR accuracy by document category →](https://txtfetch.com/benchmarks) - [Is your PDF scanned, mixed, or text — find out free →](https://txtfetch.com/tools/pdf-text-check) - [Not sure a scan will OCR cleanly? Check it free, in your browser →](https://txtfetch.com/tools/image-ocr-check) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Extract it from your language →](https://txtfetch.com/for) - [Get an API key →](https://app.txtfetch.com/signup) PDF & scans - [`.pdf (digital)`](https://txtfetch.com/extract/pdf) - [All formats →](https://txtfetch.com/extract) ## Send a real .pdf (scanned) through it. One HTTP call returns the text. Read one in your browser first, for free. [Get an API key →](https://app.txtfetch.com/signup) [Open the free .pdf (scanned) reader →](https://txtfetch.com/tools/pdf-to-text)