# PDF to AI-Ready Data (Markdown, Tables, RAG Chunks, OCR) (`loris-barce/pdf-to-ai-data`) Actor

Convert PDFs into clean Markdown, JSON/CSV tables and RAG-ready chunks with page metadata. Built-in OCR for scanned PDFs. No AI API calls, no proxies.

- **URL**: https://apify.com/loris-barce/pdf-to-ai-data.md
- **Developed by:** [Loris Barce](https://apify.com/loris-barce) (community)
- **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 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?

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 to AI-Ready Data — Markdown, Tables, RAG Chunks & OCR

Turn any PDF into **clean Markdown**, **tables as JSON/CSV** and **RAG-ready chunks with page metadata** — in seconds, with built-in **OCR for scanned documents**.

Everything runs locally inside the Actor: no AI APIs, no proxies, no browser. You only pay for the pages you actually process.

### Why use it

- 🧱 **Structure preserved** — headings (`#`, `##`) rebuilt from font sizes, bulleted and numbered lists, clean paragraphs.
- 🧹 **Noise removed** — running headers/footers and page numbers repeated on ≥ 50% of pages are stripped; words hyphenated across line breaks are re-joined.
- 📊 **Tables extracted** — fully gridded tables, tables drawn with horizontal rules only (papers, reports) and borderless financial statements made of aligned number columns all become JSON (array of rows) and CSV, and are embedded in the Markdown as GitHub tables.
- ✂️ **Chunks ready for embeddings** — split on paragraph and sentence boundaries with configurable size and overlap, each chunk tagged with `pageStart`, `pageEnd`, `chunkIndex`, `sourceUrl` and `docTitle`.
- 🔍 **OCR for scans** — pages without a text layer are OCR'd automatically (Tesseract, 300 DPI) in English, Italian, German, French and Spanish.
- 🛡️ **Robust** — one broken PDF never stops the batch; every failure is reported with a clear reason. Memory stays flat even on 300+ page documents.

### Use cases

- **RAG chatbots and AI agents over your documents** — manuals, policies, contracts, knowledge bases.
- **Table extraction from reports and invoices** — financial statements, price lists, invoices to CSV/JSON.
- **Digitising scanned archives** — make old scanned PDFs searchable and embeddable.
- **Pre-processing for LLM pipelines** — feed Markdown instead of raw PDF text to get better answers and fewer tokens.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `pdfUrls` | array of strings | – | Direct links to PDF files. |
| `uploadedFiles` | array of strings | – | Uploaded files: a file/record URL, a key in the run's default key-value store, or `storeIdOrName/key`. |
| `outputs` | array | `["markdown"]` | Any of `markdown`, `text`, `tables`, `chunks`. |
| `ocrMode` | string | `auto` | `auto` (OCR only pages without text), `always`, `never`. |
| `ocrLanguages` | array | `["eng","ita"]` | `eng`, `ita`, `deu`, `fra`, `spa`. |
| `chunkSize` | integer | `1000` | Max characters per chunk (overlap included). |
| `chunkOverlap` | integer | `150` | Characters repeated from the previous chunk. Must be < `chunkSize / 2`. |
| `pageRange` | string | all pages | e.g. `1-10,15` or `20-`. |
| `maxPages` | integer | `500` | Cost guard: max pages processed per document. |
| `maxFileSizeMb` | integer | `50` | Larger files are rejected before processing. |

Example:

```json
{
  "pdfUrls": ["https://www.rfc-editor.org/rfc/pdfrfc/rfc2119.txt.pdf"],
  "outputs": ["markdown", "tables", "chunks"],
  "ocrMode": "auto",
  "ocrLanguages": ["eng"],
  "chunkSize": 1000,
  "chunkOverlap": 150
}
```

### Output

#### Dataset (one row per document)

```json
{
  "sourceUrl": "https://example.com/annual-report.pdf",
  "docId": "annual-report-3f2a91bc",
  "title": "Annual Report 2025",
  "pages": 42,
  "totalPages": 42,
  "ocrPages": 0,
  "markdown": "# Annual Report 2025\n\n## Highlights\n\n...",
  "markdownUrl": "https://api.apify.com/v2/key-value-stores/.../records/annual-report-3f2a91bc.md",
  "text": null,
  "textUrl": null,
  "tablesCount": 7,
  "tablesUrl": "https://api.apify.com/v2/key-value-stores/.../records/annual-report-3f2a91bc-tables.json",
  "chunksCount": 118,
  "status": "succeeded",
  "error": null,
  "warnings": []
}
```

- `markdown` / `text` are inlined when ≤ 1 MB; larger documents only get the `…Url` link.
- `status` is `succeeded`, `failed` (with `error`, e.g. `password protected`, `not a PDF (content-type: text/html, missing %PDF header)`, `file too large (…)`, `HTTP 404 while downloading`), `partial` (spending limit reached mid-document — what was processed is saved) or `skipped` (spending limit reached before the document started).

#### `chunks` dataset (one row per chunk)

```json
{
  "text": "## Highlights\n\nRevenue grew 12% year over year, driven by ...",
  "pageStart": 3,
  "pageEnd": 4,
  "chunkIndex": 5,
  "sourceUrl": "https://example.com/annual-report.pdf",
  "docTitle": "Annual Report 2025"
}
```

The chunks dataset belongs to the run; its ID is in `OUTPUT.chunksDatasetId` and it is listed under the run's storages.

#### Key-value store

| Key | Content |
|---|---|
| `{docId}.md` | Markdown |
| `{docId}.txt` | Plain text (if `text` requested) |
| `{docId}-tables.json` | All tables: `[{tableIndex, page, nRows, nCols, rows}]` |
| `{docId}-table-N.csv` | Table N as CSV (N starts at 1) |
| `OUTPUT` | Run summary: `{documents, succeeded, failed, partial, skipped, pagesProcessed, ocrPages, tables, chunks, stoppedBySpendLimit, durationSec}` |

### Connect it to a vector database

1. Run this Actor with `outputs: ["chunks"]`.
2. Add an integration to the run: **Pinecone Integration**, **Qdrant Integration** (or Weaviate, Milvus, Chroma…) from Apify Store.
3. Point the integration at the `chunks` dataset, map `text` as the field to embed and keep `sourceUrl`, `docTitle`, `pageStart`, `pageEnd` as metadata.

Each vector then cites the exact pages it came from — perfect for answers with sources.

The same dataset works with LangChain / LlamaIndex Apify loaders, n8n, Make and Zapier.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| `page-processed` | **$0.001** / page | each page extracted from the text layer |
| `ocr-page` | **$0.005** / page | each page that needed OCR (instead of `page-processed`) |
| `table-extracted` | **$0.002** / table | each table, only when `tables` is in `outputs` |

A 20-page text report with 3 tables costs **$0.026**. Failed downloads, non-PDF files and password-protected files are **never charged**.

When your run reaches its maximum cost, the Actor stops cleanly: pages already paid for are saved (status `partial`), remaining documents are marked `skipped`, and the run still finishes as **SUCCEEDED**.

### Limits

- **Password-protected PDFs are not supported** — they are reported as `failed: password protected`. The Actor never tries to unlock them.
- **OCR quality depends on the scan** — resolution, skew and contrast matter. Handwriting is not supported.
- **Borderless tables are detected when they are numeric** (amounts, years, percentages in aligned columns — e.g. income statements). Borderless tables of pure text with no lines at all are kept as text. Very narrow sub-columns under a spanning header may be merged into one cell.
- Multi-column layouts are read line by line across the page; complex magazine layouts may mix columns.
- Max file size and page count are capped by `maxFileSizeMb` and `maxPages`.
- Downloads from private/internal network addresses are blocked for security.

### FAQ

**Does it send my documents to OpenAI or another AI service?** No. Extraction and OCR run entirely inside the Actor container.

**Can I process files that aren't public?** Yes — upload them to a key-value store (or use the file upload in the input) and list them in `uploadedFiles`.

***

### 🇮🇹 In breve (italiano)

Converte PDF in **Markdown pulito** (titoli, elenchi, paragrafi), **tabelle JSON/CSV** e **chunk pronti per RAG** con numero di pagina, con **OCR automatico** per i PDF scansionati (eng, ita, deu, fra, spa). Nessuna API AI esterna: tutto gira dentro l'Actor.

- **Prezzi:** 0,001 $ a pagina, 0,005 $ a pagina OCR, 0,002 $ a tabella (solo se richiedi `tables`). I file non validi non vengono addebitati.
- **Limite di spesa:** al raggiungimento del limite l'Actor si ferma in modo pulito, salva quanto elaborato e termina in SUCCEEDED.
- **Limiti:** niente PDF protetti da password; la qualità OCR dipende dalla scansione; le tabelle senza linee vengono riconosciute se sono numeriche (es. conti economici), quelle di solo testo restano come testo.
- **Vector DB:** collega il dataset `chunks` alle integrazioni Pinecone/Qdrant di Apify usando `text` come campo da indicizzare.

# Actor input Schema

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

Direct links to PDF files (http/https). The prefilled example is RFC 2119 (3 text pages), used by the daily automated test.

## `uploadedFiles` (type: `array`):

PDFs you uploaded to Apify. Each entry is either the URL of an uploaded file / key-value store record, a record key in this run's default key-value store (e.g. "my-report.pdf"), or "<storeIdOrName>/<key>" for a record in another key-value store you own.

## `outputs` (type: `array`):

What to produce for each document. markdown = clean Markdown with headings; text = plain text; tables = tables as JSON + CSV; chunks = RAG-ready chunks in the "chunks" dataset.

## `ocrMode` (type: `string`):

auto = OCR only pages without a text layer (scans); always = OCR every page; never = never OCR.

## `ocrLanguages` (type: `array`):

Languages used by Tesseract OCR.

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

Maximum length of each RAG chunk, overlap included.

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

How many characters from the end of a chunk are repeated at the start of the next one. Must be smaller than half the chunk size.

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

Only process these pages (1-based), e.g. "1-10,15" or "20-". Leave empty for all pages.

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

Cost protection: at most this many pages are processed per document.

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

Larger files are rejected without being processed.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.rfc-editor.org/rfc/pdfrfc/rfc2119.txt.pdf"
  ],
  "outputs": [
    "markdown",
    "tables",
    "chunks"
  ],
  "ocrMode": "never",
  "ocrLanguages": [
    "eng",
    "ita"
  ],
  "chunkSize": 1000,
  "chunkOverlap": 150,
  "maxPages": 500,
  "maxFileSizeMb": 50
}
```

# Actor output Schema

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

No description

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

No description

## `files` (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.rfc-editor.org/rfc/pdfrfc/rfc2119.txt.pdf"
    ],
    "outputs": [
        "markdown",
        "tables",
        "chunks"
    ],
    "ocrMode": "never"
};

// Run the Actor and wait for it to finish
const run = await client.actor("loris-barce/pdf-to-ai-data").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.rfc-editor.org/rfc/pdfrfc/rfc2119.txt.pdf"],
    "outputs": [
        "markdown",
        "tables",
        "chunks",
    ],
    "ocrMode": "never",
}

# Run the Actor and wait for it to finish
run = client.actor("loris-barce/pdf-to-ai-data").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.rfc-editor.org/rfc/pdfrfc/rfc2119.txt.pdf"
  ],
  "outputs": [
    "markdown",
    "tables",
    "chunks"
  ],
  "ocrMode": "never"
}' |
apify call loris-barce/pdf-to-ai-data --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,loris-barce/pdf-to-ai-data"
        }
    }
}
```

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/k2anpYqjMMJhtAVwT/builds/pYX6ihW7hM5nm0IpC/openapi.json
