# PDF Text & Table Extractor with CSV Tables + OCR (`inn_corp/pdf-text-table-extractor`) Actor

Extract text, RAG-ready markdown, and tables from PDF URLs. Every detected table becomes its own CSV-ready dataset record. No OCR; scanned pages are flagged, never guessed.

- **URL**: https://apify.com/inn\_corp/pdf-text-table-extractor.md
- **Developed by:** [Inn Corp](https://apify.com/inn_corp) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 document processeds

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 & Table Extractor with CSV Tables

Give it PDF URLs, get back clean per-page text, RAG-ready markdown, and, the
part the other extractors skip, **every detected table as its own dataset
record** with rows, dimensions, and a ready-to-save CSV string. Built for
data pipelines, spreadsheets, and AI agents that need the table out of the
PDF and into columns, not buried in a wall of page text.

### What it does

- Fetches each URL with a single plain HTTP GET, streams it to disk, and
  processes it page by page (memory stays flat on big documents).
- Writes three record types, discriminated by `recordType`:
  - **`page`**, one per processed page: extracted text, a light markdown
    rendering, detected tables, and `pageHasText` so you can spot scanned
    pages programmatically.
  - **`table`**, one per detected table: the rows as a 2D string array,
    `rowCount`, `columnCount`, and the same table rendered as an RFC 4180
    CSV string you can write straight to a file.
  - **`summary`**, one per document, always: status, page counts, tables
    found, file size, and the PDF's own metadata.
- Page ranges (`"1-5,8,12-"`), size caps checked with a HEAD request before
  any download, and a per-document timeout that keeps partial results.

### What it deliberately does not do

- **No OCR unless you turn it on.** Scanned pages have no text layer, so by
  default they honestly come back with empty text and `pageHasText: false`.
  Nothing is hallucinated from images. Turn on `enableOcr` to read those
  pages instead; see "Optional OCR for scanned pages" below for exactly how
  that works and how it is priced.
- **No perfect tables.** Table detection (pdfplumber) is heuristic. The
  `tableStrategy` input exposes both detection modes, ruled lines and text
  alignment; if a table looks wrong or missing, try the other one. Complex
  layouts (merged cells, forms, multi-column pages) can confuse either.
- **No invented metadata.** `title`, `author`, and `created` come from the
  PDF's own metadata dictionary. Missing values are `null`, never guessed.
- Markdown is deliberately simple: short all-caps lines become headings and
  detected tables are appended as markdown tables (first row rendered as the
  header, a layout convention rather than detection). PDFs whose fonts lack
  proper Unicode maps can yield odd characters; that is the document, passed
  through honestly.

### Documents and privacy

This Actor processes documents you supply. Use it only on PDFs you have the
right to access. It fetches URLs with a plain HTTP GET and never bypasses
paywalls, logins, or DRM. Your documents' content, including any personal
data, is yours; results go only to your own Apify dataset.

Password-protected PDFs are rejected with an `encrypted` summary record (and
no charge); the Actor never attempts to break protection.

### Output examples

Real records from a run against the IRS Form 1040-ES package
(`https://www.irs.gov/pub/irs-pdf/f1040es.pdf`, 16 pages, 12 tables found).

A `table` record (page 2, the standard deduction table):

```json
{
  "recordType": "table",
  "url": "https://www.irs.gov/pub/irs-pdf/f1040es.pdf",
  "page": 2,
  "tableIndexOnPage": 1,
  "rows": [
    ["IF your 2026 filing status is...", "THEN your standard\ndeduction is..."],
    ["Married filing jointly or\nQualifying surviving spouse", "$32,200"],
    ["Head of household", "$24,150"],
    ["Single or Married filing separately", "$16,100"]
  ],
  "rowCount": 4,
  "columnCount": 2,
  "csv": "IF your 2026 filing status is...,\"THEN your standard\ndeduction is...\"\r\n\"Married filing jointly or\nQualifying surviving spouse\",\"$32,200\"\r\nHead of household,\"$24,150\"\r\nSingle or Married filing separately,\"$16,100\"\r\n"
}
```

The matching `summary` record:

```json
{
  "recordType": "summary",
  "url": "https://www.irs.gov/pub/irs-pdf/f1040es.pdf",
  "status": "ok",
  "error": null,
  "pageCountTotal": 16,
  "pagesProcessed": 16,
  "tablesFound": 12,
  "fileSizeBytes": 331490,
  "metadata": {
    "title": "2026 Form 1040-ES",
    "author": "W:CAR:MP:FP",
    "created": "2026-02-12T06:17:45-05:00"
  },
  "fetchedAt": "2026-08-24T06:57:34+00:00"
}
```

`page` records carry `url`, `page` (1-based), `text`, `markdown`,
`tableCount`, `pageHasText`, `charCount`, `ocrText` (null unless `enableOcr`
is on and OCR found something), and (in the default mode) the `tables`
array. `summary.status` is one of `ok`, `error`, `timeout`, `too-large`, or
`encrypted`; every failure states its reason in `error`.

### Typical uses

- Pull rate tables, price lists, or financial statements out of PDFs and
  into CSV without copy-paste.
- Feed page markdown into a RAG index so chunks keep their headings and
  tables.
- Batch-convert report archives while `pageHasText` flags the scanned pages
  that need an OCR pass elsewhere.
- Let an AI agent read a PDF's actual tables instead of re-typing them.

### Optional OCR for scanned pages

Off by default (`enableOcr: false`); turning it on changes nothing about
pages that already have native text. When it is on, any page whose native
text comes back empty or near-empty (pdfplumber found no real text layer)
is rendered to an image and read with Tesseract OCR. The result goes into a
new `ocrText` field on that page's record. It never touches `text` or
`markdown`: `text` stays "native extraction only," always, so nothing that
already relied on it changes meaning; `ocrText` is a separate, clearly
labeled field precisely because OCR output is not as reliable as native
text extraction. There is no blending of the two.

Be honest with yourself about what this buys you:

- **OCR quality varies with scan quality.** A clean, high-contrast scan
  reads well; a skewed, low-resolution, or handwritten page can come back
  garbled or empty. `ocrText` is null both when OCR was never attempted
  (disabled, or the page already had real text) and when it ran but found
  nothing usable; the field alone cannot tell those apart, only your own
  `enableOcr` setting and `pageHasText` can.
- **English by default.** This build uses Tesseract's default language
  pack. Non-English scans will read poorly or fail outright.
- **Pages with real text are never OCR'd**, so you are never billed for
  OCR on a page that did not need it.
- Only meaningful when page records are emitted (`outputMode` other than
  `tables`); table-only runs have no page record for `ocrText` to live on.

### Input

| Field | Meaning |
| --- | --- |
| `pdfUrls` | Direct links to the PDFs. Required. |
| `pageRange` | 1-based, like `"1-5,8,12-"` (`"12-"` = to the end). Empty = all pages. |
| `outputMode` | `both` (default: page records with text, markdown, and tables, plus table records), `text` (no table detection), `tables` (table records only), `markdown-only` (page records without the raw text field). |
| `tablesAsRecords` | Each detected table as its own CSV-ready record. Default on. |
| `maxPages` | Per-document page cap, default 200. |
| `maxFileSizeMb` | Size cap, default 50. Checked via HEAD before downloading; oversized documents are rejected free of charge. |
| `timeoutPerPdfSecs` | Wall-clock budget per document, default 120. On timeout you keep the pages already extracted and the summary says `timeout`. |
| `tableStrategy` | `lines` (ruled tables, default) or `text` (alignment-based). Detection is heuristic; if results look wrong, try the other strategy. |
| `enableOcr` | Off by default. When on, pages with empty or near-empty native text are OCR'd with Tesseract into a separate `ocrText` field, billed as an add-on `page-ocr` event. See "Optional OCR for scanned pages" above. |

### Fair pricing

Pay per document processed, per page extracted, and per table extracted,
once pay-per-event pricing is enabled. Failed downloads, oversized rejects,
and encrypted documents cost nothing; a timeout is only charged when it
still delivered pages. No subscription.

OCR is a paid add-on on top of normal extraction, not a replacement for it:
when `enableOcr` is on and a page's native text is empty or near-empty, a
successful OCR read is billed as its own `page-ocr` event *in addition to*
that page's normal `page-extracted` charge, never instead of it. OCR that
runs but finds nothing usable (garbled scan, blank page) is not charged;
you only pay when you get OCR text back.

# Actor input Schema

## `pdfUrls` (type: `array`):

Direct links to the PDFs to process. Supply only documents you have the right to access: each is fetched with a plain HTTP GET, and the Actor never bypasses logins, paywalls, or DRM.

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

Pages to process, 1-based, like "1-5,8,12-" ("12-" means page 12 to the end). Leave empty for all pages.

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

"Text and tables" (default) writes page records with text, markdown, and tables. "Text only" skips table detection. "Tables only" writes just table records. "Markdown only" writes page records whose markdown embeds the detected tables, without the raw text field.

## `tablesAsRecords` (type: `boolean`):

When on, every detected table also becomes its own dataset record with rows, rowCount, columnCount, and a ready-to-save CSV string.

## `maxPages` (type: `integer`):

Processing stops after this many pages per document.

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

Documents over this size are rejected with a "too-large" summary record, checked with a HEAD request before any download, and are never charged.

## `timeoutPerPdfSecs` (type: `integer`):

Wall-clock budget per document, download included. When it runs out, the pages already extracted are kept and the summary record says "timeout".

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

Table detection is heuristic and imperfect. "Ruled lines" finds tables drawn with visible lines; "Text alignment" infers cells from how the text lines up. If results look wrong, try the other strategy.

## `enableOcr` (type: `boolean`):

Off by default: nothing changes unless you turn this on. When on, any page whose native text is empty or near-empty is rendered to an image and read with Tesseract OCR; the result lands in a separate ocrText field (never blended into text or markdown) and is billed as an additional page-ocr event on top of the normal page-extracted charge. OCR quality varies with scan quality and is English-only by default; it is not as reliable as native text extraction.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "pageRange": "1-5,8,12-",
  "outputMode": "both",
  "tablesAsRecords": true,
  "maxPages": 200,
  "maxFileSizeMb": 50,
  "timeoutPerPdfSecs": 120,
  "tableStrategy": "lines",
  "enableOcr": false
}
```

# Actor output Schema

## `records` (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 = {
    "pdfUrls": [
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("inn_corp/pdf-text-table-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 = { "pdfUrls": ["https://www.irs.gov/pub/irs-pdf/fw9.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("inn_corp/pdf-text-table-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 '{
  "pdfUrls": [
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ]
}' |
apify call inn_corp/pdf-text-table-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,inn_corp/pdf-text-table-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/0i5rWEm9VjgfLRfNb/builds/kS9cco9lNEAUCuB5U/openapi.json
