# PDF OCR to Markdown - Scanned PDF & Image to Text for RAG (`digivibetech/document-to-markdown-ocr`) Actor

Convert PDFs, scanned documents and images to text and clean Markdown for LLMs and RAG. Uses the text layer when present, Tesseract OCR for scans (EN, NL, DE, FR, ES, IT, PT, German Fraktur), per-page confidence. $0.001 per text page, $0.004 per OCR page.

- **URL**: https://apify.com/digivibetech/document-to-markdown-ocr.md
- **Developed by:** [DigiVibeTech](https://apify.com/digivibetech) (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 $4.00 / 1,000 page read with ocrs

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

### What does Document to Markdown OCR do?

It turns **PDFs, scanned documents, images, Word (DOCX, with headings, lists and tables) and PowerPoint (PPTX) files into clean Markdown** that you can feed to an LLM, a RAG pipeline, a search index or a spreadsheet.

- Pages with a real text layer (born-digital PDFs) are extracted directly: fast, exact and cheap.
- Scanned pages and images are read with **Tesseract 5 OCR**, the open-source engine, which runs entirely inside the Actor. Your files are not sent to any third-party AI service.
- Every page gets an **OCR confidence score**. Pages that can't be read (blank, handwriting, very poor scans) are flagged and **not charged**.
- **RAG-ready**: optional `chunks` (paragraph-aware, with page numbers) at no extra cost.
- **German Fraktur** (blackletter) is supported, alongside English, Dutch, German, French, Spanish, Italian and Portuguese.

### Quick start: example input, output and cost

OCR for scanned PDFs and images: text and Markdown for RAG and LLMs, per-page confidence, German Fraktur. $0.001 per text page, $0.004 per OCR page, blank pages free.

**Example input**

```json
{
  "fileUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/3/33/CAB_Accident_Report%2C_National_Airlines_Flight_23.pdf"
  ],
  "pageRange": "1-3"
}
```

**Cost example:** A 50-page scanned PDF x $0.004 = **$0.20**; a 50-page digital PDF x $0.001 = **$0.05**.

### Why use it?

| | This Actor |
|---|---|
| Price per scanned (OCR) page | **$0.004** |
| Price per text-layer page | **$0.001** |
| Start fee per run | **none** |
| Blank, unreadable or failed pages | **free** |
| Accuracy | **published** (see the benchmark below) |
| Per-page confidence | yes |
| Historic German Fraktur | yes |

### How much does it cost?

You pay per successfully extracted page, and only after the result is saved:

| Event | Price | When |
|---|---|---|
| `page-text` | $0.001 | the page has a usable PDF text layer; also each PPTX slide or ~3,000 characters of DOCX text |
| `page-ocr` | $0.004 | the page was read with OCR |

Some examples:

- 100 born-digital PDF pages cost $0.10.
- 100 scanned pages cost $0.40.
- A 300-page scanned book costs $1.20.

Set a **maximum cost per run** and the Actor will never go over it. Pages that don't fit are skipped and explained, not charged. Apify's free plan credit covers about 1,000+ scanned pages a month.

### Input

```json
{
  "fileUrls": ["https://example.com/scan.pdf", "https://example.com/photo-of-page.jpg"],
  "languages": ["nl", "en"],
  "ocrMode": "auto",
  "pageRange": "",
  "maxPagesPerFile": 200
}
```

- **fileUrls**: direct links to PDF, DOCX, PPTX, PNG, JPG, TIFF (including multi-page), WEBP, GIF or BMP files, up to 200 MB each. Share links to public Google Drive and Dropbox files are converted automatically.
- **languages**: up to 3 languages. Choose `de-fraktur` for old German print.
- **ocrMode**: `auto` uses the text layer when present and OCR otherwise; `always` OCRs every page; `never` uses the text layer only.
- **pageRange**: for example `1-5`, `2,4,9` or `10-`.
- **chunkSize** / **chunkOverlap**: set e.g. 1500 / 150 to get a `chunks` array for embeddings. Each chunk lists its source pages.

### Output

You get one dataset item per document:

```json
{
  "sourceUrl": "https://example.com/scan.pdf",
  "status": "ok",
  "fileType": "pdf",
  "pageCount": 12,
  "pagesOcr": 12,
  "pagesLowConfidence": 1,
  "meanOcrConfidence": 0.91,
  "billedPages": {"page-text": 0, "page-ocr": 11},
  "markdown": "<!-- page 1 -->\n\nFirst paragraph ...",
  "pages": [{"page": 1, "method": "ocr", "confidence": 0.93, "markdown": "...", "billedAs": "page-ocr", "lowConfidence": false}],
  "warnings": ["page 7: low OCR confidence 0.41 (...) - not charged"]
}
```

- Each page starts with an invisible `<!-- page N -->` marker, so you can cite or split by page.
- Very large documents (over 2 million characters) store the full Markdown in the run's key-value store, and the dataset item links to it.
- Files that fail are listed in the `ERRORS` key-value record with the reason. They are never charged.

### Accuracy benchmark (real scans, not generated pages)

We measured the Actor on **35 real book and archive pages** from Wikisource. The scans are public domain, and the ground truth is human-proofread ("validated") text under CC BY-SA. The metric is character error rate (CER); lower is better.

| Material | Pages | Median CER | Median word error | Median confidence |
|---|---|---|---|---|
| English, 19th–20th c. print | 14 | 4.8% | 8.1% | 0.92 |
| Dutch, modern print | 5 | 1.8% | 9.0% | 0.94 |
| German, modern print | 4 | 6.3% | 13% | 0.90 |
| German **Fraktur**, `de-fraktur` | 6 | **5.8%** | 28% | 0.86 |
| German Fraktur, plain `de` (for comparison) | 6 | 13.6% | 50% | 0.66 |
| Dutch, 17th–18th c. print | 2 | 13.8% | 40% | 0.81 |
| Handwriting (Kurrent) | 4 | ~71%, **not supported** | – | 0.33 (not charged) |

Some of the remaining error comes from page headers, footnotes and multi-column layouts that the proofread text arranges differently.

### Limitations (honest list)

- **Handwriting is not supported.** It is detected by low confidence and not charged.
- **Layout:** the output is paragraphs in reading order. Tables come out as text lines rather than Markdown tables, and complex multi-column pages may mix columns.
- **No images or formulas:** pictures are skipped, and math is OCR'd as plain text.
- **Speed:** about 1.5–4 seconds per scanned page per vCPU with one language. Every extra language adds time (with 3 languages it is about 6 s per page). Give the Actor more memory (1 vCPU per 4 GB) to process pages in parallel.
- **Downloads:** files must be reachable by a direct link. Servers that block automated downloads (HTTP 403) are reported, not charged.

### Privacy and legal

- Documents are processed inside your Apify run and are not sent to any external API.
- Only process documents you have the right to process.
- It uses Tesseract (Apache-2.0), tessdata_fast models (Apache-2.0) and PDFium via pypdfium2 (Apache-2.0/BSD).

### FAQ

**Does it work with photos of documents?** Yes, as long as they're reasonably sharp and flat. Rotation is detected automatically.

**Can I use it through the API, Make, Zapier or n8n?** Yes. It works like any Apify Actor: call it with the input JSON and read the dataset.

**Found a problem?** Open an issue on the Issues tab. We read every one.

# Actor input Schema

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

Direct links to PDF, DOCX, PPTX, PNG, JPG, TIFF (multi-page), WEBP, GIF or BMP files you have the right to process. Google Drive and Dropbox share links of public files are converted automatically. Up to 200 per run.

## `languages` (type: `array`):

Languages used in the scans (max 3). Choose 'German Fraktur' for old German blackletter print. Only affects OCR pages.

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

Auto is best for most files.

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

Optional, e.g. '1-5', '2,4,9' or '10-'. Empty = all pages.

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

Safety cap per document.

## `includePages` (type: `boolean`):

Adds a 'pages' array with method, confidence and Markdown per page.

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

0 = off. Otherwise adds a 'chunks' array split on paragraph boundaries, each with its page numbers, ready for embeddings. No extra charge.

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

Characters repeated from the end of the previous chunk (max half of the chunk size).

## Actor input object example

```json
{
  "fileUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/3/33/CAB_Accident_Report%2C_National_Airlines_Flight_23.pdf"
  ],
  "languages": [
    "en"
  ],
  "ocrMode": "auto",
  "pageRange": "1-3",
  "maxPagesPerFile": 200,
  "includePages": true,
  "chunkSize": 0,
  "chunkOverlap": 0
}
```

# Actor output Schema

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

Markdown, page details, confidence and billed pages per file

## `errors` (type: `string`):

Files that could not be converted, with the reason

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

Counts of files, pages, billed and free pages

# 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://upload.wikimedia.org/wikipedia/commons/3/33/CAB_Accident_Report%2C_National_Airlines_Flight_23.pdf"
    ],
    "pageRange": "1-3"
};

// Run the Actor and wait for it to finish
const run = await client.actor("digivibetech/document-to-markdown-ocr").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://upload.wikimedia.org/wikipedia/commons/3/33/CAB_Accident_Report%2C_National_Airlines_Flight_23.pdf"],
    "pageRange": "1-3",
}

# Run the Actor and wait for it to finish
run = client.actor("digivibetech/document-to-markdown-ocr").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://upload.wikimedia.org/wikipedia/commons/3/33/CAB_Accident_Report%2C_National_Airlines_Flight_23.pdf"
  ],
  "pageRange": "1-3"
}' |
apify call digivibetech/document-to-markdown-ocr --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,digivibetech/document-to-markdown-ocr"
        }
    }
}
```

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/bBQELIl1c6QeZQOWS/builds/Wu3wZaxP5Uw5dLsD1/openapi.json
