# PDF OCR – Scanned PDF to Searchable PDF & Text (`gazidev/pdf-ocr`) Actor

OCR scanned PDFs and images into searchable PDFs (invisible text layer over the original pages) plus full text, per-page text and Markdown. Tesseract 5, 22 languages, only pages without a text layer are OCR'd, confidence per page. $4.50 per 1,000 OCR pages.

- **URL**: https://apify.com/gazidev/pdf-ocr.md
- **Developed by:** [Cemal Atakli](https://apify.com/gazidev) (community)
- **Categories:** AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.50 / 1,000 scanned page 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 OCR – Scanned PDF to Searchable PDF & Text

**PDF OCR** for scanned documents: give it links to scanned PDFs (or photos/scans of pages) and get back a **searchable PDF** — the original pages with an invisible, selectable text layer — plus the full text, per-page text, Markdown paragraphs and an OCR confidence score. Built on Tesseract 5 OCR, 22 languages, runs in bulk, pay only for the pages that were actually OCR'd.

- 🔎 **Scanned PDF → searchable PDF**: Ctrl+F, copy & paste and full-text indexing work in any PDF viewer; the page image is not re-compressed or changed
- 🧠 **Smart OCR**: in `auto` mode only pages *without* a text layer are OCR'd; pages that already have text are kept and cost $0.0003
- 🖼️ **Images → one searchable PDF**: combine JPG, PNG, multi-page TIFF, WebP, HEIC scans into one multi-page PDF (or one PDF per image)
- 📝 **Text output**: full text, text per page, Markdown with re-flowed paragraphs, word counts, OCR confidence (0–100) per page and per document
- 📦 **Files**: searchable `.pdf`, `.txt` and `.md` saved in the key-value store with direct download links in every row
- 💸 **$4.50 per 1,000 OCR pages**, errors and blank pages free

### What does PDF OCR do?

1. Downloads each PDF (Google Drive, Dropbox and GitHub share links work) or image.
2. Checks every page: does it already have real text? If not (a scan, a photo, broken font encoding), the page is rendered at 300 DPI and read with **Tesseract 5 OCR**.
3. The recognised words are placed as an **invisible text layer exactly over the words on the page** (correct for rotated pages and cropped pages), so the file looks the same but becomes searchable and selectable — the same technique professional "OCR PDF" tools use.
4. Saves the searchable PDF, the text and Markdown files, and pushes one dataset row per document.

### Use cases

- Make **scanned contracts, invoices, letters and archives searchable** for document management systems, SharePoint, Google Drive or Dropbox search
- **OCR PDF** batches before feeding them to **LLMs, RAG pipelines and vector databases** (text + Markdown per page)
- Digitise **old books, newspapers, typewritten records** and library scans (archive.org, BHL, Wikimedia)
- Turn **phone photos of documents** into one searchable PDF
- Fix PDFs from scanners that save image-only files, or **redo a bad OCR layer** (`force` mode)
- Legal discovery, compliance and accounting teams who need searchable scanned PDF files at scale

### Input example

```json
{
  "pdfUrls": ["https://archive.org/download/IAPSOP-Plummer_to_Crowley_January_7_1914/IAPSOP-Plummer_to_Crowley_January_7_1914.pdf"],
  "imageUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/e/e2/Typewritten_letter_from_Dr_Claude_H_Organ_Jr_MD_FACS_to_Dr_Leonidas_Berry_dated_October_5_1965.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/5/55/A_text-book_of_agricultural_entomology_%28Page_192%29_BHL9798403.jpg"
  ],
  "combineImages": true,
  "ocrLanguages": ["eng"],
  "ocrMode": "auto",
  "dpi": 300
}
```

| Field | Default | Meaning |
|---|---|---|
| `pdfUrls` | demo scan | Scanned or mixed PDFs, one searchable PDF each |
| `imageUrls` | – | Page images; combined into one PDF by default (`combineImages`) |
| `ocrLanguages` | `["eng"]` | 22 languages: English, German, French, Spanish, Italian, Portuguese, Dutch, Turkish, Polish, Russian, Ukrainian, Czech, Swedish, Romanian, Arabic, Hindi, Chinese (simplified/traditional), Japanese, Korean, Indonesian, Vietnamese |
| `ocrMode` | `auto` | `auto` = OCR only pages without text; `force` = OCR every page (replaces an existing invisible OCR layer) |
| `dpi` | 300 | OCR render resolution (100–600) |
| `pageRange` | all | e.g. `1-5`, `1,3,10-`, `-3--1` |
| `saveSearchablePdf`, `saveTextFiles`, `includePageTexts` | on | What to output |
| `pdfPassword` | – | For encrypted PDFs |

### Output example

One row per document (shortened, see `SAMPLE_OUTPUT.json`):

```json
{
  "type": "document",
  "status": "ok",
  "url": "https://archive.org/download/IAPSOP-Plummer_to_Crowley_January_7_1914/IAPSOP-Plummer_to_Crowley_January_7_1914.pdf",
  "fileName": "IAPSOP-Plummer_to_Crowley_January_7_1914.pdf",
  "source": "pdf",
  "pageCount": 2,
  "ocrPages": 2,
  "textPages": 0,
  "emptyPages": 0,
  "ocrConfidence": 70.5,
  "ocrLanguages": "eng",
  "wordCount": 425,
  "searchablePdfUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/0001-IAPSOP-Plummer_to_Crowley_January_7_1914-searchable.pdf",
  "searchablePdfBytes": 310343,
  "textFileUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/0001-IAPSOP-Plummer_to_Crowley_January_7_1914.txt",
  "markdownFileUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/0001-IAPSOP-Plummer_to_Crowley_January_7_1914.md",
  "text": "... appealed to the writer.\n\nMay I be permitted to express the hope that the ensuing year may witness a oloser relationship ...",
  "pages": [
    {"pageNumber": 1, "method": "ocr", "confidence": 67.3, "wordCount": 344, "textLayerAdded": true, "text": "..."},
    {"pageNumber": 2, "method": "ocr", "confidence": 83.9, "wordCount": 81, "textLayerAdded": true, "text": "..."}
  ]
}
```

Failed items are separate rows with `type: "error"`, an `errorCode` (`http-404`, `not-a-pdf`, `password-required`, `invalid-pdf`, `invalid-image`, `too-large`, …) and a readable message. They are free.

### Pricing

Pay per event — no subscription, no compute bill on top:

| Event | Price |
|---|---|
| Page read with OCR (`ocr-page`) | **$0.0045** ($4.50 / 1,000 pages) |
| Page that already had text (`text-page`) | $0.0003 |
| Actor start | $0.00005 |
| Blank pages, failed pages, errors, files | free |

How it compares (Apify Store, October 2026, published prices):

| Actor | OCR page | Other charges |
|---|---|---|
| **PDF OCR (this Actor)** | **$0.0045** | searchable PDF, .txt and .md included |
| nerolabs/bulk-ocr-image-pdf-to-text | $0.005 | $0.01 per file export, $0.001 per text-layer page |
| amanatools/pdf-ocr-extractor | $0.015 | $0.005 per document |
| eliai/pdf-text-extractor | $0.015 | $0.003 per PDF |
| ely_source/pdf-ocr | $0.02 | $0.01 per run |
| tanod/pdf-ocr | – | $0.03 per document |

Set a **maximum total charge** on the run and the Actor stops cleanly when it is reached (finished pages are kept, the current PDF is marked `partial`).

### FAQ

**What is a searchable PDF?** A PDF where each scanned page image has an invisible text layer on top. It looks exactly like the scan, but you can search, select and copy the text, and search engines / document systems can index it.

**Does it change how my PDF looks?** No. The original page content stays untouched; only invisible text is added (typically +3–10 KB per page). Images you upload as JPEG are embedded without re-compression.

**What about PDFs that are already partly digital?** In `auto` mode, pages with real text are left alone and charged $0.0003; only scanned pages are OCR'd. Use `force` to OCR everything — an existing invisible OCR layer is replaced, visible digital text is never covered twice.

**How accurate is the OCR?** Tesseract 5 (LSTM) reads clean printed text very well (the demo book page scores 94/100); `ocrConfidence` (0–100) per page tells you how sure it was. Typewriter scans, faded copies and handwriting give lower confidence. Handwriting is not supported.

**Which languages?** 22 languages, several at once (e.g. `["eng","deu"]`). Pick only the languages present: each extra one slows OCR.

**How fast is it?** About 3–4 s per page at the default 2 GB memory. Raise memory to 8 or 16 GB to OCR 2–4 pages in parallel at the same price.

**Is my data stored?** Files live in your run's key-value store (your Apify account) under your data-retention settings. Nothing is sent to third-party OCR APIs — OCR runs inside the Actor.

**Rotated pages?** Pages rotated with the PDF `/Rotate` flag are handled (the text layer is aligned for 0/90/180/270°). A page scanned sideways *without* a rotate flag is OCR'd as it is displayed.

### Use with AI agents / Apify MCP

This Actor works as a tool for AI agents through the [Apify MCP server](https://mcp.apify.com) (Claude, Cursor, ChatGPT, LangChain, n8n, Make, Zapier). An agent can call `gazidev/pdf-ocr` with `pdfUrls`, read the `text` / `pages[].text` fields for reasoning, and hand the user the `searchablePdfUrl`. Example prompt: *"OCR these scanned invoices and give me searchable PDFs and the invoice totals."*

API call:

```bash
curl -X POST "https://api.apify.com/v2/acts/gazidev~pdf-ocr/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"pdfUrls": ["https://example.com/scan.pdf"], "ocrLanguages": ["eng"]}'
```

### Related Actors

- **PDF to Text & Markdown** (`gazidev/pdf-text-extractor`) — digital PDFs to Markdown with tables and RAG chunks
- **Image to Text OCR** (`gazidev/image-ocr`) — text, words and boxes from single images

*Built with Tesseract OCR (Apache-2.0) and PDFium via pypdfium2 (Apache-2.0/BSD-3).*

# Actor input Schema

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

Direct links to scanned (or mixed) PDF files, one per line. Each PDF becomes one searchable PDF. Google Drive / Dropbox share links and GitHub `blob` links are converted to direct downloads automatically.

## `imageUrls` (type: `array`):

Optional. JPG, PNG, TIFF (multi-page), WebP, GIF, BMP or HEIC scans. By default all images are combined, in this order, into ONE searchable PDF (one page per image). JPEGs are embedded without re-compression.

## `combineImages` (type: `boolean`):

On: all image URLs become one multi-page searchable PDF. Off: one searchable PDF per image.

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

Languages of the scanned text. Pick 1-3: more languages are slower.

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

`auto`: only pages without a usable text layer (scans, photos, broken font encodings) are OCR'd and charged as `ocr-page`; pages that already have text are kept as they are and charged as the cheap `text-page`. `force`: OCR every page. In force mode an existing invisible OCR layer (from another tool) is replaced; pages with visible digital text are OCR'd for the text output but no second layer is added on top.

## `dpi` (type: `integer`):

Pages are rendered at this resolution for OCR. 300 is best for normal print; 200 is about 20% faster; 400 helps with very small text. The original page image in the PDF is never changed.

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

Optional. Pages to process, e.g. `1-5`, `1,3,10-`, or `-3--1` for the last three pages. Empty = all pages (up to `Max pages per PDF`). Other pages stay in the PDF unchanged and are not charged.

## `maxPagesPerPdf` (type: `integer`):

Safety cap on processed pages per PDF (after the page range). 0 = no limit.

## `saveSearchablePdf` (type: `boolean`):

Save each searchable PDF in the run's key-value store, linked from the dataset row (`searchablePdfUrl`).

## `saveTextFiles` (type: `boolean`):

Also save the full text (.txt) and Markdown with re-flowed paragraphs and page markers (.md) for every PDF.

## `includePageTexts` (type: `boolean`):

Add a `pages` array to each row: page number, method (ocr / text / empty), OCR confidence, word count and the page text.

## `maxCharsPerItem` (type: `integer`):

Longer text is cut in the dataset (`textTruncated: true`). The complete text is always in the saved .txt/.md files and in the searchable PDF.

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

Optional. Password for encrypted PDFs (used for all PDFs of the run).

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

Larger files are skipped with a free error row.

## `requestTimeoutSecs` (type: `integer`):

Timeout for one download. Failed downloads (timeouts, HTTP 429/5xx) are retried 3 times with backoff.

## `maxConcurrentDownloads` (type: `integer`):

Files downloaded in parallel while earlier ones are OCR'd.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://archive.org/download/IAPSOP-Plummer_to_Crowley_January_7_1914/IAPSOP-Plummer_to_Crowley_January_7_1914.pdf"
  ],
  "imageUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/e/e2/Typewritten_letter_from_Dr_Claude_H_Organ_Jr_MD_FACS_to_Dr_Leonidas_Berry_dated_October_5_1965.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/5/55/A_text-book_of_agricultural_entomology_%28Page_192%29_BHL9798403.jpg"
  ],
  "combineImages": true,
  "ocrLanguages": [
    "eng"
  ],
  "ocrMode": "auto",
  "dpi": 300,
  "maxPagesPerPdf": 500,
  "saveSearchablePdf": true,
  "saveTextFiles": true,
  "includePageTexts": true,
  "maxCharsPerItem": 100000,
  "maxFileSizeMb": 100,
  "requestTimeoutSecs": 120,
  "maxConcurrentDownloads": 3
}
```

# Actor output Schema

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

No description

## `text` (type: `string`):

No description

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

No description

## `files` (type: `string`):

No description

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

No description

## `results` (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://archive.org/download/IAPSOP-Plummer_to_Crowley_January_7_1914/IAPSOP-Plummer_to_Crowley_January_7_1914.pdf"
    ],
    "imageUrls": [
        "https://upload.wikimedia.org/wikipedia/commons/e/e2/Typewritten_letter_from_Dr_Claude_H_Organ_Jr_MD_FACS_to_Dr_Leonidas_Berry_dated_October_5_1965.jpg",
        "https://upload.wikimedia.org/wikipedia/commons/5/55/A_text-book_of_agricultural_entomology_%28Page_192%29_BHL9798403.jpg"
    ],
    "ocrLanguages": [
        "eng"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("gazidev/pdf-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 = {
    "pdfUrls": ["https://archive.org/download/IAPSOP-Plummer_to_Crowley_January_7_1914/IAPSOP-Plummer_to_Crowley_January_7_1914.pdf"],
    "imageUrls": [
        "https://upload.wikimedia.org/wikipedia/commons/e/e2/Typewritten_letter_from_Dr_Claude_H_Organ_Jr_MD_FACS_to_Dr_Leonidas_Berry_dated_October_5_1965.jpg",
        "https://upload.wikimedia.org/wikipedia/commons/5/55/A_text-book_of_agricultural_entomology_%28Page_192%29_BHL9798403.jpg",
    ],
    "ocrLanguages": ["eng"],
}

# Run the Actor and wait for it to finish
run = client.actor("gazidev/pdf-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 '{
  "pdfUrls": [
    "https://archive.org/download/IAPSOP-Plummer_to_Crowley_January_7_1914/IAPSOP-Plummer_to_Crowley_January_7_1914.pdf"
  ],
  "imageUrls": [
    "https://upload.wikimedia.org/wikipedia/commons/e/e2/Typewritten_letter_from_Dr_Claude_H_Organ_Jr_MD_FACS_to_Dr_Leonidas_Berry_dated_October_5_1965.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/5/55/A_text-book_of_agricultural_entomology_%28Page_192%29_BHL9798403.jpg"
  ],
  "ocrLanguages": [
    "eng"
  ]
}' |
apify call gazidev/pdf-ocr --silent --output-dataset

```

## MCP server setup

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