# PDF Text Extractor - Markdown, Tables & RAG Chunks (`readable_slash/pdf-text-extractor-structured`) Actor

Extract text from PDF to clean Markdown with real tables, headings and lists. Bulk PDF to text conversion for RAG, LLM and vector database ingestion, with optional context-aware chunking. Detects scanned PDFs, handles encrypted files and broken URLs. Pay only for documents extracted.

- **URL**: https://apify.com/readable\_slash/pdf-text-extractor-structured.md
- **Developed by:** [HJL Analytics and AI development](https://apify.com/readable_slash) (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.24 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 - Markdown, Tables & RAG Chunks

Convert PDFs into clean, structured Markdown that a language model can
actually use. Headings, tables and lists survive the conversion. Optional
retrieval-ready chunks come with their heading breadcrumb attached.

Built for **RAG pipelines, vector databases and LLM ingestion**, where the
quality of your retrieval is capped by the quality of your text extraction.

***

### Why most PDF extractors are not good enough for RAG

Most tools call `extract_text()` and hand you the result. That produces three
problems you only notice after your retrieval quality is already bad:

| Problem | What it does to your pipeline |
|---|---|
| Tables flattened into prose | `Region North 1200 48000 South 900 36000` embeds as numeric noise. The relationship between a number and its column header is destroyed. |
| No heading structure | A chunk reading *"The limit is 40 hours."* is unretrievable. Which limit? Which policy? The section title held that context and it was thrown away. |
| Running headers on every page | *"ACME Holdings Confidential — Page 4 of 60"* repeated into 60 chunks, diluting every embedding. |
| Hard line wrapping kept | Sentences arrive broken mid-clause, so chunk boundaries land in the middle of ideas. |

This Actor addresses each of them.

***

### What it does

#### Real Markdown structure

Headings become `#`/`##`/`###` by comparing font sizes to the document's body
size — not by guessing from capitalisation. Lists become `-` items.
Paragraphs are rebuilt from wrapped lines, with hyphenation rejoined.

#### Tables that stay tables

Tables are detected, rendered as Markdown tables, and — crucially — **their
text is removed from the prose flow**, so table content is never emitted
twice.

Two detection paths run:

- **Ruled tables** — standard grids with visible borders.
- **Borderless "booktabs" tables** — the academic and analyst-report style
  with horizontal rules only and no vertical lines. Most extractors miss
  these entirely and flatten them into prose. Columns are recovered from the
  vertical whitespace corridors a human reader's eye uses.

Spurious grids are rejected: a bordered callout box is not a one-column
table, and a dense figure is not a 59-column one. When a grid is rejected its
text is still returned as prose — content is never silently dropped.

#### RAG chunking with context

Enable `chunkForRag` and each chunk arrives with the heading trail that
locates it:

```json
{
  "index": 12,
  "breadcrumb": "Employee Handbook > Leave Policy > Parental Leave",
  "headings": ["Employee Handbook", "Leave Policy", "Parental Leave"],
  "text": "Employees are entitled to 16 weeks...",
  "page": 7
}
```

- Chunks **never cross a heading**, so one chunk never mixes two sections.
- Each carries its breadcrumb, so it stays meaningful once embedded.
- Paragraphs and tables are never split mid-way.
- Overlap keeps facts that straddle a boundary retrievable, and is never
  carried across a section break.

#### Clean-up you would otherwise write yourself

- Running headers and footers detected and stripped (page numbers normalised,
  so *"Page 1 of 12"* and *"Page 7 of 12"* are recognised as the same
  furniture).
- Rotated margin text — arXiv stamps, spine labels, watermarks — excluded
  rather than dropped mid-sentence into your abstract.
- Multi-column pages read column by column, not straight across the gutter.
- Word spacing repaired on tightly-kerned PDFs that otherwise return
  `Providedproperattributionisprovided`.

#### Built for batches

One row per document. A password-protected, corrupt, oversized or 404'd file
returns a row with `status: "error"` and a reason — **it never aborts the
run**. Scanned PDFs are flagged with `isScanned: true` and a warning rather
than silently returning nothing.

***

### How to use it

1. Paste one or more **direct PDF URLs** into **PDF URLs**. Anything you can
   link to works — a file on your own server, an S3 or Drive direct link, a
   published report.
2. Choose an **output format**: Markdown keeps headings, lists and tables;
   plain text strips all markup; Both returns each in its own field.
3. Turn on **RAG chunks** if the text is going into a vector database. Each
   chunk carries the heading breadcrumb it sits under, so an embedding keeps
   its context.
4. **Start** the run. Results appear as one dataset row per document.

Every run is a normal Apify run, so you can schedule it, trigger it from the
**API**, or wire it into **Make**, **Zapier**, **LangChain** or **LlamaIndex**.

### Pricing

**You are charged only for documents that extract successfully** — a small
per-document fee plus a per-page fee. Failed downloads, encrypted files,
corrupt PDFs and 404s are **not charged**. A run that extracts nothing costs
nothing.

Pricing per page rather than a flat fee per file means a two-page invoice
costs a fraction of a 300-page annual report, instead of subsidising it.

***

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `pdfUrls` | array | — | **Required.** Direct links to the PDFs. |
| `outputFormat` | string | `markdown` | `markdown`, `text`, or `both`. |
| `extractTables` | boolean | `true` | Detect tables and render them as Markdown. |
| `chunkForRag` | boolean | `false` | Also return retrieval-ready chunks. |
| `chunkSize` | integer | `1500` | Soft character ceiling per chunk. |
| `chunkOverlap` | integer | `150` | Characters repeated between chunks. |
| `includeMetadata` | boolean | `true` | Return title, author, dates, producer. |
| `maxPagesPerDocument` | integer | `0` | Stop after N pages. `0` = no limit. |
| `password` | string | — | Password for encrypted PDFs. |

#### Example

```json
{
  "pdfUrls": [
    "https://example.com/annual-report.pdf",
    "https://example.com/policy.pdf"
  ],
  "outputFormat": "markdown",
  "extractTables": true,
  "chunkForRag": true,
  "chunkSize": 1200
}
```

### Output

One dataset item per document:

```json
{
  "url": "https://example.com/annual-report.pdf",
  "filename": "annual-report.pdf",
  "status": "ok",
  "error": null,
  "pageCount": 15,
  "tableCount": 4,
  "isScanned": false,
  "markdown": "# Annual Report\n\n## Executive Summary\n\n...",
  "metadata": { "Title": "Annual Report 2026", "Author": "ACME" },
  "chunks": [ { "index": 0, "breadcrumb": "...", "text": "...", "page": 1 } ],
  "chunkCount": 37
}
```

Failed documents:

```json
{
  "url": "https://example.com/missing.pdf",
  "filename": "missing.pdf",
  "status": "error",
  "error": "http 404"
}
```

***

### Limitations — stated up front

- **Scanned PDFs are not OCR'd.** If a page has no text layer there is
  nothing to extract. Those documents return `isScanned: true` and a warning
  so you can route them elsewhere, rather than silently returning nothing.
- **Dense interactive forms** (tax forms, application forms) extract far less
  cleanly than reports, papers and manuals. Their content is laid out as
  positioned fields rather than as a reading flow.
- **Multi-row table headers** are returned as separate rows rather than being
  merged into one header.
- Text is extracted in the document's own language; nothing is translated.

### Common uses

**PDF for LLM** and RAG pipelines: loading documentation, reports, contracts,
research papers, manuals and policies into a **vector database** as
**PDF RAG chunks**, each carrying the heading it sits under so a retrieved
chunk keeps its context. Building a **RAG knowledge base** from a document set.

Also common: **multi page PDF extraction** across long manuals and reports;
**contract PDF extraction** and **form PDF extraction**, where field labels and
table cells have to survive; pulling **invoice PDF text** and line items as
structured rows; extracting **tables from PDFs**; bulk **PDF to Markdown**
conversion for static sites and wikis; and **PDF to clean text** when you want
no markup at all.

**Encrypted PDF** files open with a supplied `password`. When one cannot be
read it is reported with the reason and charged nothing.

### Integrations

Standard Apify output: pull results via the **API**, export to
**JSON/CSV/Excel**, schedule runs, or connect to **Make**, **Zapier**,
**LangChain**, **LlamaIndex** and other Apify integrations.

### FAQ

**Is this legal?**
Yes. You supply documents you already have the right to use, and the Actor
extracts them. No third-party website is accessed, no terms of service are
involved, and no proxies are used.

**How much will a run cost?**
You are charged **per page successfully extracted**. A 40-page report costs
**$0.016**; a 300-page manual costs **$0.12**. Pricing is per page rather than
per file on purpose — extraction cost scales with pages, so a flat per-file
price would either overcharge short documents or lose money on long ones.

**What happens if one document in my batch fails?**
Only that document. It appears in the dataset with `status: "error"` and the
reason, the rest of the batch continues, and **you are not charged for it**.

**It returned nothing for my PDF. Why?**
Check `isScanned` and the `warning` field. A scanned PDF is an image with no
text layer, so there is nothing to extract without OCR. The Actor detects this
and says so rather than silently returning an empty string.

**How is this different from a plain text extractor?**
A naive extractor returns a wall of text: no headings, no tables, and words
welded together across line breaks. On a real research paper this Actor
recovers **16 headings and 4 tables** where the naive baseline finds none, and
removes several hundred welded words. Tables come out as real Markdown tables,
not as scattered numbers.

**Can I get the data out programmatically?**
Yes — the standard Apify dataset API, plus JSON, CSV and Excel export. Every
output field is described in the dataset schema, so tools and AI agents can
read the structure without guessing.

### Other Actors by this author

| Actor | What it does |
|---|---|
| [Audio & Video Transcriber — Speech to Text, SRT & Timestamps](https://apify.com/readable_slash/audio-video-transcriber) | The same idea for recordings: Whisper speech-to-text on your own audio and video, with timestamps, SRT/VTT subtitles and the same RAG chunking. Pairs naturally with this Actor when a knowledge base mixes documents and recordings. |

# Actor input Schema

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

Direct links to the PDF files you want to convert. Each one becomes a row in the dataset.

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

Markdown keeps headings, lists and tables. Plain text strips all markup. Both returns each in its own field.

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

Detect tables and render them as Markdown tables, including academic tables that have horizontal rules only. Turn off to read every page as plain prose.

## `chunkForRag` (type: `boolean`):

Also return retrieval-ready chunks. Chunks never cross a heading and each one carries its heading breadcrumb, so it stays meaningful on its own after embedding.

## `chunkSize` (type: `integer`):

Soft ceiling per chunk. Paragraphs and tables are never split mid-way.

## `chunkOverlap` (type: `integer`):

Text repeated from the end of the previous chunk, so a fact split across a boundary stays retrievable. Never carried across a heading.

## `includeMetadata` (type: `boolean`):

Return the document's title, author, creation date and producer where present.

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

Stop after this many pages. 0 means no limit. Useful for sampling large documents cheaply.

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

Password for encrypted PDFs. Applied to every document in the run.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://example.com/report.pdf"
  ],
  "outputFormat": "both",
  "extractTables": true,
  "chunkForRag": true,
  "chunkSize": 1500,
  "chunkOverlap": 150,
  "includeMetadata": true,
  "maxPagesPerDocument": 0
}
```

# Actor output Schema

## `documents` (type: `string`):

One row per PDF: Markdown, tables, metadata and any RAG chunks. Failed documents appear with status "error" and a reason.

## `markdown` (type: `string`):

Page count, table count and the extracted Markdown for each document.

## `plainText` (type: `string`):

The extracted text with all markup stripped.

## `ragChunks` (type: `string`):

Embedding-ready chunks carrying their heading breadcrumbs, when RAG chunking is enabled.

## `runDetail` (type: `string`):

This run in Apify Console.

# 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://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ],
    "outputFormat": "both",
    "chunkForRag": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("readable_slash/pdf-text-extractor-structured").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://arxiv.org/pdf/1706.03762",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    ],
    "outputFormat": "both",
    "chunkForRag": True,
}

# Run the Actor and wait for it to finish
run = client.actor("readable_slash/pdf-text-extractor-structured").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://arxiv.org/pdf/1706.03762",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "outputFormat": "both",
  "chunkForRag": true
}' |
apify call readable_slash/pdf-text-extractor-structured --silent --output-dataset

```

## MCP server setup

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

```

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/MBgQDxxChF56pGs2O/builds/kjJcuCZffO1dHJHb2/openapi.json
