PDF Text Extractor: PDF to Markdown, OCR & RAG Chunks avatar

PDF Text Extractor: PDF to Markdown, OCR & RAG Chunks

Pricing

from $0.00005 / actor start

Go to Apify Store
PDF Text Extractor: PDF to Markdown, OCR & RAG Chunks

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

Catalin Friptu

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

5 hours ago

Last modified

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:

FieldWhat you get
textPlain text in reading order.
markdownThe same content as Markdown: headings, lists, tables.
chunksOptional pre-split chunks with character offsets, ready to embed.
page, page_countPage numbers for citations.
used_ocr, ocr_pagesWhich pages were scans and went through OCR.
languageDetected language code.
errorWhy a file failed (and it wasn't charged).

Example output

A real run on two PDFs in document mode:

file_namepagescharactersOCRlanguageprice
1706.03762v7.pdf ("Attention Is All You Need", born-digital)1538,262noen15 x $0.001 = $0.015
Treated_Limited_Declassification_of_JUMPSEAT.pdf (scanned memo)35,1163 pagesen3 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

  1. Click Try for free and create a free Apify account (no credit card needed).
  2. Paste PDF links into Document URLs, or upload files.
  3. Pick One row per document or One row per page, and set a Chunk size (e.g. 1500) if you feed a vector database.
  4. Click Start, then download the text as JSON, CSV or Excel, or read it through the API.

Input

FieldDefaultDescription
startUrlsDirect links to PDF, DOCX or image files.
filesFiles you upload in the input form.
keyValueStoreIdOne of your key-value stores. Every file in it is processed; JSON and text records are skipped.
keyValueStoreKeysallOnly these keys from that store.
outputModedocumentdocument: one row per file, all pages joined. page: one row per page.
maxPagesPerDocumentallExtract only the first N pages of each file.
ocrModeautoauto: 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.
ocrDpi300Render resolution for scanned PDF pages. Use 400 for tiny print and 200 for speed.
detectLanguagetrueAdd the language field.
chunkSize0 (off)Split each row's Markdown into chunks of about this many characters. 1000–2000 suits most embedding models.
chunkOverlap200Characters 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.

FieldDescription
source_urlThe URL you gave, or the storage URL of an uploaded file.
file_nameTaken from the URL, the server's Content-Disposition header, or the key-value store key.
document_typepdf, docx or image.
pagePage number (page mode). null in document mode.
page_countTotal pages in the file.
pages_extractedPages included in this row (document mode).
textPlain text in reading order.
markdownThe same content as Markdown: # headings, - lists, `
char_countLength of text.
used_ocrtrue if the row's text came from OCR (in document mode: from at least one page).
ocr_pagesHow many pages were OCR'd (document mode).
languageISO 639-1 code of the detected language, or null if the text is too short to tell.
chunksWhen chunkSize > 0: [{"index", "start", "end", "text"}, ...], where start and end are character offsets into markdown.
full_output_urlSet 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.
errorWhy 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 ApifyClient
client = 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):

EventPriceCharged for
page$1 per 1,000 pagesEvery extracted page.
ocr_page+$7 per 1,000 pagesExtra, 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.
  • maxPagesPerDocument caps 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

TypeHow it's read
PDF with a text layerText layer, with headings, lists and ruled tables as Markdown.
Scanned PDF, or a PDF with some scanned pagesTesseract 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, GIFTesseract 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 chunks field 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.

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/pip
python -m pytest -q # OCR tests need tesseract + 'eng' data, else they are skipped
python tests/fixtures/make_fixtures.py # regenerate the test PDFs/DOCX/PNG
docker 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.