LangChain OCR for scanned PDFs, tested
0 of 1,955
words PyPDFLoader found in a scan
PyPDFLoader: 10 Documents, 0 words
On a 10-page scanned book, the loader gave no error:
docs = PyPDFLoader("scan.pdf").load()
len(docs) # 10
docs[0].page_content # ''
It reads the PDF's text layer, and a scan has none. PyMuPDFLoader gave the same 10 empty pages, and the text splitter made 0 chunks.
FAISS fails, the in-memory store stays quiet
The 0 chunks went on to the vector store:
FAISS.from_documents(chunks, embeddings)
IndexError: list index out of range
InMemoryVectorStore took the empty list with no error. Every search then gave 0 results.
extract_images=True needs rapidocr
Both loaders can OCR the page images. With only rapidocr-onnxruntime installed, the load stopped:
ImportError: `rapidocr` package not found, please install it with `pip install rapidocr`
langchain-community 0.4.2 imports the rapidocr package. After
pip install rapidocr, the default parser found 1,903 of
1,955 words.
Or use Tesseract as the parser
With Tesseract installed (brew install tesseract or
apt install tesseract-ocr), pass it in:
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_community.document_loaders.parsers import TesseractBlobParser
loader = PyMuPDFLoader(
"scan.pdf",
extract_images=True,
images_parser=TesseractBlobParser(langs=["eng"]),
)
docs = loader.load()
1,876 of 1,955 true words found
21 chunks, one Document a page
Five setups, one scan
| Setup | Words found | Seconds | Chunks |
|---|---|---|---|
| PyPDFLoader | 0 | 1.4 | 0 |
| PyMuPDFLoader | 0 | 1.1 | 0 |
| PyMuPDFLoader, RapidOCR | 1,903 | 61.7 | 19 |
| PyMuPDFLoader, Tesseract | 1,876 | 44.5 | 21 |
| Our OCR Actor | 1,874 | 134 | 23 |
Local seconds are at background priority on a Mac. The Actor's 134 include its start in the cloud. PyPDFLoader with the same two parsers found 1,902 and 1,877 words, and was slower.
No OCR install: call the Actor
If your app runs where you cannot install Tesseract, langchain-apify can run our OCR Actor and give you Documents:
from langchain_apify import ApifyWrapper
from langchain_core.documents import Document
PDF = "https://api.apify.com/v2/key-value-stores/sTnmNrGoSsi7tLQvs/records/langchain-scan-10-pages.pdf"
loader = ApifyWrapper().call_actor(
actor_id="brasspage/pdf-ocr-to-markdown",
run_input={"documentUrls": [PDF]},
dataset_mapping_function=lambda item: Document(
page_content=item["markdown"], metadata={"source": item["url"]}
),
)
docs = loader.load()
Set APIFY_TOKEN first. It found 1,874 words, in one
Markdown Document for the whole PDF.
It was the slowest setup here, and the PDF has to be at a URL. It costs $0.006 a page.
For an agent: ApifyActorsTool
The same Actor works as a tool. It reads the Actor's input schema, so you write no wrapper:
from langchain_apify import ApifyActorsTool
PDF = "https://api.apify.com/v2/key-value-stores/sTnmNrGoSsi7tLQvs/records/n8n-scan-2-pages.pdf"
tool = ApifyActorsTool("brasspage/pdf-ocr-to-markdown")
items = tool.invoke({"run_input": {"documentUrls": [PDF]}})
On a 2-page scan it found 353 of 381 words. I called the tool directly, not through a model.
How I tested it
- Scan: the 10-page PDF of OCR in n8n, the first 10 of 55 proofread Wikisource book scans, with no text layer. 1,955 true words.
- Score: true words found in order, with the scorer of the tessdata test.
- Python 3.13, langchain-community 0.4.2, langchain-core 1.7.7, pypdf 7.0.1, PyMuPDF 2.1, pytesseract 0.3.13 on Tesseract 5.5.4, rapidocr 3.10.0, faiss-cpu 1.16.0, langchain-apify 0.2.0.
- Each setup ran in a fresh process. Chunks: RecursiveCharacterTextSplitter, 1,000 characters, overlap 200. FakeEmbeddings, so no API call.
- The code blocks above are the files that ran. The Actor: three real runs, one on the 10-page PDF and two on its first 2 pages.
- langchain-community 0.4.2 warns that it "is being sunset and is no longer actively maintained".
What I did not test
- UnstructuredPDFLoader and the cloud document loaders.
- An agent with a model, LangGraph, and async loading.
- Tables, photos, handwriting, and other languages.
Sources
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
Why does PyPDFLoader return empty page_content?
The PDF is a scan. PyPDFLoader reads only the text layer, and a scan has none. On my 10-page scan it gave 10 Documents with 0 characters each.
Why does FAISS.from_documents raise IndexError: list index out of range?
The list of chunks is empty. On my scan, PyPDFLoader found 0 words, so the splitter made 0 chunks. Add OCR to the loader, then build the index.
How do I fix "rapidocr package not found"?
Run pip install rapidocr. langchain-community 0.4.2 imports rapidocr, not rapidocr-onnxruntime. Or pass images_parser=TesseractBlobParser() to use Tesseract.