PDF Text Extractor: PDF to Markdown, OCR & RAG Chunks
Pricing
from $0.00005 / actor start
PDF Text Extractor: PDF to Markdown, OCR & RAG Chunks
Extract text and Markdown from PDFs, Word (DOCX) files and scanned images. Automatic OCR only on scanned pages (13 languages), ruled tables as Markdown, per-page output for citations, RAG chunks and language detection. For LLM, RAG and AI agent pipelines. $1 per 1,000 pages; failed files are free.
Pricing
from $0.00005 / actor start
Rating
0.0
(0)
Developer
Catalin Friptu
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
5 hours ago
Last modified
Categories
Share
Turn PDFs, Word (.docx) files and scanned images into clean text and Markdown for LLMs, RAG pipelines, search indexes and AI agents. Pages that are scans or photos with no text layer are OCR'd automatically. Every other page is read straight from its text layer: that's fast, exact and costs no OCR time.
$1 per 1,000 pages, +$7 per 1,000 only for pages that need OCR. Failed files are free.
- PDF to Markdown that keeps structure. Headings come from font sizes, bullets and numbered lists are kept, and ruled tables become Markdown tables.
- OCR only where it's needed, page by page. A PDF with 40 typed pages and 2 scanned pages gets OCR on those 2 only. 13 OCR languages, including German, French, Spanish, Romanian, Russian and Ukrainian.
- RAG-ready chunks. Set a chunk size and overlap and each row comes with pre-split chunks for your embedding model.
- One row per document or one row per page, so you can cite page numbers in answers.
- Language detection on every row (
en,de,ro,ru...). - PDF, DOCX, PNG, JPEG, TIFF, WEBP, BMP, GIF from URLs, uploads or an Apify key-value store.
What does PDF Text Extractor do?
You give it document URLs (or upload files), and it returns one dataset row per document or per page with:
| Field | What you get |
|---|---|
text | Plain text in reading order. |
markdown | The same content as Markdown: headings, lists, tables. |
chunks | Optional pre-split chunks with character offsets, ready to embed. |
page, page_count | Page numbers for citations. |
used_ocr, ocr_pages | Which pages were scans and went through OCR. |
language | Detected language code. |
error | Why a file failed (and it wasn't charged). |
Example output
A real run on two PDFs in document mode:
| file_name | pages | characters | OCR | language | price |
|---|---|---|---|---|---|
| 1706.03762v7.pdf ("Attention Is All You Need", born-digital) | 15 | 38,262 | no | en | 15 x $0.001 = $0.015 |
| Treated_Limited_Declassification_of_JUMPSEAT.pdf (scanned memo) | 3 | 5,116 | 3 pages | en | 3 x ($0.001 + $0.007) = $0.024 |
Who it's for
- AI engineers loading company documents, reports, papers or contracts into a vector database.
- Agent and MCP builders who need a "read this PDF" tool that also copes with scans.
- Data teams digitising archives of scanned letters, invoices or government records.
- Legal, research and compliance teams that need searchable text from contracts, reports and filings.
How to convert a PDF to text or Markdown
- Click Try for free and create a free Apify account (no credit card needed).
- Paste PDF links into Document URLs, or upload files.
- Pick One row per document or One row per page, and set a Chunk size (e.g. 1500) if you feed a vector database.
- Click Start, then download the text as JSON, CSV or Excel, or read it through the API.
Input
| Field | Default | Description |
|---|---|---|
startUrls | Direct links to PDF, DOCX or image files. | |
files | Files you upload in the input form. | |
keyValueStoreId | One of your key-value stores. Every file in it is processed; JSON and text records are skipped. | |
keyValueStoreKeys | all | Only these keys from that store. |
outputMode | document | document: one row per file, all pages joined. page: one row per page. |
maxPagesPerDocument | all | Extract only the first N pages of each file. |
ocrMode | auto | auto: OCR pages with no text layer. always: OCR every page, for PDFs whose text layer is garbage. off: never OCR. |
ocrLanguages | ["eng"] | Languages printed in the scans: eng deu fra spa ita por nld pol ces ron rus ukr tur. Pick only the ones you need, because each extra language slows OCR. |
ocrDpi | 300 | Render resolution for scanned PDF pages. Use 400 for tiny print and 200 for speed. |
detectLanguage | true | Add the language field. |
chunkSize | 0 (off) | Split each row's Markdown into chunks of about this many characters. 1000–2000 suits most embedding models. |
chunkOverlap | 200 | Characters shared by neighbouring chunks. |
{"startUrls": [{"url": "https://arxiv.org/pdf/1706.03762"},{"url": "https://example.com/scanned-contract.pdf"}],"outputMode": "page","ocrLanguages": ["eng", "deu"],"chunkSize": 1500,"chunkOverlap": 200}
Output
Every row has the same fields. A field without a value is null, never an empty string.
| Field | Description |
|---|---|
source_url | The URL you gave, or the storage URL of an uploaded file. |
file_name | Taken from the URL, the server's Content-Disposition header, or the key-value store key. |
document_type | pdf, docx or image. |
page | Page number (page mode). null in document mode. |
page_count | Total pages in the file. |
pages_extracted | Pages included in this row (document mode). |
text | Plain text in reading order. |
markdown | The same content as Markdown: # headings, - lists, ` |
char_count | Length of text. |
used_ocr | true if the row's text came from OCR (in document mode: from at least one page). |
ocr_pages | How many pages were OCR'd (document mode). |
language | ISO 639-1 code of the detected language, or null if the text is too short to tell. |
chunks | When chunkSize > 0: [{"index", "start", "end", "text"}, ...], where start and end are character offsets into markdown. |
full_output_url | Set only for huge documents whose text doesn't fit in one dataset row (about 8 MB). The row then carries a preview, and this link holds the full text, Markdown and chunks. |
error | Why a file or page failed (download failed: HTTP 404, PDF is password-protected, the URL returned an HTML web page...). null on success. |
A born-digital paper in page mode (text and chunks shortened):
{"source_url": "https://arxiv.org/pdf/1706.03762","file_name": "1706.03762v7.pdf","document_type": "pdf","page": 1,"page_count": 15,"pages_extracted": null,"text": "Provided proper attribution is provided, Google hereby grants permission to ...\nAttention Is All You Need\nAshish Vaswani∗ Noam Shazeer∗ ...","markdown": "### Provided proper attribution is provided, ...\n\n# Attention Is All You Need\n\nAshish Vaswani∗ Noam Shazeer∗ ...\n\n### Abstract\n\nThe dominant sequence transduction models are based on complex recurrent or convolutional neural networks ...","char_count": 2819,"used_ocr": false,"ocr_pages": null,"language": "en","chunks": [{"index": 0, "start": 0, "end": 1341, "text": "### Provided proper attribution ..."}, {"index": 1, "start": 1148, "end": 2462, "text": "..."}],"full_output_url": null,"error": null}
A 3-page scanned government memo in document mode (no text layer, so every page was OCR'd):
{"source_url": "https://upload.wikimedia.org/wikipedia/commons/9/93/Treated_Limited_Declassification_of_JUMPSEAT.pdf","file_name": "Treated_Limited_Declassification_of_JUMPSEAT.pdf","document_type": "pdf","page": null,"page_count": 3,"pages_extracted": 3,"text": "CQ05159643\n\nApproved for Release: 2026/01/07 C05159643z\n\nNATIONAL RECONNAISSANCE OFFICE\n14675 Lee Road ... JUMPSEAT was a Highly Elliptical Orbit (HEO) signals collection satellite program ...","markdown": "## CQ05159643\n\nApproved for Release: 2026/01/07 C05159643z\n\nNATIONAL RECONNAISSANCE OFFICE 14675 Lee Road ...","char_count": 5116,"used_ocr": true,"ocr_pages": 3,"language": "en","chunks": null,"full_output_url": null,"error": null}
The dataset has four views: Extracted text, Markdown (with chunks), Errors, and All fields. Export as JSON, CSV or Excel, or read the rows straight from the API:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_API_TOKEN>")run = client.actor("malmurk/pdf-text-extractor").call(run_input={"startUrls": [{"url": "https://arxiv.org/pdf/1706.03762"}],"outputMode": "page", "chunkSize": 1500,})for row in client.dataset(run["defaultDatasetId"]).iterate_items():for chunk in row["chunks"] or []:print(row["file_name"], row["page"], chunk["text"][:80]) # -> your embedding + vector DB
How much does it cost to extract text from a PDF?
Pay per event (Apify platform usage is included):
| Event | Price | Charged for |
|---|---|---|
page | $1 per 1,000 pages | Every extracted page. |
ocr_page | +$7 per 1,000 pages | Extra, only for pages whose text came from OCR. |
A text-layer page costs $1 per 1,000. A scanned page costs $1 + $7 = $8 per 1,000. With ocrMode: auto, OCR runs only on pages that have no text layer, so a typed 40-page PDF with 2 scanned pages pays the surcharge on those 2 pages only.
- A file that fails (dead link, HTML page, password-protected or damaged PDF, unsupported format) costs nothing, and you get an error row explaining why.
- In page mode, a page that fails costs nothing.
maxPagesPerDocumentcaps what each file can cost.- A start fee of $0.00005 per GB of run memory ($0.00005 at the default 1 GB) is charged once per run.
- The run stops cleanly at the maximum charge you set: no page is extracted that can't be paid for. Before each page that might need OCR, the Actor reserves the full OCR price, so near the limit it stops one page early rather than run OCR it can't bill. With
ocrMode: off, or for DOCX files, only the $1 rate is reserved.
DOCX files have no fixed pages, so they are counted in pages of about 3,000 characters, roughly one printed page. An image file is one page per frame (a multi-page TIFF counts each frame).
Supported files
| Type | How it's read |
|---|---|
| PDF with a text layer | Text layer, with headings, lists and ruled tables as Markdown. |
| Scanned PDF, or a PDF with some scanned pages | Tesseract OCR on the pages without a text layer. |
| DOCX (Word 2007+) | Paragraphs, Word heading styles, lists and tables. |
| PNG, JPEG, TIFF (multi-page), WEBP, BMP, GIF | Tesseract OCR. |
Not supported: legacy .doc, .xls/.xlsx, .pptx and password-protected PDFs. Files are limited to 200 MB.
Limitations
- Markdown structure is best-effort. Headings are inferred from font size and tables from ruled lines. Tables drawn without lines (common in scientific papers) come out as text in reading order. Multi-column layouts are read row by row.
- OCR quality follows scan quality. Clean 300 DPI scans come out very well. Handwriting, stamps and strike-through marks produce noise. OCR'd pages get paragraph-level Markdown only.
- Rotated text, such as the vertical arXiv stamp in a page margin, is left out of text-layer pages.
- The link must point at the file itself. A page that shows a "Download" button is HTML and will return an error row.
Use with AI agents and MCP
Give Claude, Cursor, ChatGPT or your own agent a "read this PDF" tool that also copes with scans, through the Apify MCP server. Add this server URL to your MCP client:
https://mcp.apify.com?tools=malmurk/pdf-text-extractor
Then ask, for example: "Read https://arxiv.org/pdf/1706.03762 and summarise section 3." The agent gets Markdown with page numbers back. For long documents, set outputMode: "page" and maxPagesPerDocument so the agent only pulls what it needs.
Integrations: LangChain, LlamaIndex, n8n, Make, Zapier
- LangChain and LlamaIndex: load the dataset rows as documents with the LangChain or LlamaIndex Apify integrations, or use the
chunksfield directly. - n8n, Make and Zapier: a new PDF arrives in email, Google Drive or a form, the Actor extracts the text, and the next step sends it to an LLM, a vector database or a spreadsheet. See the n8n, Make and Zapier guides.
- Webhooks: get notified when a run finishes (webhooks).
- API: one HTTP call runs the Actor and returns the text.
$curl -X POST "https://api.apify.com/v2/acts/malmurk~pdf-text-extractor/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>" -H "Content-Type: application/json" -d '{"startUrls": [{"url": "https://arxiv.org/pdf/1706.03762"}], "outputMode": "page", "chunkSize": 1500}'
FAQ
Does it work on scanned PDFs?
Yes. With ocrMode: auto (default), every page without a text layer is OCR'd with Tesseract. Pick the languages printed in the scans with ocrLanguages.
Can I extract tables from a PDF? Tables drawn with ruled lines become Markdown tables. Tables without lines come out as text in reading order (see Limitations).
What chunk size should I use for RAG?
1000 to 2000 characters with 200 overlap suits most embedding models. Use outputMode: "page" too if you want page numbers on every chunk.
Can it read Word, PowerPoint or Excel files?
Word (.docx) yes. Legacy .doc, .pptx and .xlsx are not supported.
Does it use an LLM or send my documents to a third-party API? No. Text extraction and OCR run inside the Actor with open-source libraries (pdfplumber, Tesseract). No LLM is called.
Am I charged for files that fail? No. A dead link, an HTML page, a password-protected or damaged PDF costs nothing and gets an error row.
Related Actors
- Website Screenshot & URL to PDF: save web pages as PDFs or full-page screenshots in bulk.
- Businesses Without Websites Finder: local business leads with no website, a dead link or only a Facebook page.
How it works
PDF text and tables are read with pdfplumber (MIT). Scanned pages are rendered with pypdfium2 (Apache-2.0 / BSD-3) and OCR'd with Tesseract (Apache-2.0). DOCX is read with python-docx (MIT), and language detection uses py3langid (BSD-3). Every component has a permissive license.
Local development
python -m venv .venv && .venv/Scripts/pip install -r requirements-dev.txt # Linux/macOS: .venv/bin/pippython -m pytest -q # OCR tests need tesseract + 'eng' data, else they are skippedpython tests/fixtures/make_fixtures.py # regenerate the test PDFs/DOCX/PNGdocker build -t pdf-text-extractor . && docker run --rm -v "$PWD/INPUT.json:/usr/src/app/INPUT.json" pdf-text-extractor
On Windows without system language data, put eng.traineddata from tessdata_fast in .tessdata/ (gitignored) and the tests pick it up.