# Searchable PDF OCR — Add a Text Layer to Scanned PDFs (`northbound_works/searchable-pdf-ocr`) Actor

Turn scanned or image-only PDFs into fully searchable, selectable, copy-pasteable PDFs. Keeps the original page image untouched and adds an invisible OCR text layer underneath.

- **URL**: https://apify.com/northbound\_works/searchable-pdf-ocr.md
- **Developed by:** [Austin Cooley](https://apify.com/northbound_works) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 50.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Searchable PDF OCR — Add a Text Layer to Scanned PDFs

Convert scanned or image-only PDFs into **fully searchable, selectable, copy-pasteable
PDFs**. The page you get back looks identical to the one you sent — the original image is
preserved byte-for-byte at the default settings — with an invisible OCR text layer added
underneath it.

This is the step most PDF tools skip. Text extractors give you a `.txt` file and throw the
document away. This gives you back **the same PDF, now searchable**, so Ctrl+F works, your
document management system can index it, and copy-paste selects real words.

***

### What it does

- **Adds an invisible text layer** under the existing page image (OCRmyPDF + Tesseract)
- **Preserves the original scan** — the visual page is untouched at optimisation level 0–1
- **Skips pages that already have text**, so mixed digital/scanned documents cost less
- **Straightens crooked scans** and auto-rotates sideways pages before reading
- **26 languages**, including Chinese, Japanese, Korean, Arabic, Hebrew, Cyrillic
- **Returns the extracted text too**, if you want it in the dataset

### What it is not

It does not fill forms, split, merge, sign or translate. It does one job.

***

### Typical uses

- Make a **scanned contract archive searchable** before loading it into a DMS
- Prepare **invoices and receipts** so an extraction pipeline can actually read them
- Turn **scanned books, court filings or research papers** into searchable references
- Fix **fax-quality documents** that no search engine or indexer can read
- Preprocess documents before feeding them to an LLM or RAG pipeline — models cannot read
  an image-only PDF

***

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `pdfUrls` | array | — | Direct links to PDFs. Must point at the file, not a viewer page. |
| `language` | string | `eng` | The document's language. Wrong language produces garbage — set this. |
| `mode` | string | `skip-text` | `skip-text`, `force-ocr`, or `redo-ocr`. |
| `deskew` | boolean | `true` | Straighten tilted pages before reading. |
| `rotatePages` | boolean | `true` | Auto-correct sideways/upside-down pages. |
| `cleanBeforeOcr` | boolean | `false` | Remove speckle for recognition only; output image unchanged. |
| `optimize` | integer | `1` | 0 = none, 1 = safe lossless, 2–3 = smaller but lossy. |
| `includeTextInDataset` | boolean | `true` | Return the recognised text alongside the PDF link. |

#### Choosing a mode

- **`skip-text`** (default) — only OCRs pages with no text layer. Safest and cheapest,
  because you are not charged for pages that were already searchable.
- **`force-ocr`** — rasterises every page and re-reads it. Use when a PDF has a *bad*
  existing text layer, e.g. from a poor scanner. Existing vector text is flattened.
- **`redo-ocr`** — discards the existing text layer and re-reads without rasterising.
  Preserves vector quality. Incompatible with deskew, rotate and clean, which are
  disabled automatically if you select it.

### Output

One dataset row per input PDF:

```json
{
  "url": "https://example.com/scan.pdf",
  "status": "succeeded",
  "pagesTotal": 12,
  "pagesOcred": 9,
  "pagesAlreadyHadText": 3,
  "searchablePdfUrl": "https://api.apify.com/v2/key-value-stores/.../searchable-0000.pdf",
  "outputKey": "searchable-0000.pdf",
  "text": "..."
}
```

`status` is `succeeded`, `skipped` (already searchable) or `failed` (with an `error`
explaining why). One bad URL never aborts the run — the rest still process.

***

### Pricing

Pay per event:

| Event | What it means |
|---|---|
| Actor start | Once per run |
| Document processed | Once per PDF successfully made searchable |
| Page OCR'd | Per page that actually needed OCR |

**You are billed for pages that were actually read, not pages in the file.** A 40-page PDF
where only 3 pages are scans bills 3 pages. Documents that fail, or that were already
searchable under `skip-text`, cost nothing beyond the run start.

If you set a maximum charge for the run, processing stops cleanly at that ceiling rather
than overrunning it, and the log tells you where it stopped.

***

### Limits and honest caveats

- **Maximum 250 MB per PDF.**
- **OCR accuracy depends on scan quality.** 300 DPI clean scans are near-perfect; a
  photographed page under bad lighting will not be. No engine fixes an unreadable source.
- **Password-protected PDFs are rejected**, not cracked.
- **Handwriting is not supported.** Tesseract reads printed text. Cursive returns noise.
- **Set the language.** English OCR on a German document is the single most common cause
  of bad output.

***

### Notes

Built on [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) and
[Tesseract](https://github.com/tesseract-ocr/tesseract), both open source. This Actor
touches no third-party website and scrapes nothing — it processes files you supply.

Something not working? Report an issue on the Actor page. Issues get a response, and
broken Actors get fixed rather than abandoned.

# Actor input Schema

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

Direct links to the PDFs you want to make searchable. Each must be a publicly reachable URL ending in a PDF file — not a viewer page. The original page images are preserved exactly; an invisible text layer is added underneath.

## `language` (type: `string`):

Language of the text in the scans. Picking the right one materially improves accuracy — English OCR on a German document produces garbage.

## `mode` (type: `string`):

Most PDFs are a mix. 'Skip' is the safe default and the cheapest — it only OCRs pages that genuinely have no text, and you are only charged for those. Use 'Force' when a PDF has a bad existing text layer you want replaced.

## `deskew` (type: `boolean`):

Rotates slightly tilted pages back to level before reading them. Improves accuracy on photographed or hand-fed scans. Leave on unless your scans are already perfectly square.

## `rotatePages` (type: `boolean`):

Detects sideways or upside-down pages and rotates them upright.

## `cleanBeforeOcr` (type: `boolean`):

Removes scanner noise and speckle to help the reader. The cleaning is used only for recognition — the page you get back is still your original image, untouched.

## `optimize` (type: `string`):

How hard to compress the resulting PDF. Levels above 1 can visibly soften images, so leave it at 1 unless file size matters more than fidelity.

## `includeTextInDataset` (type: `boolean`):

Puts the full recognised text of each document into the dataset alongside the download link, so you can search or process it without opening the PDF.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "language": "eng",
  "mode": "skip-text",
  "deskew": true,
  "rotatePages": true,
  "cleanBeforeOcr": false,
  "optimize": "1",
  "includeTextInDataset": true
}
```

# 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://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbound_works/searchable-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://www.irs.gov/pub/irs-pdf/fw9.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("northbound_works/searchable-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://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ]
}' |
apify call northbound_works/searchable-pdf-ocr --silent --output-dataset

```

## MCP server setup

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