# PDF Text Extractor: Markdown, Tables & OCR (`private_image_hg9/pdf-text-table-extractor`) Actor

Turn PDF files at public URLs into clean text or Markdown with headings, lists and tables, plus metadata. Scanned pages are read with OCR. Pay per page that gave text, with a lower price for text-layer pages and nothing for files that fail.

- **URL**: https://apify.com/private_image_hg9/pdf-text-table-extractor.md
- **Developed by:** [Michael Shanker](https://apify.com/private_image_hg9) (community)
- **Categories:** Developer tools, AI
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 page extracteds

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PDF Text Extractor: Markdown, Tables & OCR

Give it the addresses of PDF files and get back their content as **clean Markdown or plain text, with tables as rows and cells, and the file's metadata**. Scanned pages are read with **OCR**. One row per file, or one row per page if you want to feed the text to an AI or search tool in chunks.

**You pay per page that gave text.** Pages read from the PDF's own text layer cost less than pages that need OCR. Files that can't be downloaded, aren't PDFs, are password protected or damaged, and blank pages are saved with the reason and cost nothing.

### What it does

- **Markdown that keeps the structure.** Headings come from the document's font sizes (`#`, `##`, `###`), wrapped lines are joined into paragraphs, words split by a hyphen at the end of a line are put back together, bullet points become Markdown lists, and tables become Markdown tables in the right place. Two-column pages (papers, reports, newsletters) are read one column at a time.
- **Tables you can use.** Ruled tables (tables drawn with lines) come back in the `tables` field as rows of cells with their page number, ready for a spreadsheet.
- **OCR only where it's needed.** In the default `auto` mode, a page is read with OCR only when it's a picture with no usable text layer, so a mostly digital PDF with a few scanned pages costs mostly the text-layer price. `always` reads every page with OCR, for PDFs whose text layer is broken. OCR uses Tesseract with English, German, French, Spanish, Italian, Portuguese and Dutch.
- **Metadata.** Title, author, subject, creator, producer, creation and modification dates (as ISO dates), page count and file size.
- **Page ranges and caps.** Read only pages like `1-5,9` or `10-`, and at most N pages per file.
- **Plain text too.** `text` is the PDF's text layer in reading order, for search indexes or your own parsing. Choose `both` to get text and Markdown in one run.

Good for: building RAG and AI search indexes from PDF reports and papers, pulling tables out of government and financial reports, archiving forms and filings as text, and checking what a batch of PDFs contains.

### How it compares

Most PDF tools in the Store charge either about $1 per 1,000 pages or a flat price per file (about $5 per 1,000 PDFs). Here is what the same work costs:

| Job | This Actor | Per-page tools (~$1 / 1,000 pages) | Per-file tools (~$5 / 1,000 PDFs) |
|---|---|---|---|
| 1,000 one-page digital PDFs | $1 | about $1 | about $5 |
| 1,000 digital PDFs of 20 pages | $20 | about $20 | about $5 |
| 1,000 scanned PDFs of 20 pages | $80 (OCR) | depends on whether they offer OCR | depends on whether they offer OCR |
| Files that fail, blank pages | free | check the tool's pricing | check the tool's pricing |

So for long digital documents a per-file tool can be cheaper; for short files, mixed batches with failures, and when you want Markdown with tables and OCR that's only paid for where it's used, this one is. Try a few of your own files first: output quality differs most on tables, columns and scans.

### Input

| Field | What it does |
|---|---|
| **PDF addresses** (`fileUrls`) | Public `http(s)` addresses of PDF files, one per line. Request-list style objects (`{"url": "..."}`) work too. Redirects are followed. A PDF sent with the wrong content type is still read; a web page is not. |
| **Output format** (`outputFormat`) | `markdown` (default), `text` or `both`. |
| **Extract tables** (`extractTables`) | Return ruled tables as cells and Markdown tables (default on). |
| **OCR for scanned pages** (`ocr`) | `auto` (default): only pages with no usable text layer. `always`: every page. `off`: never. |
| **OCR languages** (`ocrLanguages`) | Tesseract codes joined with `+`: `eng`, `deu`, `fra`, `spa`, `ita`, `por`, `nld`. Default `eng`. |
| **Pages to read** (`pageRange`) | Optional, for every file: `1-5,9` or `10-` (to the end). |
| **Max pages per file** (`maxPagesPerFile`) | Default 500, up to 5,000. Pages past the cap are not read or charged. |
| **One row per page** (`splitPages`) | One row for each page instead of each file. |
| **Files at once** (`maxConcurrency`) | Files downloaded and read in parallel (default 4). |

Example input:

```json
{
  "fileUrls": [
    "https://www.rfc-editor.org/rfc/rfc9309.pdf",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "outputFormat": "markdown",
  "extractTables": true,
  "ocr": "auto",
  "pageRange": "1-10"
}
```

### Output

One row per file (values shortened):

```json
{
  "url": "https://files.example.org/quarterly-water-report.pdf",
  "inputUrl": "https://files.example.org/quarterly-water-report.pdf",
  "status": "ok",
  "error": null,
  "note": null,
  "fileName": "quarterly-water-report.pdf",
  "bytes": 3780,
  "pageCount": 3,
  "pagesExtracted": 3,
  "pagesOcr": 0,
  "charCount": 1795,
  "title": "Quarterly Water Report",
  "author": "Harbor City Utilities",
  "subject": "Water quality, first quarter",
  "creator": "Report Builder 2.1",
  "producer": "ReportLab PDF Library",
  "creationDate": "2025-03-14T09:30:00+00:00",
  "modDate": "2025-03-14T09:30:00+00:00",
  "markdown": "<!-- page 1 -->\n\n# Quarterly Water Report\n\n## Summary of findings\n\nHarbor City delivered safe drinking water to every district this quarter...\n\n- Chlorine residual held steady across the north reservoir.\n- Two main breaks were repaired within the target window.\n\n<!-- page 2 -->\n\n## Sampling results by district\n\n| District | Samples | Lead (ppb) | Result |\n|---|---|---|---|\n| North | 48 | 2.1 | Pass |\n...",
  "tables": [
    {"page": 2, "rows": [["District", "Samples", "Lead (ppb)", "Result"], ["North", "48", "2.1", "Pass"], ["Harbor", "52", "3.4", "Pass"]]}
  ],
  "extractedAt": "2026-11-03T18:02:11.000000+00:00"
}
```

- `text` and/or `markdown` hold the pages that gave text, separated by a blank line. In Markdown, each page starts with an invisible `<!-- page N -->` marker when there is more than one.
- `pagesExtracted` counts pages that gave text and `pagesOcr` how many of those were read with OCR. These are the pages you pay for.
- `note` says when not every page was read and why (page cap, page range, blank pages, scanned pages with OCR off, spending cap).
- With **One row per page**, each row has `page` (its number) and `source` (`text` for the text layer, `ocr`, or empty for a blank page) instead of the page counts.

`status` is one of:

- `ok`: at least one page gave text. The only status that costs anything.
- `no_text`: the PDF opened but no page gave text (blank pages, or scanned pages with OCR off).
- `download_failed`: the server answered with an error, timed out, couldn't be reached, or the file is over 100 MB.
- `not_pdf`: the address returned a web page or another kind of file.
- `encrypted`: the PDF needs a password to open.
- `invalid_pdf`: the file is damaged and can't be read.
- `blocked_by_robots`: the website's robots.txt asks tools like this one not to download that address.
- `invalid_url`: not a usable public `http(s)` address.

Download results as JSON, CSV or Excel, or send them to Google Sheets, Zapier, Make or n8n. The run also saves a `SUMMARY` record in the key-value store with counts per status, pages extracted, charges and whether the spending cap stopped the run.

### Pricing

Pay per event:

- **Page extracted: $0.001** ($1 per 1,000 pages) for each page whose text came from the PDF's text layer.
- **Page read with OCR: $0.004** ($4 per 1,000 pages) for each scanned page read with OCR.

No start fee, no charge per file, nothing for failed files or blank pages. A 20-page digital report costs $0.02; the same report as a scan costs $0.08. Set a **maximum charge per run** in the run options: before each file is read, its pages are priced against what's left, a file that would go over is read up to the limit (the row's `note` says so), and the run stops cleanly.

### Good to know

- **Files up to 100 MB**, downloaded straight to disk. A file stops being read after 25 minutes; the row says how far it got.
- **Tables** are found where they are drawn with lines. Tables made only of aligned text (no lines) come through as text in reading order, not in `tables`. Tables on OCR pages aren't detected.
- **OCR quality** depends on the scan. Clean scans at 200 DPI or more read well; handwriting, very small print and photos of pages read poorly. Pages are rendered at 300 DPI for OCR. Rotated scans should be straightened first.
- **Very long documents:** if a file's row would be larger than about 8 MB, the full text and Markdown are saved in the run's key-value store, and the row keeps the first million characters with `textFileUrl` / `markdownFileUrl` links. Use **One row per page** to avoid this.
- **Password-protected PDFs** can't be read. PDFs that only restrict printing or copying (no password to open) are read normally.
- Only public addresses are downloaded: local and internal network addresses are refused, and so are links with a user name or password in them.

### Responsible use

- It downloads only the addresses you give it, names itself in its User-Agent (`PdfTextExtractorBot`), makes one request at a time to each website with at least a second between requests, and **respects robots.txt**.
- You are responsible for having the right to download and use the documents you process, and for any personal data in them. Follow copyright and the privacy laws that apply to you.

This Actor isn't affiliated with or endorsed by any of the websites it downloads from.

### More tools from the same developer

- [Image to Text OCR: Read Text from Images & Screenshots](https://apify.com/private_image_hg9/image-to-text-ocr)
- [Website Screenshot & PDF: Full Page, Mobile, Batch URLs](https://apify.com/private_image_hg9/website-screenshot-pdf)

# Actor input Schema

## `fileUrls` (type: `array`):

One public http(s) address of a PDF file per line. Each file gets one row (or one row per page with "One row per page"). Repeats are read once. Addresses that answer with a PDF under another content type, or after redirects, are fine.

## `outputFormat` (type: `string`):

Markdown keeps headings, lists and tables (good for AI and search tools). Text is the plain text layer in reading order. Both gives you both fields.

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

Find ruled tables (tables drawn with lines) and return their cells in the tables field, and as Markdown tables in the Markdown. Turn off for speed if you only need the text.

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

Auto reads with OCR only pages that are a picture with no usable text layer. Always reads every page with OCR (for PDFs whose text layer is broken). Off never uses OCR. OCR pages cost more than text-layer pages.

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

Languages of the scanned text, as Tesseract codes joined with +: eng (English), deu (German), fra (French), spa (Spanish), ita (Italian), por (Portuguese), nld (Dutch). For example deu+eng.

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

Optional. Page numbers and ranges for every file, like 1-5,9 or 10- (to the end). Leave empty to read every page.

## `maxPagesPerFile` (type: `integer`):

The most pages read from one file (after the page range). Pages past this are not read or charged.

## `splitPages` (type: `boolean`):

Save one row for each page instead of one row for each file. Handy for chunking documents for AI and search.

## `maxConcurrency` (type: `integer`):

How many files are downloaded and read in parallel. Requests to the same website still go one at a time.

## Actor input object example

```json
{
  "fileUrls": [
    "https://www.rfc-editor.org/rfc/rfc9309.pdf"
  ],
  "outputFormat": "markdown",
  "extractTables": true,
  "ocr": "auto",
  "ocrLanguages": "eng",
  "maxPagesPerFile": 500,
  "splitPages": false,
  "maxConcurrency": 4
}
```

# Actor output Schema

## `results` (type: `string`):

One row per file (or per page): text and/or Markdown, tables, metadata, or why the file could not be read.

## `summary` (type: `string`):

Counts for the run: files read by status, pages extracted and read with OCR, charges, and whether the spending cap was reached.

# 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 = {
    "fileUrls": [
        "https://www.rfc-editor.org/rfc/rfc9309.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("private_image_hg9/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 = { "fileUrls": ["https://www.rfc-editor.org/rfc/rfc9309.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("private_image_hg9/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 '{
  "fileUrls": [
    "https://www.rfc-editor.org/rfc/rfc9309.pdf"
  ]
}' |
apify call private_image_hg9/pdf-text-table-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,private_image_hg9/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/qXaLG34B0K4p0SMts/builds/b8q4k7MMaixZKlNC6/openapi.json
