# PDF & Document to Markdown – OCR + Tables (`polychrome_haymow/doc-to-markdown-ocr`) Actor

Convert PDFs, DOCX and images to clean Markdown with OCR fallback for scanned pages and table extraction. Per-page output plus a combined document. By NYC Data Works.

- **URL**: https://apify.com/polychrome_haymow/doc-to-markdown-ocr.md
- **Developed by:** [Levani Zurabiani](https://apify.com/polychrome_haymow) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $12.00 / 1,000 ocr pages

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 & Document to Markdown – OCR + Tables

**Categories:** AI, Developer tools

By **NYC Data Works**. Convert PDFs, Word (DOCX) files and images into clean, RAG-ready Markdown.

### What it does

- **Text-layer PDFs:** extracts text page by page (pdfplumber).
- **Scanned pages:** automatic OCR fallback with Tesseract (open source, runs inside the container, no external API).
- **Tables:** detected tables are rendered as Markdown tables and removed from the running text to avoid duplication.
- **DOCX:** headings, list items and tables converted to Markdown.
- **Images (PNG/JPG/TIFF, multi-page TIFF):** OCR to text.
- **Output:** one dataset item per page (optional) + one combined `document` item per file; the full combined Markdown is also saved in the key-value store as `doc-N.md`.

### Input

| Field | Description |
|---|---|
| `sources` | URLs of files you have the right to process |
| `ocrMode` | `auto` (OCR only pages with almost no text), `always`, `never` |
| `ocrMinChars` | auto-OCR threshold, default 10 |
| `extractTables` | default true |
| `maxPages` | per-document cap (default 200, max 2,000) |
| `perPageOutput` | push per-page items (default true) |

### Output example

```json
{"url":"https://.../file.pdf","type":"page","fileType":"pdf","page":1,"markdown":"# Title...\n\n| a | b |\n|---|---|","ocr":false,"tables":1,"chars":1834}
```

### FAQ

**Which languages does OCR support?** English (`eng`) only in this build. More languages can be added on request.
**How accurate is the table extraction?** Good for ruled/grid tables in digital PDFs. Tables in scanned pages come out as plain OCR text, not Markdown tables. Borderless tables may be merged into text.
**Is my file sent anywhere?** No. All processing happens inside the Apify run; no third-party APIs.
**Max file size?** 100 MB per file (checked before download); use `maxPages` to limit cost on big PDFs.
**Is DOC/PPTX/XLSX supported?** Not yet; PDF, DOCX and images only.
**How fast is OCR?** Roughly 1–3 s per page at 1 GB memory.
**Can I upload a local file?** Upload it to an Apify key-value store (or any public URL) and paste the URL.

### Limits

- Scanned-page OCR quality depends on scan resolution; handwriting is not supported.
- Complex multi-column layouts may read out of order.
- Encrypted/password-protected PDFs fail with an error item.

### Safety limits

- Max 100 sources per run; `maxPages` up to 2,000 per document (also applied to multi-page TIFF frames).
- 100 MB per file (checked before download). DOCX files are checked for zip bombs (unpacked size ≤ 200 MB, compression-ratio limit) before parsing.
- Rendered OCR pages are capped at 40 megapixels; Pillow's pixel limit is set to 50 MP.
- OCR language is fixed to `eng` (the only installed language). Auto-OCR threshold default: 10 characters, so pages with a text layer are billed as `text-page`, not `ocr-page`.
- OCR time limit: 60 s per page (Tesseract is stopped and the page counts as failed). Default run timeout: 3,600 s.
- **Page errors:** if a page fails (OCR timeout, oversized TIFF frame, corrupt page), processing of that document stops at that page; the pages already processed (and paid for) are still saved in `doc-N.md` and the document item, with an `error` note. Such a partial document is **not** charged `document-processed`.

### Security

- Only public `http(s)` URLs are fetched. Every URL and every redirect hop (max 5) is resolved and rejected if it points to a private, loopback, link-local (incl. the `169.254.169.254` cloud-metadata address), multicast or reserved address; other schemes (`file:`, `ftp:` …) are refused. The connection is pinned to the checked IP.
- Size limits are checked from `Content-Length` before downloading and enforced again while streaming, with an overall per-file deadline.
- Query strings and credentials in URLs are stripped from logs, dataset items and `SUMMARY`.

### Spending limit

Events are charged as work proceeds; each page is charged before it is delivered. When your **Max cost per run** is reached the actor stops cleanly and keeps everything already delivered; nothing beyond the limit is charged. `document-processed` is charged only for a document with at least one delivered page: if the limit stops the run before a document's first page, that document gets an error item and is **not** charged.

### Data retention & consent

- **Retention:** results are stored only in this run's own default dataset and key-value store, and are kept under Apify's standard storage retention for your plan (unnamed run storages are deleted automatically after the retention period). Nothing is copied elsewhere; no third-party API is called.
- **Your responsibility:** you must have the rights to the content you submit and, where it contains personal data, a lawful basis and any required consent (e.g. under GDPR/CCPA). Do not submit data you are not allowed to process.

### Credits & licences

- [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) – Apache-2.0 (via pytesseract – Apache-2.0)
- [pdfplumber](https://github.com/jsvine/pdfplumber) – MIT (uses pdfminer.six – MIT)
- [pypdfium2](https://github.com/pypdfium2-team/pypdfium2) – Apache-2.0 / BSD-3-Clause (PDFium – Apache-2.0/BSD)
- [Pillow](https://python-pillow.org/) – MIT-CMU (HPND)
- [python-docx](https://github.com/python-openxml/python-docx) – MIT
- [httpx](https://www.python-httpx.org/) – BSD-3-Clause; [Apify SDK for Python](https://github.com/apify/apify-sdk-python) – Apache-2.0

### Pricing

Pay-per-event – you pay only for what you process:

| Event | Price |
|---|---|
| Actor start (`actor-start`) | $0.004 per run |
| Document processed (`document-processed`) | $0.004 per file with at least one delivered page |
| OCR page (`ocr-page`) | $0.012 per OCR'd page |
| Text page (`text-page`) | $0.0005 per non-OCR page (text-layer PDF page or DOCX) |

Example: one scanned 3-page PDF costs $0.004 + $0.004 + 3 × $0.012 = $0.044. A 10-page digital PDF (no OCR) costs $0.004 + $0.004 + 10 × $0.0005 = $0.013.

# Actor input Schema

## `sources` (type: `array`):

Public URLs of PDF, DOCX or image (PNG/JPG/TIFF) files you have rights to process. Upload files via the Apify file input and paste the URL here. Max 100 per run. Only public http(s) URLs; private/internal addresses are rejected.

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

Auto runs Tesseract only on pages whose text layer is shorter than the threshold.

## `ocrMinChars` (type: `integer`):

Pages with fewer extracted characters are OCR'd in auto mode.

## `ocrLanguage` (type: `string`):

Tesseract language. Only English is installed in this build.

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

Detect tables in text-layer PDFs and render them as Markdown tables.

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

Hard cap per document to control cost (max 2000).

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

If off, only one combined item per document is pushed.

## Actor input object example

```json
{
  "sources": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ],
  "ocrMode": "auto",
  "ocrMinChars": 10,
  "ocrLanguage": "eng",
  "extractTables": true,
  "maxPages": 200,
  "perPageOutput": true
}
```

# Actor output Schema

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

Per-page items (when perPageOutput is on) and one document item per file.

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

First document's combined Markdown. Further files are doc-2.md, doc-3.md, …

# 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 = {
    "sources": [
        {
            "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("polychrome_haymow/doc-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 = { "sources": [{ "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" }] }

# Run the Actor and wait for it to finish
run = client.actor("polychrome_haymow/doc-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 '{
  "sources": [
    {
      "url": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    }
  ]
}' |
apify call polychrome_haymow/doc-to-markdown-ocr --silent --output-dataset

```

## MCP server setup

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