PDF Text Extractor: tables, OCR, Markdown, JSON per page
Extract source-aware per-page text, conservative tables as CSV/Excel-ready rows, metadata, and OCR for scanned PDF pages with confidence scores. Text spacing and reading order depend on the PDF's embedded text layer. Pay per page, no per-file fee.
Categories: AI, DEVELOPER_TOOLS, AUTOMATION. Pay per result, no subscription; Apify's free plan includes $5 of monthly usage.
Run it from the command line
curl -sS -X POST "https://api.apify.com/v2/acts/brenton8907~pdf-text-table-ocr-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"startUrls": [{"url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"}], "urlField": "url", "outputMode": "pages", "maxPagesPerDocument": 0, "extractText": true, "extractTables": true}'
Run it from Python
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("brenton8907/pdf-text-table-ocr-extractor").call(run_input={
"startUrls": [
{
"url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
}
],
"urlField": "url",
"outputMode": "pages",
"maxPagesPerDocument": 0,
"extractText": true,
"extractTables": true
})
for item in client.dataset(run.default_dataset_id).iterate_items():
print(item)
Use it as an MCP tool (Claude, Cursor, any MCP client)
The actor is listed in the official MCP registry as io.github.brenton-keller/pdf-text-table-ocr-extractor and served by Apify's hosted MCP server:
{
"mcpServers": {
"pdf-text-table-ocr-extractor": {
"url": "https://mcp.apify.com/?tools=brenton8907/pdf-text-table-ocr-extractor",
"headers": {
"Authorization": "Bearer YOUR_APIFY_TOKEN"
}
}
}
}
Pricing (pay per event)
| event | what | price | details |
|---|---|---|---|
page-record | Page processed (text layer) | $0.0005 | One PDF page extracted from its text layer: text, tables, metadata. |
ocr-page-record | Page processed (OCR) | $0.0060 | One scanned PDF page rendered and read with OCR (Tesseract), with confidence score. |
Documentation
Turn publicly reachable PDF URLs into structured, source-aware data: per-page embedded text, tables as real rows (with page numbers, detected headers and a Markdown rendering), document metadata, and OCR for scanned pages with a confidence score. Batch hundreds of files, or chain the actor after a crawler by pointing it at a dataset of URLs. Pure compute: no target website or proxy dependency.
Why this one and not the others: most PDF actors give you text only, or tables only, or OCR only. This actor does all three in one run, bills per page with no per-file or per-run fee, and ships detected tables in a shape you can open in Excel. Text-layer spacing and reading order remain source-dependent; inspect representative output before building an unattended downstream workflow.
What it does
- Downloads each PDF (size cap, retries, redirects followed) and checks it really is a PDF; HTML login walls and landing pages come back as a clear error row instead of a failed run.
- Extracts text page by page from the PDF's embedded text layer. Spacing is tuned for tightly kerned documents; reading order and character mapping still depend on how the source PDF was authored.
keepLayoutpreserves horizontal positioning when that is more useful than reading order. - Detects tables on every page with pdfplumber: ruled tables first, borderless tables by text alignment as a fallback, with a filter that rejects prose masquerading as a grid. Each table comes with
header,rows,markdown,bbox,row_count,col_count. - Runs OCR (Tesseract) on pages that have no text layer, so scanned PDFs and mixed documents just work. Every OCR page carries
ocr_confidence(0 to 100) so you can route low-confidence pages for review. - Returns metadata: title, author, subject, keywords, creator, producer, created/modified timestamps (ISO 8601), page count, page size and rotation.
- Three output shapes: one row per page, one row per document, or one row per table row (flat, CSV-ready).
- Page ranges (
1-3,7,10-), per-document page caps, encrypted PDFs with a password, and URL lists from another actor's dataset.
Who it's for
- Document search and citation workflows: page-numbered extracted text and source metadata, with representative PDFs reviewed before indexing.
- Finance, procurement and research teams: pull the tables out of reports, invoices and filings straight into Excel.
- Document automation (n8n, Make, Zapier, Clay): one HTTP call turns a PDF link into JSON fields.
- Crawler post-processing: crawl a site, collect the PDF links, hand the dataset to this actor.
- AI agents: one MCP tool for publicly reachable PDFs, using the embedded text layer when present and OCR for scanned pages.
Input examples
A few PDFs, one row per page (default):
{ "startUrls": [{ "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" }, { "url": "https://arxiv.org/pdf/1706.03762" }] }
Whole document as one row (full text + all tables), first 20 pages only:
{ "startUrls": [{ "url": "https://example.com/annual-report.pdf" }], "outputMode": "documents", "maxPagesPerDocument": 20 }
Only the tables, one row per table row, ready for CSV:
{ "startUrls": [{ "url": "https://example.com/price-list.pdf" }], "outputMode": "tableRows" }
Scanned invoices in German and English at 300 DPI:
{ "startUrls": [{ "url": "https://example.com/scan.pdf" }], "ocr": "auto", "ocrLanguages": "deu+eng", "ocrDpi": 300 }
Chain after a crawler: read PDF URLs from its dataset:
{ "inputDatasetId": "aBcDeFgHiJkLmNoPq", "urlField": "url", "outputMode": "documents" }
Pages 2 to 5 of a password-protected file, layout-preserving text, no tables:
{ "startUrls": [{ "url": "https://example.com/locked.pdf" }], "password": "s3cret", "pageRange": "2-5", "keepLayout": true, "extractTables": false }
Output
pages mode (default): one row per page
{
"source_url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
"document_id": "3f1c9a7e2b8d4c05",
"file_name": "fw9.pdf",
"file_size_bytes": 171243,
"title": "Form W-9 (Rev. March 2024)",
"author": "SE:W:CAR:MP",
"created_at": "2024-03-06T14:51:09Z",
"page_count": 6,
"pages_extracted": 6,
"page_number": 3,
"width_pt": 612.0,
"height_pt": 792.0,
"text": "Form W-9 (Rev. 3-2024) Page 3\nIF the entity/individual on line 1 is a(n) . . . THEN check the box for . . .\n...",
"char_count": 7654,
"word_count": 1284,
"extraction_method": "text",
"has_text_layer": true,
"ocr_confidence": null,
"table_count": 1,
"tables": [
{
"table_index": 0,
"page_number": 3,
"row_count": 4,
"col_count": 2,
"header": ["IF the entity/individual on line 1 is a(n) . . .", "THEN check the box for . . ."],
"rows": [["Corporation", "Corporation."], ["Individual or Sole proprietorship", "Individual/sole proprietor."]],
"markdown": "| IF the entity/individual on line 1 is a(n) . . . | THEN check the box for . . . |\n| --- | --- |\n| Corporation | Corporation. |",
"bbox": [36.0, 75.4, 300.2, 210.9],
"strategy": "lines"
}
],
"error": null,
"fetched_at": "2026-09-02T05:30:12Z"
}
| Field | Description |
|---|---|
source_url, final_url |
The URL you gave, and where it redirected to (if it did) |
document_id |
Stable short hash of the source URL; use it to group pages back into documents |
file_name, file_size_bytes |
From the Content-Disposition header or the URL path |
title, author, subject, keywords, creator, producer |
PDF metadata (null when absent) |
created_at, modified_at |
PDF dates normalized to ISO 8601 UTC |
page_count, pages_extracted |
Pages in the file, and how many this run processed (after pageRange / maxPagesPerDocument) |
page_number, width_pt, height_pt, rotation |
1-based page number and page geometry in points |
text, char_count, word_count |
Page text |
extraction_method |
text (text layer), ocr (Tesseract), or none (blank page, or OCR disabled on a scanned page) |
has_text_layer |
Whether the page had any text objects at all |
ocr_confidence |
Mean Tesseract word confidence 0 to 100 on OCR pages; null otherwise |
ocr_error, text_layer_text |
Set only when OCR failed on a page, or when OCR replaced a thin text layer (the original is kept here) |
tables, table_count |
Tables found on the page (see the table object above). strategy is lines (ruled) or text (alignment) |
error |
Null on success. Error rows have page_number: null and carry the reason (HTTP status, not a PDF, password, size cap, damaged file) |
raw_metadata |
With includeRaw: the untouched PDF metadata dictionary |
documents mode: one row per PDF
Same document fields, plus text (all pages joined), pages (array of {page_number, text, extraction_method, ocr_confidence, char_count, table_count}), tables (all tables, each with its page_number), text_pages, ocr_pages, empty_pages, and truncated (true if the run's charge limit cut the document short).
tableRows mode: one row per table row
source_url, document_id, file_name, title, page_number, table_index, row_index, col_count, header, cells, and record (a {header: cell} object when a header row was detected). Export to CSV or Excel and every table row is one spreadsheet row. In this mode OCR is skipped and only pages that contain tables are billed.
Export any of these as JSON, CSV, Excel or via the API like any Apify dataset.
Pricing
Pay per page processed. No per-run fee, no per-file fee. Failed downloads and error rows are free.
| Event | Price | When |
|---|---|---|
| Page processed (text layer) | $0.50 per 1,000 pages | Text and tables from the PDF text layer |
| Page processed (OCR) | $6.00 per 1,000 pages | Page had no text layer and was rendered and read with Tesseract |
Examples: a 40-page report costs $0.02. A 6-page form with tables costs $0.003. A 10-page scanned invoice batch of 50 files (500 OCR pages) costs $3.00. A crawler dataset of 1,000 single-page PDF brochures costs $0.50.
Set Maximum total charge on the run to cap spend; the actor stops cleanly at the cap and only pushes the pages it charged for. In tableRows mode, pages without tables are not billed.
Tips
- Speed: text-layer pages take well under a second each; OCR pages take a few seconds at 200 DPI. Use
pageRangeormaxPagesPerDocumenton long documents, and raiseconcurrency(with more memory) for large batches. - Scanned PDFs:
ocr: "auto"(default) only OCRs pages with no usable text layer, so mixed documents are billed at the cheap rate for their text pages. Useocr: "always"when a PDF has a bad embedded text layer (e.g. from a poor OCR pass) and you want a fresh read. - Borderless tables:
tableStrategy: "auto"looks for ruled tables first and falls back to text alignment. If a page shows tables that were missed, try"text"; if prose is being picked up as tables, use"lines". - Forms and multi-column layouts:
keepLayout: truekeeps horizontal positions with spaces so columns stay aligned in the text. - Text quality: embedded PDF text is not semantic document structure. Custom font encodings, columns, ligatures and unusual positioning can affect character mapping, spaces or reading order. Test representative files; try
keepLayout: true, orocr: "always"when the embedded layer is poor. - Large files:
maxFileSizeMbdefaults to 50. Raise it (and the run memory) for scanned books. - Chaining: point
inputDatasetIdat the output of any crawler and seturlFieldto the field holding the PDF link (dot paths likefile.urlwork).
Limits and notes
- Tables are detected on the text layer only. OCR pages return text, not tables.
- Text-layer output is source-dependent. The actor corrects common tight-kerning spacing, but cannot guarantee natural reading order or perfect characters for every PDF encoding and layout.
- Table detection is heuristic. Ruled tables are reliable; borderless tables depend on consistent column alignment. LaTeX-style tables with only horizontal rules are often not detected.
- OCR languages installed:
eng,deu,fra,spa,ita,por,nld. Ask on the Issues tab for others. - Password protection: one
passwordapplies to every file in the run. - Remote URL lists (
requestsFromUrl) are not supported yet; useinputDatasetIdor paste the URLs. - Only publicly reachable URLs (or Apify key-value store record URLs) can be fetched. The actor does not log in to anything.
- You are responsible for having the right to process the documents you submit.
Use from an AI agent (MCP)
Every Apify actor is available as a tool through the Apify MCP server. Add it to Claude, Cursor or any MCP client and call this actor with the JSON input above. Suggested pattern: start with outputMode: "pages", inspect extraction_method, ocr_confidence and a text sample, then use documents mode after representative source files pass your quality check.
Support
Open an issue on the actor page with the run ID and the PDF URL. If a file parses in your PDF viewer but fails here, attach it to the issue and it will be looked at.