# Image to Text & Scanned PDF OCR to RAG Chunks (`mmdincer/image-ocr-rag`) Actor

Image to text OCR for photos and scanned PDFs: text, Markdown and token-counted chunks with page numbers and a confidence score. Tesseract, 37 languages, no API key. Blank pages and failed files are free. Not affiliated with any OCR vendor.

- **URL**: https://apify.com/mmdincer/image-ocr-rag.md
- **Developed by:** [Muhammet Mustafa Dincer](https://apify.com/mmdincer) (community)
- **Categories:** AI, Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

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

![Image to text and scanned PDF OCR: a scanned memo becomes token-counted RAG chunks with page numbers and a confidence score](https://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-banner.png?signature=pyuEsa0TUVp4El13tNcE)

### What is Image to Text & Scanned PDF OCR to RAG Chunks?

**Image to Text & Scanned PDF OCR to RAG Chunks** reads text from an image or a scanned PDF. Give it links to photos, screenshots, scans or scanned PDFs and you get back:

- **one row per file** (`recordType: "document"`) with the recognised text, LLM-ready **Markdown** with headings and lists, page counts, word count, **token count** and an overall **confidence** score;
- **one row per chunk** (`recordType: "chunk"`) with the chunk text, its **token count**, the **page numbers** it came from and its **own confidence score**, so your pipeline can cite a page and skip a chunk that was read badly.

It runs the open-source Tesseract OCR engine inside the Actor. There is **no API key, no OCR account and no third-party service**: the file is downloaded over plain HTTP and recognised on the machine that runs the Actor.

### Quick answer: when should you (or an AI agent) pick this Actor?

**Pick this Actor when** the text you need is trapped in an image: a scanned contract, a photographed receipt, a screenshot of a table, a fax, an old manual, a folder of invoices exported as JPEG, or a PDF that is only pictures of pages. It is also the right tool when the next step is retrieval (RAG): the chunk rows are already inside a token budget, tagged with page numbers, and carry a confidence score you can filter on.

**Pick something else when** your PDF has a real text layer (one made from Word or LaTeX): it is cheaper and exact to read the text layer directly, for example with [PDF to Markdown & RAG Chunks Extractor](https://apify.com/mmdincer/pdf-rag-extractor). Pick something else for **handwriting**, for pages that must keep a complex table structure, or when you need a cloud vision model's understanding of a picture rather than the characters in it.

**Defaults that work:** send `{"urls": ["https://example.com/scan.jpg"]}` and you get a document row and chunks of about 512 tokens, in a couple of seconds per page. For a vector store, send `{"urls": [...], "mode": "chunks", "chunkSize": 512, "chunkOverlap": 64}` and the dataset is exactly what you feed to embeddings. For German scans add `"languages": ["deu"]`; for a mix, `"languages": ["eng", "deu"]`.

### At a glance

| | |
|---|---|
| **Does** | Downloads images and PDFs, straightens and cleans each page, runs OCR, and returns text, Markdown, quality numbers and token-counted chunks with page ranges |
| **Input** | `urls` (or `datasetId` to read links from another dataset). Optional: `languages`, `mode`, `chunkSize`, `chunkOverlap`, `lowConfidenceBelow`, `startPage`, `maxPagesPerFile`, `pdfDpi`, `enhance`, `autoRotate`, `includeMarkdown`, `includePages`, `maxFiles`, `maxFileSizeMb`, `maxChunksPerFile`, `requestsPerSecond` |
| **Output** | `recordType: "document"` rows with `text`, `markdown`, `confidence`, `quality`, `tokenCount`, page counts, status; `recordType: "chunk"` rows with `text`, `tokenCount`, `pageStart`, `pageEnd`, `headings`, `confidence`, `lowConfidence` |
| **File types** | JPEG, PNG, WebP, GIF, BMP, TIFF (also multi-page) and PDF |
| **Languages** | 37, up to 4 per run: English, German, French, Spanish, Italian, Portuguese, Dutch, Turkish, Russian, Ukrainian, Polish, Czech, Swedish, Norwegian, Danish, Finnish, Romanian, Hungarian, Bulgarian, Croatian, Serbian, Slovak, Catalan, Estonian, Latvian, Lithuanian, Hebrew, Arabic, Hindi, Bengali, Indonesian, Vietnamese, Thai, Japanese, Korean, Chinese (Simplified and Traditional) |
| **Needs** | No login, no API key, no browser, no proxy. Only a public http(s) link that returns the file itself |
| **Speed** | Measured: a photographed memo in 1.2 s, a two-page scanned PDF in 2 s; 12 dense A4 pages in 46 to 67 s on 2 GB |
| **Pricing** | Pay per event: **$0.004 per OCR page** and $0.0005 per GB of memory at start on the Free plan (cheaper on paid plans). Blank pages, failed downloads and unreadable files are free |
| **Does not** | Read handwriting reliably, reconstruct tables as Markdown tables, open password-protected PDFs, or follow a link that shows a viewer page instead of the file |

### Key terms used in this README

- **OCR (optical character recognition):** turning the picture of text into text you can search, copy and embed.
- **Scanned PDF:** a PDF whose pages are pictures. It has no selectable text, so a normal PDF parser returns nothing; OCR is the only way to read it.
- **Document row:** a dataset row with `recordType: "document"`. One per file, always written, even when the file failed — the reason is in the row.
- **Chunk row:** a dataset row with `recordType: "chunk"`. A slice of the document of roughly `chunkSize` tokens, with `pageStart`, `pageEnd` and a `confidence`.
- **Confidence:** the engine's own estimate (0 to 100) of how sure it is about the words it read, averaged over the page and weighted by word length. It is a signal for ranking and filtering, not a measured accuracy.
- **Token count:** the number of tokens the text becomes in **cl100k_base**, the tokenizer used by OpenAI's embedding and chat models. It is counted, not estimated from characters.
- **Overlap:** how much of the end of one chunk is repeated at the start of the next, so a sentence that straddles a boundary is still retrievable.
- **OCR page:** the billing unit. One image is one page; a PDF is one page per PDF page that contained text.

### What can you use it for?

#### Searchable archives of scanned documents

Turn a shared drive of scanned letters, contracts and forms into text you can search. Keep the document rows and you have a full-text dump with `fileName`, `pageCount`, `quality` and `confidence` as filters.

#### RAG over documents that are only pictures

The `mode: "chunks"` output is a ready-made embedding input: one row per chunk, inside your token budget, with the source URL and page range as metadata. Store `pageStart`/`pageEnd` next to the vector and your answer can cite "page 2" instead of guessing.

#### Receipts, invoices and forms

Small, dense documents read best with a smaller chunk size (256) and `languages` set to the language printed on the page. The Markdown keeps headings and list items on separate lines, which is what a downstream extraction prompt wants.

#### Screenshots and photographs of text

Slides, whiteboard photos, signs, error dialogs and screenshots of chat or code. Small images are enlarged before recognition and photographs are straightened by their EXIF orientation, so a phone photo that is sideways still reads.

#### Agents that can see files but not read them

Point an agent at this Actor through MCP, give it an image URL, and it receives plain text with a confidence it can reason about instead of a binary it cannot open. Chunk rows are flat and identical in shape to the other RAG Actors from this developer.

#### Crawl, then read what you crawled

Run a crawler or screenshot Actor into a dataset, then pass its **dataset ID** in `datasetId`. The Actor finds the image and PDF links in the rows and returns text; you never copy links by hand.

### How does it work?

1. The URL list is built from your `urls` and, if you set one, from the links found in `datasetId`. Duplicates are removed and every URL is validated; non-http links and links with a username and password are rejected.
2. Each file is downloaded directly, with a timeout, a size cap and a retry for transient errors. A 403 or 429 is treated as a refusal: the file is reported as `blocked`, **no more files are requested from that host**, and the Actor moves on rather than trying to get around it.
3. The file type is detected from the bytes, not the URL, so a `.jpg` that is really HTML is reported as unsupported instead of crashing.
4. Each page is prepared: EXIF rotation is applied, transparency is flattened onto white, the page is turned grey, its contrast is stretched and small images are enlarged. PDFs are drawn page by page at `pdfDpi` (200 by default).
5. Tesseract reads the page. If the result is poor, the Actor checks whether the page is sideways or upside-down, turns it and reads it again. This costs nothing extra.
6. The word positions are turned into Markdown: lines become paragraphs, large text becomes `#` and `##` headings, bullets and numbered items become lists and hyphenated line breaks are joined.
7. Pages are packed into chunks until the token budget is reached, and every chunk gets its own confidence. A page with no text, or one that reads as noise, is marked `blank` or `unreadable` and is **not charged**.
8. Rows are written. Only pages with text are charged; chunk rows are free, so chunk size never changes the bill.

### How to OCR an image or scanned PDF (step by step)

1. Open the Actor and paste one or more **image or PDF URLs** in the input, one per line, for example `https://example.com/scan.jpg`.
2. Set **Languages** to the language printed on the page. English is the default; add a second language only if the page really mixes two.
3. Leave **What to return** on *Document and chunks* for a first look, or choose *Chunks only* if you are feeding embeddings.
4. Set **Chunk size** to 512 for general retrieval. Use 256 for receipts and narrow facts and 1024 or more for long arguments.
5. For a long PDF, set **First page** and **Maximum pages per file** so you only pay for the pages you need.
6. Click **Start**. In the **Output** tab open the **Documents** view to read the text and quality, or the **Chunks** view for the embedding input.
7. Download the dataset as JSON, JSONL, CSV or Excel, or read it from the API; `recordType` tells the two kinds of row apart.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `urls` | list of text | two example files | Direct links to image files or PDFs, up to 200 per run. A link that shows a web page instead of the file is reported as `unsupported_type` |
| `datasetId` | text | empty | Read links from an existing Apify dataset. Looks for `imageUrl`, `fileUrl`, `pdfUrl`, `screenshotUrl`, `url`, `link` and similar fields |
| `languages` | list of choices | `eng` | Up to 4 languages that appear in the text. The first matters most. Each extra language slows recognition |
| `mode` | choice | `both` | `both` writes document and chunk rows; `chunks` writes only chunks; `document` skips chunking |
| `chunkSize` | number | 512 | Token budget per chunk (64–4096), counted in cl100k_base |
| `chunkOverlap` | number | 64 | Tokens repeated from the previous chunk, in whole paragraphs, at most half of `chunkSize` |
| `lowConfidenceBelow` | number | 70 | Chunks under this confidence get `lowConfidence: true`. Nothing is removed |
| `startPage` | number | 1 | First page of a PDF or multi-page TIFF to read, 1-based |
| `maxPagesPerFile` | number | 50 | Pages read from one file, up to 200 |
| `pdfDpi` | number | 200 | Resolution a PDF page is drawn at (100–300). Use 300 for small type, 150 for faster reads |
| `enhance` | yes/no | yes | Grey, contrast and enlarge before recognition. Turn off only for clean scans at a good size |
| `autoRotate` | yes/no | yes | Fix sideways and upside-down pages when the first read is poor |
| `includeMarkdown` | yes/no | yes | Add the `markdown` field to the document row |
| `includePages` | yes/no | no | Add a `pages` array with status, confidence, size and text of every page |
| `maxFiles` | number | 100 | Upper bound on files read in one run |
| `maxFileSizeMb` | number | 50 | Skip files larger than this instead of downloading them |
| `maxChunksPerFile` | number | 1000 | Safety cap on chunk rows for one file |
| `requestsPerSecond` | number | 2 | Download rate per host. Lower it to 1 when every file comes from one small server |
| `concurrency` | number | by memory | Pages read at the same time. Default is about one per GB of run memory |
| `proxyConfiguration` | proxy | off | Only needed when an image host blocks data-centre IPs |

### Output

Every run writes one dataset with two kinds of rows. Filter on `recordType`.

**Document row.** Real output for a photographed memo (text and Markdown shortened here):

```json
{
  "recordType": "document",
  "url": "https://example.com/memo.jpg",
  "fileName": "ocr-sample-memo.jpg",
  "status": "processed",
  "failureReason": null,
  "message": null,
  "fileType": "image",
  "contentType": "image/jpeg",
  "fileSizeBytes": 208663,
  "pageCount": 1,
  "pagesProcessed": 1,
  "pagesWithText": 1,
  "pagesWithoutText": 0,
  "pagesNotProcessed": 0,
  "languages": "eng",
  "confidence": 93.3,
  "quality": "good",
  "charCount": 930,
  "wordCount": 156,
  "tokenCount": 207,
  "chunkCount": 1,
  "text": "Harbor Lane Cooperative\n\nOperations memo: winter dispatch schedule\n\nTo all depot leads. From 3 December…",
  "markdown": "# Harbor Lane Cooperative\n\n## Operations memo: winter dispatch schedule\n\nTo all depot leads. From 3 December…",
  "pages": null,
  "durationMs": 1184,
  "processedAt": "2026-11-04T11:21:56.607Z"
}
```

**Chunk row.** From a run with chunk size 128:

```json
{
  "recordType": "chunk",
  "url": "https://example.com/memo.jpg",
  "documentTitle": "ocr-sample-memo.jpg",
  "chunkIndex": 1,
  "text": "## Why we are doing this\n\nLast winter 41 percent of late deliveries were caused…",
  "tokenCount": 89,
  "charCount": 391,
  "pageStart": 1,
  "pageEnd": 1,
  "headings": ["Why we are doing this"],
  "isPartial": false,
  "confidence": 95.4,
  "lowConfidence": false
}
```

![Sample output: document rows with page and confidence, and chunk rows with page ranges, token counts and per-chunk confidence](https://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-output.png?signature=sVudMtNLZCk1RDI8CMwq)

Field notes:

- `status` is `processed`, `empty` or `failed`. Only pages with text inside a `processed` file are charged.
- `failureReason` when a row is not processed: `invalid_url`, `unsupported_type`, `too_large`, `blocked`, `stopped_after_block`, `http_error`, `network_error`, `password_protected`, `corrupt`, `ocr_error`, `budget_reached` or `no_text_found`.
- `confidence` on the document is the mean over its pages with text; `quality` is a plain band for it: `good` (85 or more), `fair` (60 to 84), `poor` (under 60). The bands are ours, not the engine's.
- A page with fewer than three letters or digits is `blank`; a page whose confidence is under 25 is `unreadable`. Both are listed in `pagesWithoutText` and are free.
- `pagesNotProcessed` counts pages cut off by `startPage` or `maxPagesPerFile`.
- `pageStart` and `pageEnd` on a chunk are the pages of the text inside it. A chunk that spans a page break reports both, which is why the citation is a range.
- `headings` lists the nearest heading before the chunk plus any heading inside it. Headings are inferred from text height, so they are right for typical memos, reports and forms and are a hint rather than a guarantee.
- `isPartial` is `true` when a paragraph had to be cut to respect the chunk size.
- `text` and `markdown` are `null` when the file failed or the option is off. `pages` is `null` unless `includePages` is on.
- Page headers and footers are **not removed**: a "page 1 of 2" footer at the foot of a page appears in the text where it sits on the page.

### How much does it cost?

You pay per OCR page, never per chunk, so changing the chunk size never changes the bill.

| Event | When | Free plan price |
|---|---|---|
| Actor start | once per run, one event per GB of memory (2 GB by default) | $0.0005 per event |
| OCR page | each page in which text was found and recognised | $0.004 |

Paid Apify plans get a lower per-page price: $0.0036 (Bronze), $0.0032 (Silver) and $0.0028 (Gold and above). Blank pages, pages that read as noise, failed and blocked downloads, unsupported and corrupt files, and pages outside your `startPage`/`maxPagesPerFile` window are **free**.

**Examples**

- One receipt photo: $0.001 + $0.004 = **$0.005**.
- A 20-page scanned contract: $0.001 + 20 × $0.004 = **$0.081**.
- 1,000 single-page scans: $0.001 + 1,000 × $0.004 = **$4.00**.
- 1,000 scans of which 100 are blank or unreadable: $0.001 + 900 × $0.004 = **$3.60**.

Set a **maximum cost per run** in the run options to cap spending. The Actor checks the remaining budget before each file and stops cleanly at the limit; files it did not reach are reported as `budget_reached` and cost nothing.

**Measured cost.** Runs on this Actor, 2026-11-04, default 2 GB of memory:

| Run | Pages | Chunks | Wall clock | Platform usage |
|---|---|---|---|---|
| Example input (1 photo, 1 two-page PDF) | 3 | 2 | 10.6 s | $0.0012 |
| 12 dense A4 pages, 2 pages at a time | 12 | — | 67 s | $0.0076 |
| 12 dense A4 pages, 3 pages at a time | 12 | — | 46 s | $0.0053 |

Recognition is CPU-bound, so the platform cost is about **$0.0004 to $0.0007 per page** on a dense page, with some variation from run to run. That is roughly 10 to 18% of the $0.004 list price on the Free plan.

> Note on the numbers above: they were measured on runs on this Actor. As the Actor's owner, my own runs are not billed to a customer, so "list price" is what a customer would pay, not money this Actor has earned.

### Image to text and scanned PDF OCR compared with other approaches

| | This Actor | Other OCR Actors in the Store | Your own Tesseract setup | Cloud OCR API |
|---|---|---|---|---|
| Output | Text, Markdown, token-counted chunks | Usually text only | You build it | Text and layout, JSON |
| Page numbers on every chunk | Yes | Rarely | You build it | Per page |
| Confidence per chunk, with a flag | Yes (`lowConfidence`) | Rarely | Raw per-word numbers | Per word or line |
| Blank pages and failures charged | No | Varies | Your time | Usually yes |
| Reads links from another dataset | Yes, `datasetId` | Sometimes | You build it | You build it |
| Needs an API key or cloud account | No | No | No | Yes |
| Handwriting | No | Mostly no | No | Often better |
| Price per 1,000 pages | $4.00 on the Free plan | About $2 to $15 | Your servers and time | About $1.50 and up |

Several image and PDF OCR Actors already exist, so this is not an empty niche — compare before you pick. Where this one is different is narrower than "we are the only ones": the output is shaped for retrieval (token-budgeted chunks with page ranges, the same row format as this developer's other RAG Actors), every chunk carries a **confidence you can filter on**, pages that are blank or unreadable are **not charged**, and a sideways or upside-down page is turned and read again automatically. It uses an open-source engine, so on handwriting and very poor photographs a cloud vision service will read better; this Actor is for printed text.

Competitor prices change; the figures above are from the Store listings on 2026-11-04 and are a range, not a quote.

### Tips for best results

- **Set the language.** The wrong language is the most common reason for poor reads. Pick what is on the page; add a second only for genuinely mixed pages.
- **Use 300 DPI for small type.** `pdfDpi: 300` is slower and reads six-point print better; 150 is faster for large, clean headings.
- **Keep `enhance` on** unless your images are already clean scans at 300 DPI. Phone photos and small screenshots gain the most.
- **Filter on `lowConfidence`.** Drop or review chunks under your threshold before embedding; a badly read chunk is worse than a missing one.
- **Use `mode: "chunks"`** for a production embedding job: the dataset then contains only chunk rows and your loader needs no filter.
- **Skip the cover and the index** of long PDFs with `startPage`; each page with text is one charged page.
- **Look at `quality`** on the document rows after a first small run before you pay for a thousand files.

### Limits and good to know

- **Printed text only.** Handwriting is read poorly or not at all. Confidence will be low and `lowConfidence` will say so.
- **Not a table extractor.** Table cells are read as text lines; the content is there but the grid is not rebuilt as a Markdown table.
- **Multi-column pages** are read in the engine's layout order, which is correct for most reports and papers and can interleave columns on busy magazine layouts.
- **Repeated headers, footers and page numbers are left out** when the same short line sits at the top or bottom of at least half the pages (two or more). A one-page image is unchanged, so a title is not removed. The document row says how many lines were left out.
- **Password-protected PDFs are not opened.** They are reported as `password_protected` and are not charged.
- **A viewer link is not a file link.** If the URL returns an HTML page, the row says `unsupported_type`; open the link in a browser, use the download button and copy the direct file URL.
- **Size limits.** Up to 200 pages per file, 100 MB per file (50 MB by default) and images are shrunk when a side exceeds 4,500 pixels.
- **Confidence is an estimate**, not a measured accuracy. A clean scan scores 90 or more; a blurred phone photo can score 60 and still be mostly right.
- **Text is not summarised, translated or corrected.** What the engine reads is what you get, including its mistakes.

### Use it from AI agents, the API and code

**Apify MCP server (ChatGPT, Claude, Cursor and other MCP clients).** Connect `https://mcp.apify.com`, then:

1. `search-actors` with a query such as `image OCR to text RAG chunks`.
2. `fetch-actor-details` for `mmdincer/image-ocr-rag` to read the input schema.
3. `call-actor` with the input below.
4. `get-dataset-items` with the returned dataset ID and `recordType: "chunk"` as your filter.

```json
{
  "actor": "mmdincer/image-ocr-rag",
  "input": {
    "urls": ["https://example.com/scan.jpg"],
    "languages": ["eng"],
    "mode": "chunks",
    "chunkSize": 512,
    "chunkOverlap": 64
  },
  "callOptions": { "maxTotalChargeUsd": 0.5 }
}
```

To give an MCP client this Actor as its own tool (no search step), connect `https://mcp.apify.com?tools=mmdincer/image-ocr-rag`.

**JavaScript**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('mmdincer/image-ocr-rag').call(
    { urls: ['https://example.com/scan.jpg'], mode: 'chunks', chunkSize: 512 },
    { maxTotalChargeUsd: 0.5 },
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const reliable = items.filter((row) => row.recordType === 'chunk' && !row.lowConfidence);
```

**Python**

```python
import os
from decimal import Decimal
from apify_client import ApifyClient

client = ApifyClient(token=os.environ['APIFY_TOKEN'])
run = client.actor('mmdincer/image-ocr-rag').call(
    run_input={'urls': ['https://example.com/scan.jpg'], 'mode': 'chunks', 'chunkSize': 512},
    max_total_charge_usd=Decimal('0.5'),
)
items = client.dataset(run['defaultDatasetId']).list_items().items
reliable = [row for row in items if row['recordType'] == 'chunk' and not row['lowConfidence']]
```

**HTTP (one request, returns the rows)**

```bash
curl -X POST "https://api.apify.com/v2/acts/mmdincer~image-ocr-rag/run-sync-get-dataset-items?maxTotalChargeUsd=0.5" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://example.com/scan.jpg"], "mode": "chunks", "chunkSize": 512}'
```

The synchronous endpoint waits for the run, so use it for a handful of files. For a large batch, start the run asynchronously and read the dataset when it finishes.

### Automation recipes

**Scanned archive to vector store.** Save the input as a task. After each run, send the `chunk` rows that are not `lowConfidence` to your embedding endpoint and upsert them keyed on `url` + `chunkIndex`.

**Crawl, then OCR.** Run a crawler or screenshot Actor into a dataset, then run this Actor with `datasetId` set to that dataset's ID. Image links are found in the rows and the text lands in a new dataset.

**Review queue in a spreadsheet.** Fetch the dataset as CSV and sort by `confidence`. `status`, `failureReason` and `quality` make it obvious which files need a better scan before you rely on them.

**Zapier or Make.** Use Apify as the trigger for a finished task run, add the step that gets dataset items, filter `recordType` equal to `chunk`, then send the rows on. See the [Apify Zapier integration](https://docs.apify.com/integrations/zapier) and the [Apify Make integration](https://docs.apify.com/integrations/make).

**Budget guard.** Set a maximum cost per run on the task. The Actor stops with `stoppedForBudget: true` in the run summary instead of overspending.

### Is it legal to OCR images and PDFs?

The Actor downloads the files you point it at, one at a time, at a low request rate, with a normal browser User-Agent. It does not crawl a site on its own, does not guess URLs, does not log in, does not read anything behind a paywall, and does not try to get around a 403, a 429 or a bot challenge: it reports the refusal in the dataset and stops asking that host.

That makes the legal question mostly about **the files you choose**, not about the tool. Documents you own, documents your organisation produced, public-domain material and openly licensed images are the normal case. An image you are not allowed to copy is not made lawful by recognising its text. Check the terms of the site you take it from and the licence of the document itself.

**Personal data.** Scans of IDs, medical records, payslips and similar documents contain personal data. The text goes to your dataset in your Apify account and nowhere else, but you remain responsible for having a lawful basis to process it and for how long you keep the dataset.

**Copyright.** Recognised text is a copy of the document. Indexing it for your own search is one thing; republishing it is another. The Actor claims no rights over the content and gives you none.

**No affiliation.** This Actor is an independent tool. It is not affiliated with, endorsed by or sponsored by Google, the Tesseract project, ABBYY, Adobe or any OCR vendor; product names are used only to describe what the Actor does and what it is built on. Tesseract is open-source software released under the Apache License 2.0.

If you are the owner of a file and want a source removed from your own runs' data, delete the run's dataset in your Apify account; the Actor itself stores nothing outside your run.

### FAQ

#### How do I extract text from an image?

Paste the image URL in **Image and PDF URLs**, set **Languages** to the language on the picture and click **Start**. The document row has the text and Markdown, and the chunk rows have the same text cut for retrieval.

#### Does it work on scanned PDFs?

Yes. A scanned PDF is drawn page by page at `pdfDpi` (200 by default) and each page is read like an image. If your PDF has a selectable text layer, a text-layer extractor is cheaper and exact; this Actor will still read it, as a picture.

#### Which languages are supported?

37, listed in the table above, up to four per run. Add the language printed on the page; combining two (for example English and German) works for mixed documents but is slower.

#### Why is the confidence low?

Blur, a small image, a decorative font, handwriting, a heavy background or the wrong language. Try a larger image, `pdfDpi: 300` for PDFs, `enhance` on, and make sure **Languages** matches the page. Chunks under your `lowConfidenceBelow` threshold are flagged, never silently dropped.

#### Why did my page come back as blank or unreadable?

A page with almost no letters or digits is `blank`; a page the engine could barely read is `unreadable`. Both are free. If you expected text, check the image at full size and the language setting.

#### How do I get chunks for a vector database?

Set **What to return** to *Chunks only*, choose a **Chunk size** that matches your embedding model's budget (512 is a good default) and leave the overlap at 64. Every chunk row has `text`, `tokenCount`, `pageStart`, `pageEnd`, `headings` and `confidence`.

#### Are the token counts real?

Yes. They are counted with **cl100k_base**, the tokenizer behind OpenAI's embedding and chat models, not estimated from characters. A different model family tokenizes slightly differently, but the numbers stay close enough to plan a budget.

#### Why does my link fail with `unsupported_type`?

The URL returned something that is not an image or a PDF — usually an HTML page. This happens when the link opens a viewer, when the file needs a login or when the site shows a cookie wall. Open the link in a browser, use the download button and copy the direct file URL.

#### Can I read image links from another Actor's dataset?

Yes, that is what **Read links from a dataset** is for. Give the dataset ID and the Actor looks for `imageUrl`, `fileUrl`, `pdfUrl`, `screenshotUrl`, `url`, `link` and similar fields. Both the pasted list and the dataset are merged and deduplicated.

#### Can I process only some pages?

Yes. **First page** and **Maximum pages per file** limit what is read and what is charged. A 200-page manual you need three pages from costs three pages.

#### Does it read handwriting?

Not reliably. The engine is built for printed text. Handwriting usually comes back with low confidence, and the flag tells you so.

#### What happens if one file fails in a batch?

Nothing else stops. The failed file writes its own row with `failureReason` and a readable `message`, the run continues with the next link, and the run summary lists the first failures. The run only fails when **no** file could be read at all.

#### How much does a run cost?

A single-page scan is about **$0.005** on the Free plan; 1,000 one-page scans about **$4.00**. Pages with text are what drive the price, so `maxPagesPerFile` is the lever. Set a maximum cost per run to hard-cap it.

#### Do I need a proxy?

Normally no. Files are downloaded directly. Use Apify Proxy only when a host blocks data-centre IPs, and note that the Actor still stops at a refusal instead of trying to defeat a challenge.

#### Can I use the output commercially?

The Actor gives you recognised text; whether you may use that text commercially depends on the document's licence and the site's terms, not on this tool. See the legal section above and take your own advice.

### Related Actors

These are other Actors by the same developer. They are separate tools; this README does not depend on them.

**RAG family: the same chunk format for other sources**

- [PDF to Markdown & RAG Chunks Extractor](https://apify.com/mmdincer/pdf-rag-extractor) — PDFs with a text layer to Markdown and token-counted chunks with page ranges. Use it first for digital PDFs and this Actor for scans.
- [Audio & Video Transcriber](https://apify.com/mmdincer/audio-video-transcriber) — audio, video and podcast feeds to timestamped transcripts, SRT/VTT and token-counted chunks with time ranges.
- [Video Frame Extractor & Scene Chunks for RAG](https://apify.com/mmdincer/video-frame-extractor) — video files to scenes with keyframe images, time ranges and aligned transcript chunks.

**Other tools**

- [Greenhouse, Lever, Ashby & Workable Jobs Scraper & Monitor](https://apify.com/mmdincer/ats-jobs-monitor) — company job boards as one dataset with change monitoring.
- [Shopify Price Tracker & Stock Monitor](https://apify.com/mmdincer/shopify-store-monitor) — new products, price changes and stock changes for any Shopify store.
- [Autocomplete Keyword Scraper](https://apify.com/mmdincer/autocomplete-keyword-scraper) — Google, YouTube and Bing suggestions with an intent label.
- [Eventbrite Events Scraper & Discovery](https://apify.com/mmdincer/eventbrite-events-finder) — public events by city, category and date range.

# Changelog

This Actor's version history is a separate document: https://apify.com/mmdincer/image-ocr-rag/changelog.md

# Actor input Schema

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

Direct links to image files or PDFs, one per line. Up to 200 per run. The link must return the file itself (for example <code>https://…/scan.jpg</code>), not a web page that shows it. A PDF is read page by page.

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

Optional. Pick an Apify dataset whose items contain image or PDF links. The Actor looks for <code>imageUrl</code>, <code>fileUrl</code>, <code>pdfUrl</code>, <code>screenshotUrl</code>, <code>url</code>, <code>link</code> and similar fields, which is how you chain it after a crawler without copying links by hand. The dataset is opened read-only.

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

Languages that appear in the text, up to 4 (for example English and German together). The first one matters most. Each extra language slows recognition down, so pick only what is on the page. The default is English.

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

<b>Document and chunks</b> writes one document row per file plus its chunk rows. <b>Chunks only</b> skips the document row, so the dataset is exactly what an embedding job wants. <b>Document only</b> returns the text, Markdown and quality numbers without chunking.

## `chunkSize` (type: `integer`):

Token budget per chunk, counted with the cl100k_base tokenizer that OpenAI embedding and chat models use. 512 is a good default for retrieval; use 256 for narrow facts such as receipts and 1024 or more for long documents.

## `chunkOverlap` (type: `integer`):

Text from the end of one chunk repeated at the start of the next, in whole paragraphs, so an answer that straddles a boundary is still findable. At most half the chunk size.

## `lowConfidenceBelow` (type: `integer`):

Chunks whose mean OCR confidence (0 to 100) is under this number get <code>lowConfidence: true</code>, so your pipeline can skip or review them. Nothing is removed. 70 is a careful default; clean scans score 90 or more.

## `startPage` (type: `integer`):

For PDFs and multi-page TIFFs: the first page to read, 1-based. Use it to skip a cover page. Single images ignore it.

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

Pages read from one PDF or multi-page TIFF, counted from the first page. Up to 200. Each page that contains text is charged once.

## `pdfDpi` (type: `integer`):

The resolution a PDF page is drawn at before recognition. 200 reads normal print well. Use 300 for small type, 150 for faster, rougher reads. Very large pages are drawn smaller automatically.

## `enhance` (type: `boolean`):

Turns the image grey, stretches its contrast and enlarges small images before recognition. Turn it off only if your images are already clean scans at a good size.

## `autoRotate` (type: `boolean`):

When a page reads badly, the Actor checks whether it is rotated by 90, 180 or 270 degrees and reads it again the right way up. It costs nothing extra and only runs on pages that read poorly the first time.

## `includeMarkdown` (type: `boolean`):

Add the <code>markdown</code> field to the document row: headings, paragraphs and lists rebuilt from the layout. Turn it off to keep the dataset smaller.

## `includePages` (type: `boolean`):

Add a <code>pages</code> array to the document row with the status, confidence, size and text of every page.

## `maxFiles` (type: `integer`):

Upper bound on files read in one run, counting links from the list and from the dataset together.

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

Files larger than this are skipped instead of downloaded, and reported as too_large.

## `maxChunksPerFile` (type: `integer`):

Safety cap on chunk rows for one file. The document row always holds the whole text.

## `requestsPerSecond` (type: `number`):

How fast files are downloaded from one server. Lower it to 1 when all your images come from one small server. The Actor never retries a refusal: a 403 or 429 marks the file as blocked and no more files are requested from that host.

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

Optional. Recognition is CPU-bound, so by default the Actor reads about one page per GB of run memory. Leave empty unless you changed the memory and want to override it.

## `proxyConfiguration` (type: `object`):

Only needed when an image host blocks data-centre IP addresses. Off by default.

## Actor input object example

```json
{
  "urls": [
    "https://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-memo.jpg?signature=1KzzIzLZdq8wNUqXypxnv",
    "https://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-field-report.pdf?signature=pT3nFk45ZKxfsfenwH0X"
  ],
  "datasetId": "wL9ZqWz8Kk3vJm2Rt",
  "languages": [
    "eng"
  ],
  "mode": "both",
  "chunkSize": 512,
  "chunkOverlap": 64,
  "lowConfidenceBelow": 70,
  "startPage": 1,
  "maxPagesPerFile": 50,
  "pdfDpi": 200,
  "enhance": true,
  "autoRotate": true,
  "includeMarkdown": true,
  "includePages": false,
  "maxFiles": 100,
  "maxFileSizeMb": 50,
  "maxChunksPerFile": 1000,
  "requestsPerSecond": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One row per file with status, pages, OCR confidence, text and Markdown. Filter on recordType document.

## `chunks` (type: `string`):

One row per token-counted chunk with page range and OCR confidence. Filter on recordType chunk. This is the view to send to an embedding model.

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

Files read, failed and empty, pages with and without text, chunks and tokens written, mean confidence and whether the run stopped at the spending limit.

# 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://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-memo.jpg?signature=1KzzIzLZdq8wNUqXypxnv",
        "https://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-field-report.pdf?signature=pT3nFk45ZKxfsfenwH0X"
    ],
    "languages": [
        "eng"
    ],
    "mode": "both",
    "chunkSize": 512,
    "chunkOverlap": 64,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("mmdincer/image-ocr-rag").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://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-memo.jpg?signature=1KzzIzLZdq8wNUqXypxnv",
        "https://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-field-report.pdf?signature=pT3nFk45ZKxfsfenwH0X",
    ],
    "languages": ["eng"],
    "mode": "both",
    "chunkSize": 512,
    "chunkOverlap": 64,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("mmdincer/image-ocr-rag").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://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-memo.jpg?signature=1KzzIzLZdq8wNUqXypxnv",
    "https://api.apify.com/v2/key-value-stores/vbQuJ5uhWVgUngr57/records/ocr-sample-field-report.pdf?signature=pT3nFk45ZKxfsfenwH0X"
  ],
  "languages": [
    "eng"
  ],
  "mode": "both",
  "chunkSize": 512,
  "chunkOverlap": 64,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call mmdincer/image-ocr-rag --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mmdincer/image-ocr-rag"
        }
    }
}
```

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/lw9zC0pfnCw89N559/builds/N4yLcK629iy1OKtmr/openapi.json
