PDF is one of the most common file formats in the wild and one of the most frustrating to programmatically extract data from. The format was designed for printing, not data interchange — text may be stored as individual character glyphs with no word boundaries, tables are usually floating boxes with no semantic structure, and "scanned" PDFs are just images with no text layer at all. In 2026, the right extraction approach depends entirely on the PDF type.
What changed in 2026
- LLM vision (Claude 3.5 Sonnet, GPT-4o) is now the most reliable approach for extracting tables, forms, and complex layouts — more accurate than rule-based parsers for anything non-trivial.
pymupdf (PyMuPDF) 1.24 significantly improved table detection via its built-in find_tables() API.
- AWS Textract and Azure Document Intelligence are production-grade managed OCR+structure services that handle handwriting and complex forms.
pdfjs-dist released a Node-compatible ESM build, removing the CommonJS wrapper workaround needed in earlier versions.
- LlamaParse (LlamaIndex's hosted parser) became a popular choice for RAG pipelines — it returns clean Markdown from PDFs for LLM ingestion.
PDF types and their parsers
| PDF type |
Description |
Right approach |
| Digital text PDF |
Text is embedded as characters |
pdf-parse, pdfjs-dist, pymupdf |
| Scanned / image PDF |
Pages are rasterised images |
OCR (Tesseract, Textract) |
| Complex layout |
Tables, columns, forms |
LLM vision or pymupdf.find_tables() |
| Password-protected |
Encrypted |
Must decrypt first (qpdf) |
| Portfolio / embedded |
PDFs inside PDFs |
Unsupported by most libraries |
Node.js: basic text extraction
npm install pdf-parse
import fs from 'fs/promises'
import pdfParse from 'pdf-parse'
async function extractText(filePath: string): Promise<string> {
const buffer = await fs.readFile(filePath)
const result = await pdfParse(buffer)
return result.text
}
const text = await extractText('./document.pdf')
console.log(`Pages: ${result.numpages}`)
console.log(text.slice(0, 500))
Node.js: page-by-page with pdfjs-dist
import { getDocument } from 'pdfjs-dist/legacy/build/pdf.mjs'
async function extractPages(filePath: string) {
const pdf = await getDocument(filePath).promise
const pages: string[] = []
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i)
const content = await page.getTextContent()
const text = content.items
.map((item: any) => ('str' in item ? item.str : ''))
.join(' ')
pages.push(text)
}
return pages
}
Python: fast extraction with pymupdf
import fitz # pip install pymupdf
def extract_text(path: str) -> str:
doc = fitz.open(path)
return "\n".join(page.get_text() for page in doc)
def extract_tables(path: str):
doc = fitz.open(path)
for page in doc:
tabs = page.find_tables()
for tab in tabs:
print(tab.to_pandas()) # pip install pandas
LLM vision for complex PDFs
import Anthropic from '@anthropic-ai/sdk'
import fs from 'fs/promises'
const client = new Anthropic()
async function extractWithVision(pdfPath: string, prompt: string) {
const pdfBytes = await fs.readFile(pdfPath)
const base64 = pdfBytes.toString('base64')
const response = await client.messages.create({
model: 'claude-opus-4-5',
max_tokens: 2048,
messages: [{
role: 'user',
content: [
{
type: 'document',
source: { type: 'base64', media_type: 'application/pdf', data: base64 },
},
{ type: 'text', text: prompt },
],
}],
})
return response.content[0].type === 'text' ? response.content[0].text : ''
}
const tables = await extractWithVision(
'./invoice.pdf',
'Extract all line items as a JSON array with fields: description, qty, unit_price, total'
)
OCR for scanned PDFs
# pip install pytesseract Pillow pdf2image
from pdf2image import convert_from_path
import pytesseract
def ocr_pdf(path: str) -> str:
images = convert_from_path(path, dpi=300)
return "\n".join(pytesseract.image_to_string(img) for img in images)
For production accuracy, prefer managed services: AWS Textract (boto3.client('textract')) or Azure Document Intelligence — they handle multi-column layouts and tables significantly better than Tesseract.
How to pick an approach
| Need |
Tool |
| Simple text extraction, Node.js |
pdf-parse |
| Page-level control, Node.js |
pdfjs-dist |
| Fast bulk extraction, Python |
pymupdf |
| Table detection, Python |
pymupdf.find_tables() |
| Scanned PDF |
Tesseract, AWS Textract |
| Complex tables / forms |
LLM vision (Claude, GPT-4o) |
| RAG pipeline input |
LlamaParse |
Common mistakes
Assuming text order is reading order — PDF character positions are absolute coordinates; pdf-parse concatenates left-to-right but columns and footnotes often produce garbled output. Use pymupdf with sort=True for better reading order.
Ignoring encoding issues — some PDFs use custom font encodings that map characters incorrectly. If text looks garbled, it is likely a custom encoding; LLM vision or pymupdf with flags=fitz.TEXT_PRESERVE_LIGATURES can help.
Running OCR on digital PDFs — unnecessary and slow. Check if pdf-parse returns real text first; only fall back to OCR if it returns empty or garbled output.
No rate limiting on LLM vision — vision API calls are expensive (~$0.01–0.03 per page depending on model). Cache results and avoid re-processing unchanged documents.
What to skip
PyPDF2 — largely superseded by pymupdf for speed and accuracy; still fine for simple extraction but not the best choice for new projects.
- Regex-based field extraction on raw text — PDF text whitespace is unreliable; structured extraction with an LLM is far more robust.
- Processing password-protected PDFs without the key — strip the password first with
qpdf --decrypt, then parse.
FAQ
Why does the extracted text look scrambled?
The most common causes are: multi-column layout (text from column 2 interleaved with column 1), custom font encoding, or right-to-left text. Use pymupdf with sorting enabled, or LLM vision.
Can I extract images from PDFs?
Yes. pymupdf: page.get_images(full=True) returns a list of embedded images which you can save via doc.extract_image(xref).
What about form fields (fillable PDFs)?
pymupdf: page.widgets() returns form fields with their names and values. pdfjs-dist also supports AcroForm field extraction.
How do I handle very large PDFs (500+ pages)?
Process page-by-page and stream results. Avoid loading the entire document text into memory. For OCR, split into batches and process in parallel workers.
Where to go next