# PDF Text Extractor — OCR for scanned PDFs (Arabic + English) (`amanatools/pdf-ocr-extractor`) Actor

Turn scanned or image-only PDFs into structured text that keeps its reading order: per-block bounding boxes, per-block OCR confidence, column-aware ordering, and a spatial plain-text rendering. Arabic and English OCR built in. Pay per document plus per page.

- **URL**: https://apify.com/amanatools/pdf-ocr-extractor.md
- **Developed by:** [Dos](https://apify.com/amanatools) (community)
- **Categories:** Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.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

## PDF Text Extractor — OCR for scanned PDFs (Arabic + English)

Extract text from **scanned and image-only PDFs** by URL — the OCR half of amanatools' PDF toolkit, and the only extractor here that keeps the layout. Arabic, Persian and Urdu included.

Feed it PDF links. Get back text that still knows where it came from: per-block bounding boxes, per-block OCR confidence, column-aware reading order, and a spatial plain-text rendering of every page. Arabic and English are both first-class.

### Three things this does that the alternatives don't

#### 1. Layout survives

Every other PDF text extractor hands you one flat blob per page. This one returns `blocks[]` — each with `bbox`, `confidence`, `column` and `order` — sorted into true reading order using whitespace-gutter column detection, plus a `text_layout` field that reproduces the page on a character grid the way `pdftotext -layout` does.

Two-column papers, brochures, invoices with a side panel, and ruled-line-free tables come out readable instead of interleaved. The gutter detector is tuned to **real** column geometry, not to a comfortable round number: a standard two-column paper (US Letter, 1 in margins, LaTeX's default 10 pt `\columnsep` — columns at 72–301 pt and 311–540 pt) has a gutter just **1.6% of the page wide**, and that is detected. See **Limits** for what still defeats it. Downstream LLMs and agents get something they can actually parse.

#### 2. Arabic is a first-class citizen

A store-wide search for "arabic" returns no OCR or document-processing actors at all. This one ships the `ara` model, right-to-left reading order (bands ordered right→left, RTL lines joined in logical order), and automatic Latin / Arabic / Cyrillic script detection per document. Built for MENA invoices, receipts and government PDFs, by an operator based in Abu Dhabi.

It is also honest about what tesseract Arabic can and cannot do — see **Limits**.

#### 3. It knows when *not* to OCR

`ocr_mode: "auto"` (the default) reads the digital text layer when there is one — fast, cheap, character-perfect — and only rasterizes and OCRs the pages that actually need it. A born-digital report with three scanned annexes is handled page by page, and the invoice reflects that: OCR pages cost $0.02, text-layer pages cost $0.005.

#### Which actor do I want?

- **Born-digital PDF with real tables?** Use **[amanatools/pdf-table-extractor](https://apify.com/amanatools/pdf-table-extractor)** — it returns `{headers, rows}`.
- **Scanned PDF, image-only pages, or you need coordinates and confidence?** Start here.

### Input

```json
{
    "pdf_urls": [
        "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/v16.0.0/tests/resources/ccitt.pdf",
        "https://example.com/invoice-scan.pdf"
    ],
    "pages": "all",
    "ocr_mode": "auto",
    "language": "auto",
    "preserve_layout": true,
    "dpi": 200,
    "auto_rotate": true,
    "min_word_confidence": 0,
    "max_pages_per_document": 20,
    "include_failed_documents": true
}
```

| Field | Default | Notes |
| --- | --- | --- |
| `pdf_urls` | — | Required. Up to 50 direct PDF links per run, 50 MB each. Repeated links are processed once. |
| `pages` | `all` | `all`, `1-5`, or `1,3,7-9`. Same syntax as pdf-table-extractor. |
| `ocr_mode` | `auto` | `auto` | `always` | `never`. `auto` OCRs only pages with under 100 digital characters. |
| `language` | `auto` | `auto`, `eng`, `ara`, `ara+eng`, `fra`, `deu`, `spa`, `por`, `ita`, `nld`, `tur`, `rus`, `fas`, `urd`. |
| `preserve_layout` | `true` | Off = no `blocks`, no `text_layout`, much smaller items. |
| `dpi` | `200` | 72–400. Use 300 for dense or small print. |
| `auto_rotate` | `true` | Detects and fixes sideways / upside-down scans. |
| `min_word_confidence` | `0` | Drop OCR words below this score. 0 keeps everything. |
| `max_pages_per_document` | `20` | Also your cost ceiling — see Pricing. Raise it (up to 500) for long scans. |
| `include_failed_documents` | `true` | Failed or empty documents still appear, with `status` and `error`. |

### Output

One dataset item per PDF, with pages nested — the same shape philosophy as pdf-table-extractor, so pipelines can branch on document status identically for both actors.

```json
{
    "url": "https://example.com/scan.pdf",
    "status": "ok",
    "error": null,
    "n_pages_in_document": 1,
    "n_pages": 1,
    "n_ocr_pages": 1,
    "n_text_pages": 0,
    "truncated_pages": false,
    "language_used": "eng",
    "script": "latin",
    "mean_confidence": 93.9,
    "n_words": 729,
    "text": "page one text\fpage two text",
    "truncated_fields": [],
    "pages": [
        {
            "page": 1,
            "source": "ocr",
            "status": "ok",
            "error": null,
            "width": 612.0,
            "height": 792.0,
            "rotation_applied": 0,
            "n_words": 729,
            "mean_confidence": 93.9,
            "n_columns": 2,
            "charged": true,
            "text": "block one\n\nblock two",
            "text_layout": "  INVOICE                      2026-07-31\n  Total                              1,240",
            "blocks": [
                {
                    "order": 0,
                    "column": 0,
                    "bbox": [140.4, 31.3, 472.0, 73.1],
                    "text": "The LinnSequencer\n32 Track MIDI Sequence Recorder",
                    "n_words": 7,
                    "confidence": 95.2,
                    "script": "latin"
                }
            ]
        }
    ]
}
```

- `text` joins pages with a form feed (`\f`), the `pdftotext` convention.
- `bbox` is `[x0, top, x1, bottom]` in **PDF points**, origin at the top-left of the **rendered** page.
- `column` is always the visual left→right band index. Only `order` flips on right-to-left pages.
- `source` is `"ocr"` or `"text-layer"` per page, so you can always see what you paid for.

### Pricing

Pay-per-event, no start fee, no subscription:

| Event | Price | When |
| --- | --- | --- |
| `document-processed` | $0.05 | Once per PDF that delivered at least one page of text. |
| `ocr-page` | $0.02 | Per page that had to be rasterized and OCR'd. |
| `text-page` | $0.005 | Per page read straight from the digital text layer. |

**You never pay for undelivered work.** Failed downloads, unreadable and password-protected files, and pages that produce zero words are not charged at all — not even the per-document fee. Your maximum possible spend is fixed before you press Start:

```
max spend = $0.05 x documents + $0.02 x max_pages_per_document x documents
```

If you cap the run with **Max total charge** (`maxTotalChargeUsd`), the actor stops as soon as that budget is spent: it logs the reason, keeps everything already pushed and finishes successfully. It does **not** carry on processing the rest of your list for free.

### Limits — read these before you buy

- **OCR is statistical, not exact.** Expect roughly 95–99% character accuracy on clean 300-DPI printed scans, and materially worse on low-resolution faxes, skewed or shadowed phone photos, stamped or watermarked pages, and dot-matrix print. Every block and every page carries a confidence score precisely so you can gate on it instead of trusting blindly.
- **Handwriting is not supported.** Tesseract is a printed-text engine; handwritten notes, signatures and annotations come back as noise or nothing at all.
- **Arabic, stated honestly.** The `ara` model is trained on modern printed Arabic and does well on clean naskh-style print. It is weak on calligraphic and ligature-heavy display fonts, on Quranic typesetting with full harakat, and on old or degraded scans. Diacritics are frequently dropped or misread. Lines that mix Arabic, Latin and digits — typical of MENA invoices — may order imperfectly at the boundaries, because bidirectional reordering from a bitmap is genuinely ambiguous. We ship Arabic because nobody else in the store does, not because it is solved.
- **Column detection works on whitespace gutters — including tight ones.** A gutter counts when a clear vertical channel at least **1.5% of the page width** (9.2 pt on US Letter; LaTeX's default `\columnsep` is 10 pt) runs inside the middle 70% of the page. Channels narrower than 5% of the page must also be *corroborated*: at least three text lines on each side, and the channel clear across at least 90% of the page's line rows. That is what lets a normal two-column paper be detected while an incidental wide space in ordinary prose does not split the page in half. Up to two gutters survive per horizontal stripe, so at most three columns.
- **What still defeats column detection.** Pages with no clean vertical channel at all, rotated or nested layouts, magazine-style wrapping around images, scattered labels on a diagram, and — the honest one — **tables**: a table whose columns are more than 9 pt apart looks exactly like a two-column page, so its cells are read column by column rather than row by row. Two- or three-line fragments with a wide gap (an invoice header, an address block) are deliberately *not* split. When the ordering comes out wrong, use `text_layout` — it reproduces the page spatially and does not depend on the ordering heuristic.
- **This actor does not recover table structure.** It returns blocks, coordinates and spatial text, not `{headers, rows}`. For born-digital PDFs with real tables use [amanatools/pdf-table-extractor](https://apify.com/amanatools/pdf-table-extractor). For scanned tables, `text_layout` plus block bounding boxes is the raw material; the parsing is yours.
- **No searchable-PDF output.** This actor returns text and JSON; it does not write a new PDF with an invisible text layer.
- **Password-protected and encrypted PDFs are reported as errors, not cracked.**
- **Bounding boxes are in the rendered frame.** If `auto_rotate` fired (`rotation_applied` is non-zero), the coordinates are in the rotated frame and will not line up with the original page orientation.
- **Limits per run:** 50 URLs, 50 MB per PDF, 500 pages per document (default cap **20**, lowered from 50 so a default run fits inside a default timeout). Anything beyond is truncated with `truncated_pages: true` rather than silently dropped.
- **Very large dataset items are trimmed.** If a document would exceed Apify's ~9 MB item limit, `text_layout` is dropped first, then `blocks`, then `text` is truncated — and every stage is recorded in `truncated_fields`.
- **The run stops before its timeout rather than dying inside it.** The actor reads the run deadline, tracks how long a page actually costs, and stops queueing documents when the remaining time will not cover the next one. It logs the reason, pushes what exists and exits successfully — you never lose output you were charged for. Same for a migration: Apify restarts long-running containers, and this actor checkpoints processed URLs to the run's key-value store, so a migrated run resumes instead of re-pushing and re-charging documents it already delivered. Charges are raised only *after* an item reaches the dataset.
- **Very tall or very wide pages are rendered coarser, never dropped.** The render scale is clamped on both axes at 10000 px, so a 612 x 14400 pt page (the PDF maximum — long receipt scans, web-to-PDF exports, plotter output) renders at reduced DPI instead of exhausting memory. OCR quality on such a page suffers accordingly; use `pages` to work through it in slices if you need full DPI. The `text_layout` grid is likewise capped at 400 columns by widening the character cell, so wide engineering drawings come out coarse rather than cropped.
- **Persian and Urdu keep their ZWNJ.** U+200C (zero-width non-joiner) and U+200D are orthography, not junk, and are preserved verbatim; only true invisibles (U+200B, U+200E/F, the bidi overrides, U+FEFF, soft hyphen) are stripped. Tatweel and harakat are preserved too.
- **Speed:** roughly 1–4 seconds per OCR page at 200 DPI, more at 300+. A 100-page scan is minutes, not seconds. Text-layer pages are effectively instant, which is why `ocr_mode: auto` is the default.

### Developer notes

Local test run (no network, no Apify platform, tesseract binary optional):

```
pip install -r requirements.txt reportlab
python test_main.py        # or: pytest test_main.py
```

`test_main.py` covers the pure logic — page specs, OCR TSV parsing, RTL word order (including ZWNJ preservation), script detection, column ordering on real two-column paper geometry plus the negative cases that must NOT split, spatial rendering of oversized pages, render-scale clamping on both axes, input clamping, language resolution, the size guard, the charging plan, the charge-limit and run-deadline guards and the migration resume — plus an end-to-end text-layer run on a reportlab-generated fixture, and a real OCR round-trip that skips itself when `tesseract` is not on `PATH`.

Hostile QA fixtures worth running by hand (all under the OCRmyPDF `v16.0.0` resources path, deliberately *not* in the input prefill):

| File | Exercises |
| --- | --- |
| `invalid.pdf` | 44-byte corrupt file → clean `status: "error"` |
| `blank.pdf` | blank page → zero words, delivered but uncharged |
| `rotated_skew.pdf`, `skew.pdf` | `auto_rotate` |
| `francais.pdf` | non-English OCR |
| `graph_ocred.pdf` | existing invisible GlyphLessFont text layer → `auto` reuses it |

Runtime notes: the Docker image installs the tesseract binary plus 13 language packs and `tesseract-ocr-osd`, and sets `OMP_THREAD_LIMIT=1` (without it, tesseract's OpenMP parallelism makes single-page OCR *slower* in a container). `TESSDATA_PREFIX` is deliberately left unset — the Debian packages already configure the right path. `main.py` logs `pytesseract.get_languages()` at startup, which is the fastest way to diagnose a broken image build from the run log. Pages are rasterized with `pypdfium2`, not PyMuPDF (AGPL) and not `pdf2image` + poppler (an extra system dependency for no benefit).

Recommended run options: **4096 MB memory**, 3600 s timeout. Apify allocates roughly one CPU core per 4 GB, so at 2048 MB OCR runs on half a core — the same compute-unit cost for twice the wall clock.

### Other actors by amanatools

- [PDF Text Extractor](https://apify.com/amanatools/pdf-table-extractor) — tables and text from PDF files into clean JSON rows
- [ATS Job Scraper](https://apify.com/amanatools/ats-jobs-extractor) — open jobs straight from Greenhouse, Lever, Workday and more
- [Data Cleaner](https://apify.com/amanatools/data-cleaner) — messy CSV, Excel and JSON into clean, typed data
- [Doc to Markdown](https://apify.com/amanatools/doc-to-markdown) — DOCX, PDF and web pages into clean markdown

# Actor input Schema

## `pdf_urls` (type: `array`):

Direct links to the PDF files to process (up to 50 per run).

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

Which pages to read: 'all', a range like '1-5', or a list like '1,3,7-9'.

## `ocr_mode` (type: `string`):

Auto is the cheapest and the default: pages that already contain digital text are read directly, only image pages are OCR'd.

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

Language of the scanned text. 'Auto' detects Latin vs Arabic vs Cyrillic script from the first OCR'd page of each document.

## `preserve_layout` (type: `boolean`):

Return per-block bounding boxes, confidence and column index, plus a spatially laid-out plain-text rendering of each page. Turn off for smaller dataset items when you only need raw text.

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

Higher DPI is more accurate on small print and much slower. 200 suits most scans; use 300 for dense or small text.

## `auto_rotate` (type: `boolean`):

Detect sideways or upside-down scans and rotate before OCR.

## `min_word_confidence` (type: `integer`):

Drop OCR words below this confidence (0-100). Leave at 0 to keep everything and filter on the reported confidence yourself.

## `max_pages_per_document` (type: `integer`):

Hard cap on pages read per PDF — this is also your cost ceiling: max spend = $0.05 x documents + $0.02 x this number x documents. The default is deliberately modest so a full run finishes inside the run timeout; raise it for long scans.

## `include_failed_documents` (type: `boolean`):

If enabled, documents that fail to download or yield no text still appear in the dataset with their status and error.

## Actor input object example

```json
{
  "pdf_urls": [
    "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/v16.0.0/tests/resources/ccitt.pdf",
    "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf"
  ],
  "pages": "all",
  "ocr_mode": "auto",
  "language": "auto",
  "preserve_layout": true,
  "dpi": 200,
  "auto_rotate": true,
  "min_word_confidence": 0,
  "max_pages_per_document": 20,
  "include_failed_documents": true
}
```

# Actor output Schema

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

Default dataset: one item per PDF with url, status, page count, per-page blocks, confidence, detected script, and text\_layout.

# 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 = {
    "pdf_urls": [
        "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/v16.0.0/tests/resources/ccitt.pdf",
        "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("amanatools/pdf-ocr-extractor").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 = { "pdf_urls": [
        "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/v16.0.0/tests/resources/ccitt.pdf",
        "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("amanatools/pdf-ocr-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "pdf_urls": [
    "https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/v16.0.0/tests/resources/ccitt.pdf",
    "https://raw.githubusercontent.com/jsvine/pdfplumber/stable/examples/pdfs/ca-warn-report.pdf"
  ]
}' |
apify call amanatools/pdf-ocr-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=amanatools/pdf-ocr-extractor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/KgGvNdcBpEH1eU8ee/builds/Gl1hg3aBtAJhfnieH/openapi.json
