# Bulk OCR: Image & Scanned PDF to Text from CSV or Google Sheet (`nerolabs/bulk-ocr-image-pdf-to-text`) Actor

OCR every image and scanned PDF linked in an Apify dataset, CSV, Excel or Google Sheet, keeping your columns. Inputs: datasetId or fileUrl or fileUrls, urlField, languages. Adds text, confidence and page counts. Tesseract inside the run, no outside service. Charged per page. Agent-ready: x402, MCP.

- **URL**: https://apify.com/nerolabs/bulk-ocr-image-pdf-to-text.md
- **Developed by:** [Adam Pearce](https://apify.com/nerolabs) (community)
- **Categories:** Developer tools, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.50 / 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.
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?

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

## Bulk OCR: text out of every image and scanned PDF in your list

Got a spreadsheet of links to receipts, scanned letters, invoices, screenshots, forms or product photos, and no way to get the words in them into a column?

Point this Actor at an **Apify dataset, a CSV or Excel file, or a Google Sheet**. For every row it downloads the image or PDF, recognises the text, and hands you back **every original column with the text, a confidence score and page counts added alongside**. No retyping, no copying out of a viewer, no re-joining results to your own data afterwards.

Every other OCR Actor on the Store takes a typed-in list of image URLs and gives you back a bare list of text. If your links live in a spreadsheet, you have to pull the column out, paste it in, and stitch the answers back together by hand. This one takes the whole sheet, and it takes scanned PDFs as well as images.

### What you get per file

| Field | What it is |
|---|---|
| `text` | Everything recognised, in reading order, pages separated by a blank line |
| `confidence` | The engine's average confidence in the words it read, 0 to 100 |
| `lowConfidence` | True when that average is under 60, so a bad scan cannot pass as a good one |
| `fileType` | What the file really was: pdf, png, jpeg, webp, gif, bmp or tiff |
| `pageCount`, `pagesRead`, `pagesTruncated` | How big the file was and whether the per-file cap cut it |
| `ocrPages`, `textLayerPages`, `blankPages` | How each page was read, which is also what you were charged for |
| `wordCount`, `charCount` | Size of what came back |
| `ocrStatus`, `statusDetail` | What happened, in plain English, when it did not work |
| `languages`, `fileSizeKb`, `resolvedUrl`, `ocrMs` | The settings and facts behind the row |
| `pages` | Optional: each page's text, method and confidence separately |

Plus every column that was already in your row.

### How it reads

- **Images** (PNG, JPEG, WebP, GIF, BMP, TIFF) are flattened onto white, enlarged when they are small (small text reads far better doubled) and capped at 4,000 pixels on the longest side, then recognised.
- **Scanned PDFs** are rendered page by page at the resolution you choose (200 dpi by default) and recognised page by page, up to your per-file page cap.
- **Born-digital PDFs** already contain their text. Those pages are copied exactly rather than recognised, at a fifth of the price, unless you switch `usePdfTextLayer` off. A mixed document gets the right treatment page by page.

The engine is Tesseract, the open-source OCR engine, running as WebAssembly **inside the run**. No outside OCR service, no API key, and no third party ever sees your files.

### The honest limit

OCR is not magic. Tesseract reads clean scans, screenshots, receipts and printed documents well. It reads photographs of documents less well, small or decorative type worse, and handwriting badly. Rather than hide that, every file carries its `confidence`, files under 60 are flagged `lowConfidence`, and the run summary counts them. When results disappoint:

- set **Languages** to the document's language (the single biggest factor; English is built in, 100+ others download once at the start of the run),
- raise **PDF render resolution** to 300 for small print,
- switch **Page layout** to *Sparse* for screenshots, slides and product photos, or *Single column* for narrow receipts,
- use **Minimum word confidence** to drop the engine's guesses and keep only what it is sure of.

### What it costs

- **$0.005 per page read by OCR** (an image is one page). A page that produced no words at all is free.
- **$0.001 per PDF page copied from its own text layer.**
- $0.01 per export file, $0.02 per webhook delivery.
- Bronze, Silver and Gold Store discounts apply from day one.

Never charged: unreachable links, 404s, bot-blocked downloads, HTML pages, HEIC or SVG files, corrupt or password-protected PDFs, files over your size limit, blank cells, and blank pages.

- 1,000 receipt photos a month: **$5.00**
- 200 scanned contracts of 10 pages: **$10.00**
- A nightly run over 50 new scanned delivery notes: about **$7.50 a month**

`maxPagesPerFile` (default 20) is your ceiling per document: a 300-page scan cannot run up a bill you did not expect.

### Inputs

Give it any one of these:

- **Dataset** picked from your account, for example a scraper's output with a photo column
- **File or Google Sheet URL**: CSV, TSV, Excel, JSON or JSON Lines. A normal Google Sheets link works, shared as "Anyone with the link can view"
- **Image or PDF URLs**: a plain list, for a quick one-off
- **Rows**: inline JSON

The link column is detected automatically, preferring a column whose values end in `.jpg`, `.png` or `.pdf` over one merely named `url`. Set `urlField` if you would rather be explicit. Google Drive and Dropbox share links are converted to direct downloads.

Useful options: `languages`, `dpi`, `pageSegmentation`, `maxPagesPerFile`, `minWordConfidence`, `includePageText` for per-page text you can cite a page from, `keep: "problems"` to find every broken link in a large list, and `keep: "no_text"` to find the blank or unreadable files.

### Speed

Measured on Apify at the default 2 GB of memory: a receipt or screenshot image takes about 1 to 2 seconds, a scanned A4 page rendered at 200 dpi about 2 to 3 seconds (roughly double at 300 dpi), and a born-digital PDF page copied from its text layer well under a tenth of a second. The default example (four files, four OCR pages, six text-layer pages) finishes in about 8 seconds including start-up. Two files are read at once by default; raise memory before raising `concurrency`, since each slot runs its own engine.

### Exports and pipelines

- `exportFormats` writes a real downloadable **CSV or Excel** file. Per-page text is JSON-encoded into a single cell so it fits a spreadsheet.
- `outputDatasetName` appends every run into one named dataset, building a growing text archive. Not charged again.
- `webhookUrl` POSTs the run summary to Slack, Zapier, Make, n8n or your own API the moment the run finishes. Charged only on a confirmed 2xx.

### FAQ

**Does it handle PDFs that already have text?** Yes. Those pages are copied from the text layer at the low rate. If all your PDFs are born-digital and you want tables rebuilt too, [Dataset PDF Extract](https://apify.com/nerolabs/dataset-pdf-extract) is the better tool; it flags the scans it cannot read, and this Actor is where you send them.

**Which languages?** Any Tesseract language: `eng`, `deu`, `fra`, `spa`, `por`, `ita`, `nld`, `pol`, `rus`, `ukr`, `tur`, `jpn`, `kor`, `chi_sim`, `chi_tra`, `ara`, `hin` and about a hundred more. List several for mixed documents. Only English ships inside the Actor; others are fetched once when the run starts.

**Handwriting?** Expect poor results. Tesseract is a print engine. The confidence score will tell you.

**HEIC photos from an iPhone?** Not supported, and reported as such without charge. Convert to JPEG first.

**Can it read a photo of a receipt?** Usually, if the photo is sharp, flat and well lit. Check `confidence`; try `pageSegmentation: "single_column"` for narrow receipts.

**Can I feed the text straight into an LLM?** Yes. Pair it with [Dataset AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich) to turn the recognised text into typed columns (total, date, supplier) with one plain-English instruction.

**Does filtering make a run cheaper?** No. The file has to be fetched and read before we know whether it matches, so `keep` changes what is written, not what is charged.

**Is my data safe?** The Actor reads the files you point it at and writes the results to your own dataset. Recognition happens inside the run; nothing is sent to an OCR service. If you set a webhook, the run summary goes to the URL you chose and nowhere else.

### The rest of the toolkit

Sibling Actors that fit together as a pipeline:

- [Dataset PDF Extract](https://apify.com/nerolabs/dataset-pdf-extract) text and tables out of born-digital PDFs
- [Dataset Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter) dedupe, flatten, clean, export
- [Dataset Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform) filter by rule, rename, compute, cast
- [Dataset Join & Merge](https://apify.com/nerolabs/dataset-join-merge) VLOOKUP for datasets
- [Dataset Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot) SQL GROUP BY and pivot tables
- [Dataset Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector) what changed since last time
- [Dataset AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich) one instruction, typed output columns
- [Dataset Charts & Report](https://apify.com/nerolabs/dataset-charts-report) chart images and a PDF report
- [Dataset to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database) push rows into a real database
- [Dataset to REST API](https://apify.com/nerolabs/dataset-to-rest-api) one HTTP request per row
- [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner) chain any of these in one call

### Notes

Recognition uses [tesseract.js](https://github.com/naptha/tesseract.js) (Apache-2.0), PDF pages are rendered by Mozilla's [pdf.js](https://github.com/mozilla/pdf.js) (Apache-2.0) through [unpdf](https://github.com/unjs/unpdf) (MIT), and images are decoded by [@napi-rs/canvas](https://github.com/Brooooooklyn/canvas) (MIT). The sample files in the default input are synthetic documents made for this Actor; no real people or businesses.

If this saved you retyping a pile of scans, a review on the Apify Store helps a lot. Found a file it read badly? Open an issue with the link and I will look at it.

# Actor input Schema

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

An Apify dataset holding one row per image or PDF, for example the output of a scraper that collected photos, receipts or document links. Every original column is kept and the recognised text, confidence and page counts are added alongside. Use the picker rather than typing an ID.

## `fileUrl` (type: `string`):

A public link to a CSV, TSV, Excel, JSON or JSON Lines file holding one row per image or PDF. A normal Google Sheets link works: share it as 'Anyone with the link can view'. Used when no dataset is given.

## `fileFormat` (type: `string`):

Leave on 'Detect automatically' unless the link has no file extension and the server reports the wrong content type.

## `sheetName` (type: `string`):

Which sheet to read from an Excel workbook. Defaults to the first sheet.

## `fileUrls` (type: `array`):

A plain list of image (PNG, JPEG, WebP, GIF, BMP, TIFF) or PDF links, for a quick one-off run with no spreadsheet. Use the dataset, file or Google Sheet inputs above to keep your own columns alongside the results.

## `data` (type: `array`):

Rows as inline JSON, an alternative to a dataset or file. Each object needs a field holding the image or PDF link.

## `urlField` (type: `string`):

The column holding the image or PDF link. Left empty it is detected automatically, preferring a column whose values end in .jpg, .png or .pdf over one merely named 'url'.

## `languages` (type: `array`):

Tesseract language codes, one per line, for example eng, deu, fra, spa, por, ita, nld, pol, rus, jpn, chi\_sim, chi\_tra, ara, hin, kor. Several codes read mixed documents. English is built in; any other language is downloaded once at the start of the run (5 to 15 MB each). Picking the right language is the single biggest factor in accuracy.

## `usePdfTextLayer` (type: `boolean`):

A born-digital PDF page already contains its text. When this is on, such pages are copied exactly and charged at the low text-layer rate instead of being rendered and recognised. Turn it off to force OCR on every page, for example when a PDF's text layer is known to be garbage.

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

Resolution used to render PDF pages before recognition. 200 suits most scans; 300 helps small print and takes about twice as long; 150 is faster for large clean type. Images are used at their own resolution, enlarged when small and capped at 4000 pixels on the longest side.

## `pageSegmentation` (type: `string`):

How the engine should look for text. 'Automatic' handles documents, receipts and letters. 'Sparse' finds scattered text on screenshots, slides and product photos. 'Single block' suits a cropped paragraph, 'single column' a narrow receipt, 'single line' a cropped field.

## `maxPagesPerFile` (type: `integer`):

Pages read from each PDF before stopping. This is also your cost ceiling per file: a 300-page scan cannot run up a bill you did not expect. Images always count as one page.

## `minWordConfidence` (type: `integer`):

Drop words the engine is less sure about than this, from 0 to 100. Leave at 0 to keep everything and judge by the reported confidence instead. Raising it cuts noise out of poor scans at the cost of real words.

## `includePageText` (type: `boolean`):

Add a 'pages' array with each page's text, method (OCR or copied text layer), confidence and word count, on top of the combined text. Useful for citing a page number, and it makes rows larger.

## `keep` (type: `string`):

Filtering happens after the file has been fetched and read, so it does not make a run cheaper. 'Problems only' is the quick way to find broken links and unsupported files in a large list.

## `keepOriginalFields` (type: `boolean`):

Keep every column from the input row next to the recognised text, so results line up with your own data. Turn off for text and counts only.

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

How many files to recognise in parallel. Each slot runs its own OCR engine, so the default of 2 fits the default 2 GB of memory; raise memory before raising this.

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

How long to wait for one file before giving up on it. A timed-out file is never charged.

## `maxFileMb` (type: `integer`):

Files bigger than this are skipped and not charged, rather than spending the run's memory on one enormous scan.

## `maxItems` (type: `integer`):

A hard ceiling on how many rows are read from the input, as a safety net on a large dataset.

## `exportFormats` (type: `array`):

Also write the results as a real downloadable CSV or Excel file, linked from the run's output. Per-page text is JSON-encoded into a single cell so it fits a spreadsheet.

## `outputDatasetName` (type: `string`):

Also append every kept row to a named dataset that persists across runs, building one growing text archive. Not charged again.

## `webhookUrl` (type: `string`):

POST the run summary to this URL when the run finishes, for Slack, Zapier, Make, n8n or your own API. Charged only on a confirmed 2xx response.

## Actor input object example

```json
{
  "fileFormat": "auto",
  "fileUrls": [
    "https://nerolabs-samples.nerolabs.workers.dev/sample-receipt.png",
    "https://nerolabs-samples.nerolabs.workers.dev/sample-scanned-letter.pdf",
    "https://nerolabs-samples.nerolabs.workers.dev/sample-screenshot.png",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "languages": [
    "eng"
  ],
  "usePdfTextLayer": true,
  "dpi": 200,
  "pageSegmentation": "auto",
  "maxPagesPerFile": 20,
  "minWordConfidence": 0,
  "includePageText": false,
  "keep": "all",
  "keepOriginalFields": true,
  "concurrency": 2,
  "requestTimeoutSecs": 60,
  "maxFileMb": 25,
  "exportFormats": []
}
```

# Actor output Schema

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

Every original row with the recognised text, confidence, page counts and file type added.

## `ocrSummary` (type: `string`):

Statuses, page and word totals, average confidence, low-confidence files, and the data note.

# 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 = {
    "fileUrl": "",
    "fileUrls": [
        "https://nerolabs-samples.nerolabs.workers.dev/sample-receipt.png",
        "https://nerolabs-samples.nerolabs.workers.dev/sample-scanned-letter.pdf",
        "https://nerolabs-samples.nerolabs.workers.dev/sample-screenshot.png",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
    ],
    "languages": [
        "eng"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/bulk-ocr-image-pdf-to-text").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 = {
    "fileUrl": "",
    "fileUrls": [
        "https://nerolabs-samples.nerolabs.workers.dev/sample-receipt.png",
        "https://nerolabs-samples.nerolabs.workers.dev/sample-scanned-letter.pdf",
        "https://nerolabs-samples.nerolabs.workers.dev/sample-screenshot.png",
        "https://www.irs.gov/pub/irs-pdf/fw9.pdf",
    ],
    "languages": ["eng"],
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/bulk-ocr-image-pdf-to-text").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 '{
  "fileUrl": "",
  "fileUrls": [
    "https://nerolabs-samples.nerolabs.workers.dev/sample-receipt.png",
    "https://nerolabs-samples.nerolabs.workers.dev/sample-scanned-letter.pdf",
    "https://nerolabs-samples.nerolabs.workers.dev/sample-screenshot.png",
    "https://www.irs.gov/pub/irs-pdf/fw9.pdf"
  ],
  "languages": [
    "eng"
  ]
}' |
apify call nerolabs/bulk-ocr-image-pdf-to-text --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nerolabs/bulk-ocr-image-pdf-to-text"
        }
    }
}
```

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/gzzNKKyLzA7Z0ZSMc/builds/o5kUzF6v7ezTQRX7Y/openapi.json
