# PDF & Image to Text: Bulk, Markdown, Tables, OCR (`charlesmarkette/pdf-image-to-text-markdown-ocr`) Actor

Turn PDFs and images into Markdown or text in bulk: headings, lists, PDF metadata and ruled tables (borderless tables stay as text). Scanned pages and images are read with OCR. Paste URLs or enrich any dataset of document links. One row per document or per page.

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

## Pricing

from $3.50 / 1,000 processed documents

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/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** turns PDFs and images into clean **Markdown** or **plain text**, in bulk. Give it a list of file URLs, or point it at any Apify dataset of document links, and each document becomes one row with its **text**, its **tables** (as rows of cells), the PDF's **title, author and creation date**, and which pages needed **OCR**. Scanned pages and images (PNG, JPEG, TIFF, WEBP, BMP, GIF) are read with **Tesseract OCR** in English, German, French, Spanish, Italian, Portuguese and Dutch.

### Why this Actor

- **Markdown for LLMs and search.** Headings come from font sizes (`#`, `##`, `###`), bullet and numbered lists become Markdown lists, wrapped lines are joined into paragraphs (`exam-` + `ple` = `example`), and tables drawn with ruling lines become Markdown tables in the right place in the text (borderless tables come out as aligned text lines).
- **Reading order that follows the page.** Two-column layouts are read column by column, not line by line across both columns, with full-width titles and footers kept in place.
- **OCR only where it's needed.** In the default *auto* mode, pages with a real text layer are read directly (fast and exact) and only pages without one (scans) are rendered and OCR'd. You pay for OCR only on those pages, and blank pages are never OCR'd or charged.
- **Tables as data.** Tables drawn with ruling lines also come back in a `tables` array, one list of cells per row, with the page number, ready for a spreadsheet.
- **Detects the file type from the content**, not the URL, so links like `/download?id=42` work. HTML error pages, ZIPs and other files get a clear error row instead of garbage.
- **One bad file never stops the run.** Password-protected, damaged and oversized files, 404s and timeouts each get an `error` row, and those rows are free. Damaged PDFs that the main parser can't read are retried with a second, more forgiving PDF engine.
- **Guards against risky link lists.** Downloads to private, loopback, link-local and cloud-metadata addresses are refused, on the first request and on every redirect. File size is capped while downloading (compressed downloads included).

### Use cases

- **RAG and LLM ingestion:** turn reports, manuals, papers and policies into Markdown, then load them into a vector database or prompt.
- **Invoices, statements and reports to text:** extract the text and tables of business PDFs for parsing, search or data entry.
- **Scanned documents:** OCR scanned contracts, letters and archive PDFs that have no text layer.
- **Image OCR:** read text from screenshots, photos of documents, receipts and multi-page TIFF faxes.
- **Enriching a dataset of document links:** a scraper collected PDF links; this Actor adds each document's text next to the scraped fields.

### How to use it

1. Add **PDF or image URLs**, and/or pick a **Dataset to enrich**. For datasets, set **URL fields** if the links aren't in `url`, `pdfUrl`, `fileUrl` or `link`.
2. Pick the **Output format** (Markdown or plain text) and, for scans in other languages, the **OCR languages**.
3. Optional: limit **Pages** (e.g. `1-5,8`), turn on **One row per page** for page-level chunks and citations, or raise **Max file size**.
4. Run it. Export as JSON, CSV or Excel, or chain it with an integration.

**Input example**

```json
{
  "urls": ["https://example.com/annual-report-2025.pdf", "https://example.com/scans/contract.pdf", "https://example.com/receipt.jpg"],
  "outputFormat": "markdown",
  "ocr": "auto",
  "ocrLanguages": ["eng", "deu"],
  "extractTables": true,
  "outputPerPage": false,
  "maxFileSizeMb": 50
}
```

**Output example** (shortened; values are illustrative)

```json
{
  "url": "https://example.com/annual-report-2025.pdf",
  "fileType": "pdf",
  "mimeType": "application/pdf",
  "pageCount": 48,
  "pagesProcessed": 48,
  "ocrPages": [47, 48],
  "format": "markdown",
  "text": "# Annual Report 2025\n\n## Revenue Overview\n\nRevenue grew 12% ...\n\n| Region | Q2 | Q3 |\n| --- | --- | --- |\n| Europe | 1,200 | 1,550 |",
  "charCount": 96412,
  "truncated": false,
  "tables": [{"page": 3, "rows": [["Region", "Q2", "Q3"], ["Europe", "1,200", "1,550"]]}],
  "title": "Annual Report 2025",
  "author": "Example Ltd",
  "creationDate": "2026-02-03T09:15:00Z",
  "fileSizeBytes": 2483120,
  "warnings": [],
  "error": null,
  "processedAt": "2026-10-16T12:00:00Z"
}
```

With **One row per page**, each row also has `page`, and `text`, `tables` and `ocrPages` cover that page only. Failed URLs have `error` set (for example `PDF is password-protected`, `HTTP 404 Not Found`, `File is larger than the 50 MB limit`) and empty text. `warnings` notes things like pages skipped by the page limit or scanned pages left unread with OCR off. With **Keep the item's original fields** (the default), dataset items keep all their fields and gain these columns. A run summary is saved as `OUTPUT` in the key-value store.

### How much does it cost?

Pay per event, Apify platform usage included:

**Apify Store discounts:** the prices above are for Apify's Free plan. Paid plans pay less per 1,000: processed documents $4.50 on Starter, $4.00 on Scale, $3.50 on Business; OCR pages $13.50, $12.00 and $10.50. The "from" price at the top of the Store page is the Business price.

- **$0.005 per run** (run start).

- **$0.005 per document** (PDF or image).

- **$0.015 per OCR'd page:** each scanned PDF page that was OCR'd, and each page after the first of a multi-page TIFF (an image's price includes OCR of its first page).

- A document is charged once whether you get one row or one row per page.

- PDF pages read from their text layer cost nothing extra; only pages that were actually OCR'd add $0.015 (listed in `ocrPages`). With OCR *always*, every processed page is OCR'd and charged, except blank pages, which are never OCR'd or charged.

- Free: failed downloads, files that aren't a PDF or image, password-protected files, files too damaged to read at all, invalid URLs, and repeats skipped by *Process each URL once*.

**Worked examples:**

*Examples use the Free-plan rate. Where an example doesn't add it, each run also has its run-start charge, counted per GB of Actor memory (minimum one; one at the default memory); Apify Proxy (where used) is billed separately.*

- 1,000 text-based PDFs (reports, invoices) in one run: $0.005 + 1,000 × $0.005 = **$5.005**.
- 100 scanned PDFs of 10 pages each: $0.005 + 100 × $0.005 + 1,000 × $0.015 = **$15.505**.
- 500 receipt photos: $0.005 + 500 × $0.005 = **$2.505**.
- One 20-page TIFF fax: $0.005 + $0.005 + 19 × $0.015 = **$0.295**.

If you set a maximum cost per run, the Actor checks the price of each document (including its OCR pages) before storing it and stops cleanly when the next document wouldn't fit. Nothing is stored that wasn't paid for, and nothing is charged that wasn't stored.

### FAQ and limits

#### How accurate is the OCR?

It depends on the scan. Clean, straight scans at normal print size read very well; low-resolution, skewed, blurry or faint scans produce errors. Raise **OCR resolution** to 300 for small print. Handwriting is not supported in any reliable way, and pages that display sideways (scanned sideways without a rotation flag, or rotated the wrong way in the file) are not auto-rotated, so their text comes out garbled. Rotated text in a text-layer PDF (such as a sideways stamp in the margin or a sideways table heading) is left out of the output on purpose, because it would come out one letter per line.

#### Why is a scanned page empty?

OCR is set to *never*, or the page has a little real text (a stamp, a header) and only the rest is scanned. *Auto* OCRs a page only when it has fewer than about 20 readable characters and contains an image or drawing (a page whose only content is a few words of real text, such as a title page, is returned as is); use **OCR: always** for such documents.

#### Which tables are detected?

Tables drawn with ruling lines (cell borders). Borderless tables come out as text, one tab-separated (plain text) or space-separated (Markdown) line per row. Merged cells come back as separate cells, some of them empty.

#### Are there size limits?

50 MB per file by default (up to 200 MB), 200 pages per document by default (up to 2,000), and images up to 80 megapixels. The text of one row is capped at 2,000,000 characters (Apify's 9 MB item limit), with `truncated: true`; use **One row per page** for very long documents. A file that takes more than 5 minutes on a single page is stopped with an error row.

#### Can it open password-protected PDFs?

No. Those return `PDF is password-protected` (free). PDFs that only restrict printing or copying open normally.

#### Does it read form fields, attachments or scripts in PDFs?

It reads the text drawn on each page. Values typed into fillable form fields are usually not part of the text layer and are not read (except on pages that are OCR'd). Attachments and scripts are ignored.

### Other tools by the same author

| Actor | Use it to |
|---|---|
| [Vector DB Sync: Pinecone & Qdrant RAG Loader](https://apify.com/charlesmarkette/vector-db-sync-pinecone-qdrant-rag-loader-delta-updates) | Chunk, embed and load the extracted text into Pinecone or Qdrant (its default text field is `text`, ID field `url`). |
| [Dataset Filter & Transform](https://apify.com/charlesmarkette/dataset-filter-transform-rename-flatten-fields) | Keep only the columns you need, drop failed rows, rename fields. |
| [Google Sheets Sync](https://apify.com/charlesmarkette/google-sheets-sync) | Send extracted text or table rows to a Google Sheet. |
| [Merge & Deduplicate Datasets](https://apify.com/charlesmarkette/merge-deduplicate-datasets-only-new-items) | Keep only new document links between runs, so each file is processed once. |

Built with permissively licensed open-source software: [pdfplumber](https://github.com/jsvine/pdfplumber) and [pdfminer.six](https://github.com/pdfminer/pdfminer.six) (MIT) for text and tables, [pypdfium2](https://github.com/pypdfium2-team/pypdfium2) (Apache-2.0/BSD-3) for page rendering, and [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) (Apache-2.0) via pytesseract.

# Actor input Schema

## `urls` (type: `array`):

One per line. Links to PDF files or images (PNG, JPEG, TIFF, WEBP, BMP, GIF). The file type is detected from the file's content, so links without a .pdf extension work too.

## `datasetId` (type: `string`):

Optional. A dataset from another run that has document links (scraped PDFs, invoice links, report URLs…). For each item, the first field below that holds a URL is processed and the text columns are added next to the item's fields. Use {{resource.defaultDatasetId}} in an integration.

## `urlFields` (type: `array`):

Item fields to take the document URL from, in order of preference. Dotted paths work (files.pdf). Default: url, pdfUrl, fileUrl, link.

## `keepItemFields` (type: `boolean`):

On: each output row is the original item plus the text columns (text columns win if a name clashes). Off: only the text columns.

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

Markdown: headings (from font size), bullet and numbered lists, and tables as Markdown tables; the best input for LLMs and RAG. Text: plain text in reading order, with table rows tab-separated.

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

Detect tables drawn with ruling lines and return them in a `tables` array (rows of cells, with the page number), and as Markdown tables in the text. Borderless tables come out as tab-separated lines instead.

## `outputPerPage` (type: `boolean`):

Off: one row per document with the whole text. On: one row per page (with a `page` number), handy for chunking and citations. Pricing is per document either way.

## `ocr` (type: `string`):

Auto: OCR only PDF pages without a usable text layer (scanned pages: fewer than 20 readable characters, plus an image or drawing). Blank pages are never OCR'd or charged. Always: OCR every PDF page (for PDFs whose text layer is garbled). Never: text layer only, so scanned pages come back empty and images are skipped. Images are always read with OCR unless this is Never.

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

Languages in the scanned documents. Picking only the languages you need is faster and more accurate.

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

Resolution PDF pages are rendered at for OCR. 200 suits most scans; 300 helps with small print but is slower and needs more memory.

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

Optional. Which pages to process, e.g. 1-5,8 or 10- (from page 10 to the end). Empty: all pages, up to the maximum below. For multi-page TIFFs these are the TIFF's pages.

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

Process at most this many pages of each document (after the Pages filter). Longer documents get a warning.

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

Larger files are skipped with an error row (free).

## `maxResults` (type: `integer`):

Optional. Stop after this many rows (documents, or pages with One row per page).

## `concurrency` (type: `integer`):

How many documents are downloaded and processed at once. OCR is CPU-heavy: with more parallel documents, give the run more memory (about 512 MB per parallel OCR document).

## `dedupe` (type: `boolean`):

Skip repeats of the same URL (and don't charge for them twice).

## Actor input object example

```json
{
  "urls": [
    "https://arxiv.org/pdf/1706.03762"
  ],
  "keepItemFields": true,
  "outputFormat": "markdown",
  "extractTables": true,
  "outputPerPage": false,
  "ocr": "auto",
  "ocrLanguages": [
    "eng"
  ],
  "ocrDpi": 200,
  "maxPagesPerDocument": 200,
  "maxFileSizeMb": 50,
  "concurrency": 2,
  "dedupe": true
}
```

# Actor output Schema

## `items` (type: `string`):

No description

## `summary` (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 = {
    "urls": [
        "https://arxiv.org/pdf/1706.03762"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("charlesmarkette/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 = { "urls": ["https://arxiv.org/pdf/1706.03762"] }

# Run the Actor and wait for it to finish
run = client.actor("charlesmarkette/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 '{
  "urls": [
    "https://arxiv.org/pdf/1706.03762"
  ]
}' |
apify call charlesmarkette/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,charlesmarkette/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/pHCbxLEFAJn9Rfng1/builds/kCZ56fYV2yXnKp5tJ/openapi.json
