Extract text from PDFs in bulk, with OCR for scanned pages
Getting text out of a PDF is easy for one file and surprisingly fiddly for a thousand: some have a text layer, some are scans, some are encrypted or not PDFs at all, and a RAG pipeline wants chunks, not one long string. This guide shows the free command-line way with pdftotext and Tesseract, tested on real files, and PDF Text Extractor: PDF to Text, Pages & OCR, an Apify actor that takes a list of PDF links and returns text, per-page text, chunks and metadata, reading scanned pages with OCR only where needed.
Run it on Apify Opens the actor's page on apify.com. A free Apify account is needed to run it.
Do it yourself: pdftotext and Tesseract
Poppler's pdftotext reads the text layer. Tested on 2026-10-12 with the Bitcoin white paper:
curl -sL -o bitcoin.pdf https://bitcoin.org/bitcoin.pdf
pdfinfo bitcoin.pdf | grep -E "Pages|Producer" # Pages: 9, Producer: OpenOffice.org 2.4
pdftotext -layout bitcoin.pdf - | head -3 # "Bitcoin: A Peer-to-Peer Electronic Cash System"
pdftotext bitcoin.pdf - | wc -w # 3568 words
A scanned page has no text layer, so pdftotext returns nothing. Render it and run Tesseract. OCRmyPDF's skew.pdf test file:
pdftotext skew.pdf - | wc -w # 0 words: no text layer
pdftoppm -r 300 -gray -png skew.pdf skew # render at 300 dpi
tesseract skew-1.png - | head -2 # "The LinnSequencer" / "32 Track MIDI Sequence Recorder"
tesseract skew-1.png - | wc -w # 729 words
In Python, pypdf or pypdfium2 do the same. What a script has to add for a real batch:
- Deciding per page whether OCR is needed (OCR-ing everything is slow and makes text worse on clean pages).
- Telling apart broken downloads, HTML error pages served as
.pdf, encrypted files and empty scans. - Cutting text into chunks at paragraph or sentence boundaries with overlap for embeddings.
- Reading PDF metadata dates such as
D:20240306081813-05'00'into ISO dates. - Installing Tesseract and its language packs wherever the job runs.
Extract text from many PDFs without code
- Open PDF Text Extractor: PDF to Text, Pages & OCR on Apify.
- Paste direct PDF links, up to 10,000.
- Choose One row per PDF or One row per page, and optionally a Chunk size (for example 1,500 characters with 200 overlap) for RAG.
- Leave OCR scanned pages on and pick the languages (English, German, French, Spanish, Italian, Portuguese, Dutch).
- Click Start and export JSON, CSV or Excel, or call it from your pipeline through the API.
Text comes from PDFium, the PDF engine in Chrome. Only pages with fewer than 20 non-space characters are rendered at 300 dpi and read with Tesseract 5. A row from a real run on 2026-10-12, text shortened:
{
"url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
"status": "ok",
"pageCount": 6,
"pagesExtracted": 6,
"pagesOcr": 0,
"wordCount": 6516,
"title": "Form W-9 (Rev. March 2024)",
"subject": "Request for Taxpayer Identification Number and Certification",
"producer": "Designer 6.5",
"creationDate": "2024-03-06T08:18:13-05:00",
"pdfVersion": "1.7",
"text": "Form W-9\n(Rev. March 2024)\nRequest for Taxpayer\nIdentification Number and Certification …"
}Worked examples from our test runs
| Run (2026-10-12) | Result |
|---|---|
| Default input: Bitcoin white paper, arXiv 1706.03762 ("Attention Is All You Need"), IRS Form W-9 | 3 PDFs, 30 pages, no scans, 1.3 seconds |
| Per-page rows with 1,000-character chunks, 4 inputs | 2 PDFs read into 8 page rows; an HTML page returned not_pdf and a missing file http_error, both free |
OCRmyPDF's image-only skew.pdf on Apify | 1 page read with OCR, 740 words, 8 seconds; body text clean, a skewed heading split oddly |
The local Tesseract run above found 729 words on the same scan, so the actor's OCR matches a plain Tesseract call; the gain is doing it only where needed and across thousands of files.
What each row contains
| Field | What it holds |
|---|---|
url, finalUrl, status, httpStatus | What was requested and what happened; status is ok or why the file was not read |
text, textLength, wordCount | Full text, pages separated by a blank line |
pageCount, pagesExtracted, pagesOcr, pagesWithoutText | Page statistics |
title, author, subject, keywords, creator, producer | Document information |
creationDate, modificationDate, pdfVersion, fileSizeBytes | ISO dates with the PDF's time zone, version, size |
pages, chunks | Optional per-page texts and RAG chunks |
page, pageText, pageOcr | In one-row-per-page mode |
Honest limits
- Tables become text lines, not cells, and no Markdown headings are generated. For table extraction, use a layout-aware tool such as Camelot.
- Multi-column layouts usually come out column by column, following the PDF's text objects.
- Encrypted PDFs are reported as
encryptedand skipped; handwriting is not supported; OCR quality depends on the scan. - Files must be at a public URL (no uploads, no logins); 50 MB per file by default, 200 MB at most.
- Downloads follow each site's robots.txt and do not get around bot protection.
All input fields
Generated from the actor's live input schema (build 1.0.4).
| Field | Type | Default | What it does |
|---|---|---|---|
PDF URLs requiredurls | array | Direct links to PDF files, one per line. Up to 10,000 per run. | |
RowsoutputMode | string | document | One row per PDF (full text in 'text'), or one row per page (text in 'pageText', with the PDF's metadata repeated). Options: document, pages |
Add per-page texts to each PDF rowincludePageTexts | boolean | false | Adds 'pages': [{page, text, ocr}] in 'One row per PDF' mode. |
Chunk size for RAG (characters)chunkSize | integer | 0 | 0 = off. Otherwise adds 'chunks' (or 'pageChunks' per page): pieces of at most this many characters, cut at paragraph, sentence or word boundaries. |
Chunk overlap (characters)chunkOverlap | integer | 200 | Characters repeated between consecutive chunks (at most half the chunk size). |
OCR scanned pagesocr | boolean | true | Pages with fewer than 20 characters of embedded text are rendered at 300 dpi and read with Tesseract OCR. |
OCR languagesocrLanguages | array | ["eng"] | Languages in the scanned documents. More languages are slower. Options: eng, deu, fra, spa, ita, por, nld |
Maximum pages per PDFmaxPagesPerPdf | integer | 500 | Pages after this are skipped (the row says so). |
| Advanced | |||
Maximum file size (MB)maxFileSizeMb | integer | 50 | Larger files are reported as 'too_large' and not charged. |
Parallel downloadsmaxConcurrency | integer | 4 | PDFs downloaded and read at the same time. At most 2 requests run against the same host. |
Request timeout (seconds)requestTimeoutSecs | integer | 60 | How long to wait for each download. |
RetriesmaxRetries | integer | 2 | Retries for network errors and HTTP 429/5xx answers, with backoff. Retry-After is honoured up to 30 s. |
Call it from code
Start a run and get the results in one HTTP request with the Apify API (your token is under Console, Settings, API & Integrations):
curl -X POST "https://api.apify.com/v2/acts/adamhaleworks~pdf-text-extractor-ocr/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urls":["https://bitcoin.org/bitcoin.pdf"],"chunkSize":1000}'The run's Output tab offers 2 ready-made views; every view exports to CSV, Excel, JSON or XML:
- Overview:
url,status,pageCount,pagesOcr,wordCount,title,author,creationDate,text,warning,error - Pages (one row per page mode):
url,page,pageCount,pageOcr,pageTextLength,pageText
What it costs
Free to run right now. The actor charges no fee of its own today. Apify bills only the compute a run uses against your account's platform credit, and the free Apify plan includes $5 of usage every month. For scale: building all five of our actors and test-running each once on its default input used $0.03 of credit in total (2026-10-12).
Planned: pay-per-event pricing of $0.002 per PDF read ($2 per 1,000), $0.003 per scanned page read with OCR ($3 per 1,000) once the account's payout setup is finished. This page and the Apify page will show the change, and Apify notifies existing users before a price change takes effect.
At the planned prices, the 3-PDF default run would cost $0.006 and the scanned page another $0.003. Failed downloads, non-PDFs, encrypted or broken files, PDFs with no text and OCR that finds nothing would not be charged.
FAQ
How do I extract text from a PDF for free?
For one file, pdftotext from Poppler (pdftotext file.pdf out.txt) or a Python library such as pypdf. For scans, render pages with pdftoppm and read them with Tesseract. The commands on this page were tested on real files.
How does it know which pages need OCR?
A page with fewer than 20 non-space characters in its text layer is treated as scanned. Only those pages are rendered at 300 dpi and read with Tesseract, so normal PDFs stay fast and are not charged for OCR.
Can I use the output for RAG?
Yes. Set a chunk size and overlap and each PDF (or page) gets chunks cut at paragraph, sentence or word boundaries, ready to embed.
Which languages does OCR support?
English, German, French, Spanish, Italian, Portuguese and Dutch; you can combine several.
Can I upload PDFs instead of links?
Not directly. Put the files anywhere that gives a public download link without a login, and paste those links.
Try it on your own data
Paste PDF links, get clean text, per-page text, RAG-ready chunks and metadata (title, author, dates, page count). Scanned pages are read with Tesseract OCR in 7 languages. Failed, encrypted and empty files are free. Free while in launch; planned price $2 per 1,000 PDFs.
Open PDF Text Extractor: PDF to Text, Pages & OCR Opens the actor's page on apify.com. A free Apify account is needed to run it.