# PDF Text Extractor: tables, OCR, Markdown, JSON per page (`brenton8907/pdf-text-table-ocr-extractor`) Actor

Turn PDF URLs into clean data: per-page text, tables as real rows (CSV/Excel-ready, with page numbers and Markdown), document metadata, and OCR for scanned pages with confidence scores. Chain from any dataset of URLs. Pay per page, no per-file fee.

- **URL**: https://apify.com/brenton8907/pdf-text-table-ocr-extractor.md
- **Developed by:** [Brenton Keller](https://apify.com/brenton8907) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 page processed (text layer)s

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## PDF Text Extractor with Tables & OCR

Turn any list of **PDF URLs** into clean, structured data: **per-page 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, no proxies, nothing that breaks when a site changes.

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 tables in a shape you can open in Excel or feed to an LLM without post-processing.

### 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 text layer (reading order, or layout-preserving with `keepLayout`).
- 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

- **RAG and LLM pipelines**: per-page text with page numbers for citations, tables as Markdown so models read them correctly.
- **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**: a single MCP tool that reads any PDF an agent encounters, including scans.

### Input examples

**A few PDFs, one row per page (default):**

```json
{ "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:**

```json
{ "startUrls": [{ "url": "https://example.com/annual-report.pdf" }], "outputMode": "documents", "maxPagesPerDocument": 20 }
```

**Only the tables, one row per table row, ready for CSV:**

```json
{ "startUrls": [{ "url": "https://example.com/price-list.pdf" }], "outputMode": "tableRows" }
```

**Scanned invoices in German and English at 300 DPI:**

```json
{ "startUrls": [{ "url": "https://example.com/scan.pdf" }], "ocr": "auto", "ocrLanguages": "deu+eng", "ocrDpi": 300 }
```

**Chain after a crawler: read PDF URLs from its dataset:**

```json
{ "inputDatasetId": "aBcDeFgHiJkLmNoPq", "urlField": "url", "outputMode": "documents" }
```

**Pages 2 to 5 of a password-protected file, layout-preserving text, no tables:**

```json
{ "startUrls": [{ "url": "https://example.com/locked.pdf" }], "password": "s3cret", "pageRange": "2-5", "keepLayout": true, "extractTables": false }
```

### Output

#### `pages` mode (default): one row per page

```json
{
  "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 `pageRange` or `maxPagesPerDocument` on long documents, and raise `concurrency` (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. Use `ocr: "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: true` keeps horizontal positions with spaces so columns stay aligned in the text.
- **Large files**: `maxFileSizeMb` defaults to 50. Raise it (and the run memory) for scanned books.
- **Chaining**: point `inputDatasetId` at the output of any crawler and set `urlField` to the field holding the PDF link (dot paths like `file.url` work).

### Limits and notes

- Tables are detected on the text layer only. OCR pages return text, not tables.
- 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 `password` applies to every file in the run.
- Remote URL lists (`requestsFromUrl`) are not supported yet; use `inputDatasetId` or 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](https://mcp.apify.com). Add it to Claude, Cursor or any MCP client and call this actor with the JSON input above. Suggested pattern: `outputMode: "documents"` with `maxPagesPerDocument` set, so the agent gets one object per PDF with the text and every table as Markdown.

### 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.

# Actor input Schema

## `startUrls` (type: `array`):

Direct links to PDF files (one per line, or upload a text file of URLs). Apify key-value store record URLs work too.

## `inputDatasetId` (type: `string`):

Optional: read PDF URLs from an existing Apify dataset (ID or name), e.g. the output of a crawler. Combined with the URLs above.

## `urlField` (type: `string`):

Field holding the PDF URL in each dataset item. Dot paths like `file.url` are allowed.

## `outputMode` (type: `string`):

Pages: one dataset row per PDF page. Documents: one row per file with the whole text and all tables. Table rows: only tables, one row per table row, with page number, header and cells.

## `pageRange` (type: `string`):

Which pages to process, 1-based, e.g. `1-3,7,10-`. Empty = all pages.

## `maxPagesPerDocument` (type: `integer`):

Stop after this many pages of each PDF (after the page range is applied). 0 = no limit.

## `extractText` (type: `boolean`):

Per-page text from the PDF text layer.

## `extractTables` (type: `boolean`):

Detect tables on each page and return them as rows (plus a Markdown rendering).

## `tableStrategy` (type: `string`):

`lines` finds tables drawn with borders (most reliable). `text` infers tables from column alignment (for borderless tables, more false positives). `auto` tries lines first and falls back to text when a page has none.

## `keepLayout` (type: `boolean`):

Preserve horizontal positioning with spaces (useful for columns and forms). Off = reading-order text.

## `ocr` (type: `string`):

Scanned pages have no text layer. `auto` runs Tesseract OCR only on pages whose text layer has fewer than `ocrMinChars` characters. OCR pages are billed at the OCR rate.

## `ocrMinChars` (type: `integer`):

In `auto` mode, a page with fewer extracted characters than this is treated as scanned and sent to OCR.

## `ocrLanguages` (type: `string`):

Tesseract language codes joined with `+`, e.g. `eng+deu`. Installed: eng, deu, fra, spa, ita, por, nld.

## `ocrDpi` (type: `integer`):

Render resolution for OCR. 200 is a good default; 300 for small print (slower).

## `password` (type: `string`):

Password for encrypted PDFs (applied to every file).

## `maxFileSizeMb` (type: `integer`):

Files larger than this are skipped with an error row instead of being downloaded.

## `downloadTimeoutSecs` (type: `integer`):

Per-file download timeout in seconds.

## `concurrency` (type: `integer`):

How many PDFs to download and process at once. Raise memory for large files when increasing this.

## `includeRaw` (type: `boolean`):

Attach the untouched PDF metadata dictionary to each row.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ],
  "urlField": "url",
  "outputMode": "pages",
  "maxPagesPerDocument": 0,
  "extractText": true,
  "extractTables": true,
  "tableStrategy": "auto",
  "keepLayout": false,
  "ocr": "auto",
  "ocrMinChars": 25,
  "ocrLanguages": "eng",
  "ocrDpi": 200,
  "maxFileSizeMb": 50,
  "downloadTimeoutSecs": 60,
  "concurrency": 3,
  "includeRaw": false
}
```

# Actor output Schema

## `pages` (type: `string`):

Dataset items (JSON). Append ?format=csv or ?format=xlsx to the URL for spreadsheets.

## `run` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "startUrls": [
        {
            "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("brenton8907/pdf-text-table-ocr-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "startUrls": [{ "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("brenton8907/pdf-text-table-ocr-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ]
}' |
apify call brenton8907/pdf-text-table-ocr-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,brenton8907/pdf-text-table-ocr-extractor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/6WYPCFds8AiGepdD4/builds/FXDCXPxNLcPe17J5Z/openapi.json
