# PDF & Image to Text / Markdown (with OCR) (`singgendarhan/pdf-image-to-text-markdown-ocr`) Actor

Convert PDFs and images to clean Markdown and text for RAG, AI agents and automations. Keeps headings, lists, tables and page breaks; OCRs scanned pages and images automatically (28 languages). Pay only for pages that produce text.

- **URL**: https://apify.com/singgendarhan/pdf-image-to-text-markdown-ocr.md
- **Developed by:** [Nick Na](https://apify.com/singgendarhan) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.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

## PDF & Image to Text / Markdown (with OCR)

Turn **PDFs and images into clean Markdown and plain text**, ready for RAG pipelines, AI agents and no-code automations. The Actor keeps the document's structure (**headings, bullet and numbered lists, tables, reading order across columns, page breaks**) and reads **scanned pages and images with OCR** automatically, in 28 languages. One dataset row per file, plus downloadable `.md`, `.txt` and `.json` files. **You only pay for pages that produced text.** Blank pages, broken links, password-protected or corrupt files cost nothing.

Built for RAG and LLM ingestion (LangChain, LlamaIndex, vector databases), AI agents through Apify's MCP server, and n8n / Make / Zapier flows.

### What it does

- **PDF text layer first.** Born-digital PDFs are read directly from their text layer (fast and exact). OCR runs **only** on pages that need it: scans, photos of documents, pages whose fonts have no readable text. You can force OCR on every page or turn it off.
- **Images too.** PNG, JPEG, TIFF (including multi-page faxes and scans), WebP, BMP and GIF are OCR'd page by page.
- **Real Markdown, not a text dump.** Headings (`#`, `##`, `###`) from font sizes and bold lines, `-` and `1.` lists from bullets and numbering, Markdown tables from ruled tables and clearly aligned columns, end-of-line hyphenation undone, running headers, footers and page numbers removed.
- **Multi-column layouts** (reports, newsletters, fact sheets) are read column by column instead of across the page.
- **Rotated scans** are detected and turned upright before OCR.
- **Page markers** (`<!-- page 3 -->`) in the Markdown and a form feed between pages in the text, so you can chunk and cite by page. Optional **one row per page** in a second dataset.
- **Metadata per file:** page count, title, author, PDF dates and producer, whether it has a text layer, how many pages were OCR'd, OCR confidence, detected language, word count.
- **Robust bulk processing.** Downloads are retried with backoff, file types are detected from the bytes (not the URL), each document runs in its own process with a time limit, and one bad file never stops the run. Every failure gets a clear `errorCode`.
- **Pay only for results, never more than you allow.** Charges happen after the files are saved. If you set a maximum cost per run, the Actor stops cleanly before going over it.

### Use cases

- **RAG and chatbots:** convert manuals, reports, contracts and policies into Markdown that chunks well (headings and tables intact) before embedding.
- **AI agents (Apify MCP):** let an agent read any PDF or screenshot from a URL and get back Markdown it can reason over.
- **Document automation in n8n / Make / Zapier:** "new invoice in Drive → extract text → send to an LLM or a spreadsheet".
- **Digitizing scans:** archives, faxes, receipts, old reports and typewritten pages become searchable text.
- **Data extraction from statistical PDFs:** government and financial reports with tables come out as Markdown tables.

### How to use

1. Paste one or more **PDF or image URLs** (Google Drive, Dropbox and GitHub share links are converted automatically). Files already in an Apify key-value store work too.
2. Leave **OCR mode** on *Auto* and pick the **OCR languages** of your scans (English by default).
3. Click **Start**. Each file gets a row in the **Output** tab with its title, page counts, language and links to the Markdown, text and JSON files.

#### Input example

```json
{
    "fileUrls": [
        "https://pubs.usgs.gov/fs/2013/3082/pdf/fs2013-3082.pdf",
        "https://images-assets.nasa.gov/image/S69-39334/S69-39334~medium.jpg"
    ],
    "ocrMode": "auto",
    "ocrLanguages": ["eng"],
    "perPageOutput": true
}
```

| Field | Default | What it does |
|---|---|---|
| `fileUrls` | none | Public http(s) links to PDFs or images. Objects like `{"url": "..."}` are accepted too. |
| `ocrMode` | `auto` | `auto` (OCR only where needed), `always` (OCR every page, ignore the text layer), `never` (text layer only) |
| `ocrLanguages` | `["eng"]` | Up to 4 of: eng, deu, fra, spa, ita, por, nld, pol, ces, slk, swe, dan, nor, fin, ron, hun, tur, ell, rus, ukr, ara, hin, vie, ind, chi_sim, chi_tra, jpn, kor. ISO codes (`de`) and names (`German`) work too. |
| `perPageOutput` | `false` | Also write one row per page to the `pages` dataset |
| `keyValueStoreId` / `keyValueStoreKeys` | none | Process PDFs/images stored in an Apify key-value store |
| `detectTables` | `true` | Turn tables into Markdown tables |
| `removeHeadersFooters` | `true` | Drop running headers, footers and page numbers |
| `pageSeparator` | `comment` | `comment` (`<!-- page N -->`), `rule` (`---`) or `none` |
| `maxCharsInDataset` | `200000` | Markdown and text are also included inline in the dataset row up to this length (files always have everything) |
| `maxPagesPerDocument` | `300` | Only the first N pages of each file are processed and billed (max 5,000) |
| `maxFileSizeMb` | `50` | Larger files are skipped with `FILE_TOO_LARGE` (max 300) |
| `documentTimeoutSecs` | `600` | Per-file time limit; finished pages are still returned (status `partial`) |
| `ocrDpi` | `300` | Rendering resolution for OCR (150-400) |
| `pdfPassword` | none | Password for protected PDFs (stored encrypted) |
| `downloadTimeoutSecs` | `30` | Connect/read timeout per download attempt (3 retries) |

#### Output example

One dataset row per file (Markdown shortened here):

```json
{
    "index": 0,
    "source": "https://pubs.usgs.gov/fs/2013/3082/pdf/fs2013-3082.pdf",
    "status": "succeeded",
    "fileType": "pdf",
    "title": "Natural Hazards Science at the U.S. Geological Survey",
    "titleSource": "heading",
    "language": "en",
    "pageCount": 2,
    "pagesProcessed": 2,
    "textLayerPages": 2,
    "ocrPages": 0,
    "emptyPages": 0,
    "hasTextLayer": true,
    "ocrUsed": false,
    "wordCount": 1072,
    "tableCount": 0,
    "headingCount": 6,
    "markdown": "<!-- page 1 -->\n\n# Natural Hazards Science at the U.S. Geological Survey\n\nA sustainable society needs robust assessments of hazard and risk ...\n\n## USGS and Natural Hazards\n\n...",
    "text": "Natural Hazards Science at the U.S. Geological Survey\n\nA sustainable society ...",
    "markdownUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/markdown-0000-fs2013-3082.md",
    "textUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/text-0000-fs2013-3082.txt",
    "jsonUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/json-0000-fs2013-3082.json",
    "metadata": { "producer": "Adobe PDF Library 10.0.1", "creationDate": "2013-09-12T14:28:01-05:00", "pdfVersion": "1.6" },
    "warnings": [],
    "processingMs": 315,
    "chargedEvents": { "page-extracted": 2 },
    "errorCode": null
}
```

A statistics table from a USGS commodity report comes out like this:

```markdown
| Salient Statistics—United States: | 2020 | 2021 | 2022 | 2023 | 2024e |
| --- | --- | --- | --- | --- | --- |
| Production: |  |  |  |  |  |
| Mine, recoverable | 1,200 | 1,230 | 1,230 | 1,130 | 1,100 |
| Primary (from ore) | 872 | 922 | 930 | 843 | 850 |
```

**Files** (in the run's key-value store, linked from each row): `markdown-<index>-<name>.md`, `text-<index>-<name>.txt` and `json-<index>-<name>.json` (metadata plus per-page Markdown, text and table rows). The `OUTPUT` record holds a run summary.

**Per-page rows** (with `perPageOutput`): `docIndex`, `pageNumber`, `method` (`text`, `ocr`, `empty`), `reason` (e.g. `text-layer`, `scanned-page`, `blank-page`), `markdown`, `text`, `ocrConfidence`, `billedAs`.

**Statuses:** `succeeded`, `partial` (some pages missing: timeout, max cost reached or failed pages; see `warnings`), `failed`, `skipped` (run stopped). Failed and skipped files are never charged:

```json
{ "index": 4, "source": "https://example.com/contract.pdf", "status": "failed", "chargedEvents": {},
  "errorCode": "PASSWORD_REQUIRED", "errorMessage": "The PDF is password-protected. Provide the password in pdfPassword to extract it." }
```

**Error codes:** `INVALID_URL`, `URL_NOT_ALLOWED`, `DOWNLOAD_FAILED`, `DOWNLOAD_TIMEOUT`, `FILE_TOO_LARGE`, `EMPTY_FILE`, `UNSUPPORTED_FILE_TYPE` (for example an HTML page or a DOCX), `PASSWORD_REQUIRED`, `WRONG_PASSWORD`, `CORRUPT_FILE`, `IMAGE_TOO_LARGE`, `OCR_DISABLED`, `DOCUMENT_TIMEOUT`, `OUT_OF_MEMORY`, `KVS_RECORD_NOT_FOUND`, `PROCESSING_FAILED`, `RUN_STOPPED`.

### Pricing

Pay-per-event: you pay for pages with text, not for compute time.

| Event | Price | When |
|---|---|---|
| Page extracted | **$0.001 per page** | A PDF page read from its text layer that contains text |
| OCR page | **$0.01 per page** | A scanned page or image read with OCR that contains text |

Each page is billed at most once. **Free:** blank pages, pictures without text, pages beyond your limits, failed or skipped files.

Examples:

- A 20-page digital report: **$0.02**
- A 10-page scanned contract: **$0.10**
- 1,000 receipt photos: **$10**
- A 100-page PDF where 3 pages are scans: 97 × $0.001 + 3 × $0.01 = **$0.127**

Set **Maximum cost per run** in the run options to cap spending. The Actor reserves the cost of each file before processing it, never goes over the limit, and marks what it could not afford as `skipped` (or `partial` for a file cut short).

### Limits and good to know

- **Speed (CPU).** Text-layer pages take about 0.1 s each. OCR takes about 2-3 s per dense page on one CPU (default 2 GB memory = half a CPU, so ~5 s). More memory means more CPUs and pages OCR'd in parallel: 8 GB = 2 CPUs, 16 GB = 4 CPUs (about 4x faster on scanned documents).
- **Tables.** Detected in PDFs from ruling lines and from clearly aligned columns, and in scans from aligned columns. Complex tables (merged cells, nested headers) come out simplified, and tables without visible column gaps come out as text.
- **Existing OCR layers.** Many scanned PDFs already contain a hidden OCR text layer. In `auto` mode that layer is used (cheap). If it is poor, use `ocrMode: "always"` to re-OCR the scan.
- **OCR quality** depends on the scan. Clean 200-300 DPI prints are read with 95%+ word accuracy. Handwriting, very low resolution, decorative fonts and unusual layouts are much less accurate. Bullets that OCR cannot read may merge list items.
- **Very complex layouts** (magazines, posters, scanned pages with line numbers in the gutter) can come out in a slightly mixed reading order.
- **Languages.** OCR uses Tesseract's language models for the 28 languages listed above. PDFs with a text layer work in any language.
- **Limits.** Up to 5,000 pages and 300 MB per file, images up to 150 megapixels, 5,000 files per run.
- **Where your files live.** Results are written only to your run's own storage. Unnamed storages are deleted after your plan's retention period (7 days by default), so download or copy what you need.
- **Public URLs only.** Links to private or local network addresses are blocked. For private files, upload them to an Apify key-value store and pass the keys.

### Use it from code, n8n, Make, Zapier or AI agents

Run it synchronously and get the rows back in one HTTP call (best for a few files; synchronous runs time out after 300 s):

```bash
curl -X POST "https://api.apify.com/v2/acts/<ACTOR_ID>/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"fileUrls": ["https://example.com/report.pdf"], "maxCharsInDataset": 1000000}'
```

Then use `markdown` from each row, or download `markdownUrl`. In **n8n**, **Make** or **Zapier**, use the Apify app's "Run Actor" action with the same JSON and map `markdown`, `text` or `markdownUrl` to the next step. **AI agents** can call the Actor through the Apify MCP server; the input and output schemas describe every field.

### Privacy

Files are downloaded, processed in memory and in temporary files for the duration of the run only, then deleted. The results are written only to your own run's storage; nothing is kept elsewhere or used for anything else. Logs contain file URLs, page counts and timings, never document content. Only public URLs (or your own key-value stores) are fetched. Make sure you have the right to process the documents you submit.

### FAQ

**Do I pay for pages that fail or are blank?** No. Only pages that produced text are billed, and only after the results are saved.

**Why is a page billed as OCR when my PDF "has text"?** In `auto` mode OCR runs only when a page has no usable text layer: a scanned page (maybe with a small stamp or page number in its text layer), or fonts that map to unreadable characters. The per-page dataset (`perPageOutput`) shows the `reason` for every page. Use `ocrMode: "never"` to never pay for OCR.

**Can it read Word, Excel or PowerPoint files?** Not directly. Export them to PDF first. You get a clear `UNSUPPORTED_FILE_TYPE` error instead of a charge.

**Password-protected PDFs?** Pass `pdfPassword`. PDFs with only an owner password (printing or copying restrictions) open without one.

**How do I chunk the Markdown by page?** Split on `<!-- page N -->` in `markdown`, split the text on the form feed character, or turn on `perPageOutput`.

**What OCR engine is it?** [Tesseract 5](https://github.com/tesseract-ocr/tesseract) (Apache-2.0), with its `tessdata_fast` language models, on CPU. It was chosen over a deep-learning OCR after a benchmark: about 6x faster on CPU at the same word accuracy, and it keeps multi-column reading order.

**Something looks wrong?** Open an issue on the Issues tab with the run ID and, if you can, a link to the file. We read every one.

### Credits

- PDF parsing and rendering: [PDFium](https://pdfium.googlesource.com/pdfium/) via [pypdfium2](https://github.com/pypdfium2-team/pypdfium2) (BSD-3-Clause / Apache-2.0). Table grid detection helpers from [pdfplumber](https://github.com/jsvine/pdfplumber) (MIT).
- OCR: [Tesseract](https://github.com/tesseract-ocr/tesseract) and [tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast) (Apache-2.0). Language detection: [py3langid](https://github.com/adbar/py3langid) (BSD-3-Clause).
- Example files: USGS Fact Sheet 2013-3082 (public domain, U.S. Geological Survey) and NASA photo S69-39334 (public domain). Neither agency endorses this Actor.

# Changelog

This Actor's version history is a separate document: https://apify.com/singgendarhan/pdf-image-to-text-markdown-ocr/changelog.md

# Actor input Schema

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

Public http(s) links to PDF files or images (PNG, JPEG, TIFF incl. multi-page, WebP, BMP, GIF), one per line. Google Drive, Dropbox and GitHub 'view' links are converted to direct downloads automatically. Objects like {"url": "..."} are accepted too.

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

Auto (recommended): read the PDF's own text layer and run OCR only on pages without usable text (scans, photos of documents, unreadable fonts) and on images. Always: OCR every page and ignore the text layer (use it for scans whose existing OCR layer is poor). Never: text layer only; pages without one come back empty and images fail.

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

Languages of the scanned text (up to 4). Picking the right language noticeably improves accuracy; every extra language makes OCR slower. Not used for pages that have a text layer.

## `perPageOutput` (type: `boolean`):

Adds a second dataset ('pages') with one row per page: page number, method (text layer or OCR), Markdown and text. Handy for RAG chunking and page citations.

## `keyValueStoreId` (type: `string`):

Optional. ID or name of an Apify key-value store that holds PDFs or images to process (use together with 'Record keys').

## `keyValueStoreKeys` (type: `array`):

Keys of the PDF or image records in the key-value store above, one per line (for example contract-001.pdf). Without a store ID, the run's default store is used.

## `detectTables` (type: `boolean`):

Turn tables in PDFs with a text layer into Markdown tables (ruled grids and clearly aligned columns). Turn off to get tables as plain lines of text.

## `removeHeadersFooters` (type: `boolean`):

Drops text that repeats at the top or bottom of most pages (document titles, confidentiality notes) and bare page numbers. Recommended for RAG and summaries.

## `pageSeparator` (type: `string`):

How page breaks appear in the Markdown. 'Comment' puts an invisible  line before every page (easy to split on); 'Rule' puts a horizontal rule (---) between pages. The .txt output always separates pages with a form feed character.

## `maxCharsInDataset` (type: `integer`):

The full Markdown and text are always saved as files. The dataset row also carries them inline, up to this many characters each (longer documents are cut and flagged with inlineTextTruncated). Use 0 to keep only the file links.

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

Only the first N pages of each document are processed (and billed). Raise it for long documents.

## `maxFileSizeMb` (type: `number`):

Files larger than this are skipped with a FILE_TOO_LARGE error (no charge).

## `documentTimeoutSecs` (type: `integer`):

Processing time limit for one file. When it is hit, the pages finished so far are returned (status 'partial') and only those are billed.

## `ocrDpi` (type: `integer`):

Resolution used to render PDF pages for OCR. 300 is the sweet spot; use 400 for very small print or 200 for speed. Very large pages are rendered at a lower resolution automatically.

## `pdfPassword` (type: `string`):

Optional password for password-protected PDFs (the same password is tried for every PDF in the run). Stored encrypted.

## `downloadTimeoutSecs` (type: `integer`):

Connect / read timeout for each download attempt (a slow but steady download is not cut off). Failed downloads are retried up to 3 times with backoff (honoring Retry-After).

## Actor input object example

```json
{
  "fileUrls": [
    "https://pubs.usgs.gov/fs/2013/3082/pdf/fs2013-3082.pdf",
    "https://example.com/scans/invoice-0042.png"
  ],
  "ocrMode": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "perPageOutput": false,
  "detectTables": true,
  "removeHeadersFooters": true,
  "pageSeparator": "comment",
  "maxCharsInDataset": 200000,
  "maxPagesPerDocument": 300,
  "maxFileSizeMb": 50,
  "documentTimeoutSecs": 600,
  "ocrDpi": 300,
  "downloadTimeoutSecs": 30
}
```

# Actor output Schema

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

Status, title, page counts (text layer vs OCR), language, Markdown, text, file links and error details for every input file.

## `markdownFiles` (type: `string`):

One .md file per document in the run's key-value store.

## `textFiles` (type: `string`):

One .txt file per document.

## `jsonFiles` (type: `string`):

One .json file per document with per-page Markdown, text and tables.

## `pages` (type: `string`):

Filled when 'Also output one row per page' is on.

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

Counts of succeeded, partial, failed and skipped files, pages processed and events charged.

# 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://pubs.usgs.gov/fs/2013/3082/pdf/fs2013-3082.pdf",
        "https://images-assets.nasa.gov/image/S69-39334/S69-39334~medium.jpg"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("singgendarhan/pdf-image-to-text-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://pubs.usgs.gov/fs/2013/3082/pdf/fs2013-3082.pdf",
        "https://images-assets.nasa.gov/image/S69-39334/S69-39334~medium.jpg",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("singgendarhan/pdf-image-to-text-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://pubs.usgs.gov/fs/2013/3082/pdf/fs2013-3082.pdf",
    "https://images-assets.nasa.gov/image/S69-39334/S69-39334~medium.jpg"
  ]
}' |
apify call singgendarhan/pdf-image-to-text-markdown-ocr --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,singgendarhan/pdf-image-to-text-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/Qu6J6Z02CXWNTI7KN/builds/sAkn6Ag9yXxjzkki7/openapi.json
