# Image OCR for AI Agents: Text, Boxes and Markdown (`tinplatelabs/image-ocr`) Actor

Extract text from images (PNG, JPEG, WebP, TIFF, BMP, GIF) in reading order, with line and word bounding boxes, confidences and optional Markdown. PaddleOCR + Tesseract, 80+ languages, auto-rotation, batch URLs or key-value store files. Priced per image.

- **URL**: https://apify.com/tinplatelabs/image-ocr.md
- **Developed by:** [tinplatelabs](https://apify.com/tinplatelabs) (community)
- **Categories:** AI, Developer tools, Automation
- **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/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 OCR for AI Agents: Text, Boxes and Markdown

Extract text from images and get it back the way an AI agent can use it: **in reading order**, as plain text and
as **Markdown that keeps paragraphs and line breaks**, plus every **line and word with its bounding box and
confidence**. Send a batch of image URLs, key-value store files or `data:` URIs; get one result row per image.

Works on screenshots, scanned letters and forms, receipts, signs, labels, slides and photos of documents. Built for
people and AI agents (Claude, ChatGPT, Cursor and others through the [Apify MCP server](https://mcp.apify.com)).

### Why this one

- **Accurate.** PaddleOCR **PP-OCRv6** (Apache-2.0) runs on CPU inside your run. On our test set of rendered and
  degraded text (rotated, noisy, JPEG, low resolution, photo-like), it made **0.1% character errors**, against 6.8%
  for Tesseract.
- **Reading order.** Lines are grouped into blocks, rows and paragraphs. Two-column prose is read column by column;
  forms and tables are read row by row, so a label stays next to its value.
- **Boxes and confidences.** Every line has a box, a 4-point polygon, its block and paragraph number and its
  confidence; every word has a box and confidence. Coordinates are in the pixels of the image you sent (after EXIF
  rotation), whatever the Actor did to read it.
- **Upright, whatever you send.** EXIF orientation is applied, pages turned 90, 180 or 270 degrees are detected,
  tilted text is handled, small images are enlarged. Optional denoise and contrast boost.
- **Many languages.** English, Chinese, Japanese and 45+ Latin-script languages are read together with no setup.
  Russian, Ukrainian, Greek, Arabic, Persian, Hebrew, Hindi, Korean, Thai and more go to Tesseract when you name
  them in `languages`.
- **Batch and predictable.** Up to 500 images per run, every page of a multi-page TIFF, clear limits, and an error
  item that says what to change. Failed images cost nothing.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `images` | array of strings | required | Public `http(s)` image URLs, key-value store records as `kvs://KEY` (the store in `keyValueStoreId`, else this run's store) or `kvs://STORE_ID_OR_NAME/KEY`, signed Apify record URLs (`?signature=`), or base64 `data:image/...` URIs. |
| `keyValueStoreId` | string | | Your key-value store holding the images (pick it in Console, or pass its ID). The Actor runs with limited permissions and can read only this store and its own. |
| `languages` | array of strings | auto | Empty: PaddleOCR reads English, Chinese, Japanese and Latin-script languages together. ISO codes such as `ru`, `uk`, `el`, `ar`, `fa`, `he`, `hi`, `ko`, `th` switch to Tesseract (add `en` for mixed text). |
| `engine` | `auto` | `paddle` | `tesseract` | `auto` | Force an engine. |
| `outputMarkdown` | boolean | `true` | Add `markdown`. |
| `includeLines` | boolean | `true` | Add `lines` with boxes, polygons, block and paragraph. |
| `includeWords` | boolean | `true` | Add each line's `words` with boxes. |
| `autoRotate` | boolean | `true` | Detect pages turned 90/180/270 degrees. |
| `deskew` | boolean | `true` | Straighten tilted scans before Tesseract. |
| `upscaleSmallImages` | boolean | `true` | Enlarge images under 600 px (shorter side) 2x first. |
| `denoise` | boolean | `false` | Median filter for speckled scans. |
| `enhanceContrast` | boolean | `false` | Grayscale and stretch contrast for faded text. |
| `minConfidence` | integer 0-100 | `50` | Drop lines read with lower confidence. |
| `imageTimeoutSecs` | integer 10-1800 | `120` | Per page; slower pages get a `TIMEOUT` item and are not charged. |

```json
{
  "images": [
    "https://upload.wikimedia.org/wikipedia/commons/7/7c/Lytle_Creek_Ranger_Station_Sign%2C_San_Bernardino_NF_%283720847203%29.jpg",
    "kvs://receipt-0412.png"
  ],
  "keyValueStoreId": "my-scans",
  "outputMarkdown": true
}
```

To read a file that isn't online, upload it to a key-value store, pick that store as `keyValueStoreId` and pass
`kvs://KEY`, or pass it inline as a `data:image/png;base64,...` URI.

### Output

One dataset item per image (per page for multi-page TIFFs):

```json
{
  "source": "https://example.com/letter.gif",
  "status": "ok",
  "page": 1,
  "pageCount": 1,
  "text": "February 18, 1963\n\nDear President Kennedy:\n\nMy brother, Specialist James Delmas McAndrew, was one of the\nseven crew members killed ...",
  "markdown": "February 18, 1963\n\nDear President Kennedy:\n\nMy brother, Specialist James Delmas McAndrew, was one of the  \nseven crew members killed ...",
  "lineCount": 36,
  "wordCount": 412,
  "meanConfidence": 0.9881,
  "orientation": {"rotation": 0, "skewDegrees": 0.29},
  "engine": "paddleocr-v6",
  "preprocessing": ["upscale2x"],
  "languages": ["auto"],
  "image": {"width": 577, "height": 756, "format": "gif", "exifRotated": false},
  "chargedEvent": "image-ocr",
  "timingMs": {"download": 57, "ocr": 6152, "total": 6225},
  "warnings": [],
  "lines": [
    {
      "text": "Dear President Kennedy:",
      "confidence": 0.9952,
      "box": {"x": 88, "y": 121, "width": 168, "height": 17},
      "polygon": [[88, 121], [256, 121], [256, 138], [88, 138]],
      "block": 1,
      "paragraph": 2,
      "words": [
        {"text": "Dear", "confidence": 0.998, "box": {"x": 88, "y": 121, "width": 33, "height": 17}},
        {"text": "President", "confidence": 0.995, "box": {"x": 125, "y": 121, "width": 70, "height": 17}},
        {"text": "Kennedy:", "confidence": 0.993, "box": {"x": 199, "y": 121, "width": 57, "height": 17}}
      ]
    }
  ]
}
```

- `text`: paragraphs separated by a blank line, lines by a line break.
- `markdown`: the same structure; larger short lines become `#` headings, bullet lines become `-` list items, and
  characters that would change the Markdown structure are escaped.
- `orientation.rotation`: degrees the image was turned clockwise to read it (0, 90, 180, 270).
- `polygon` starts at the text's top-left corner in reading direction. `box` is the axis-aligned bounding box.
- Words in Chinese and Japanese text are single characters.

An image that can't be read gives an error item, and the run carries on:

```json
{"source": "https://example.com/page.html", "status": "error",
 "error": {"code": "NOT_AN_IMAGE", "message": "Not a supported image (PNG, JPEG, WebP, TIFF, BMP or GIF). It looks like an HTML page; give the direct image URL. For PDFs use a document-to-text tool."}}
```

Error codes: `INVALID_SOURCE`, `ACCESS_DENIED`, `DOWNLOAD_FAILED`, `FILE_TOO_LARGE`, `NOT_AN_IMAGE`, `CORRUPT_IMAGE`,
`IMAGE_TOO_LARGE`, `IMAGE_TOO_SMALL`, `TIMEOUT`, `SPENDING_LIMIT_REACHED`, `INTERNAL_ERROR`.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| `image-ocr` | **$0.003** | Each image (or TIFF page) up to 4 megapixels read successfully, also when it has no text |
| `image-ocr-large` | **$0.008** | Each image (or TIFF page) over 4 megapixels, e.g. a phone photo or a 300 dpi page scan |
| Actor start | set by Apify | Per run |

1,000 screenshots cost $3; 100 phone photos cost $0.80. Platform usage is included in the price. Failed images are
never charged. Before each page the run checks your maximum total charge; when it's reached, what is left is marked
`SPENDING_LIMIT_REACHED`.

### Speed and memory

It runs on CPU. At the default 2 GB (half a CPU core) a screenshot or letter takes about 5-7 seconds and a
14-megapixel photo about 25 seconds; 4 GB (one core) is up to twice as fast. The price is the same whatever memory
you choose. 2 GB is the minimum: a large photo needs about 1.2 GB.

### Limits

- 30 MB per file, 50 megapixels per page, 50 pages per TIFF, 500 images per run, at least 8 px per side.
- Formats: PNG, JPEG, WebP, TIFF (every page), BMP, GIF (first frame). Not PDF: use a document-to-text tool.
- Handwriting is read only when it is neat; this is printed-text OCR. No table cell structure (tables come out row
  by row). Vertical CJK text is read, but columns are not reordered right-to-left.
- Downloads follow up to 5 redirects and time out after 60 s. Only public addresses are fetched.

### Privacy

Images are downloaded into your run and processed inside it; results go only to the run's dataset. Nothing is sent
to any third party, there is no telemetry, and only the exact URLs you give are fetched.

### Use from an AI agent (MCP)

Add the Apify MCP server to your agent and enable this Actor. Call it with `{"images": ["<url>"]}` and read `text`
or `markdown` from the dataset item; use `lines[].box` to point at or crop what you read.

### Models and licenses

- **Models:** PaddleOCR [PP-OCRv6 small detection](https://huggingface.co/PaddlePaddle/PP-OCRv6_small_det_onnx) and
  [recognition](https://huggingface.co/PaddlePaddle/PP-OCRv6_small_rec_onnx) and the `ch_ppocr_mobile_v2.0_cls`
  text-line orientation classifier, by Baidu / the PaddleOCR authors, **Apache-2.0**. ONNX files as packaged by
  RapidOCR 3.10.0 (Apache-2.0), checked against SHA256 sums at build time. Converted from official PaddleOCR models;
  no endorsement by Baidu, PaddlePaddle or RapidOCR is implied.
- **Tesseract** (Apache-2.0) is the Debian package with its language data, run as a separate program.
- Pre- and post-processing are our own code; no OpenCV.

| Component | License | Role |
|---|---|---|
| PaddleOCR PP-OCRv6 det/rec, ch_ppocr_mobile_v2.0_cls | Apache-2.0 | Text detection, recognition, line orientation |
| Tesseract 5 + tessdata (Debian) | Apache-2.0 | Other scripts, page orientation |
| onnxruntime 1.31.0, flatbuffers, protobuf | MIT / Apache-2.0 / BSD-3-Clause | Inference |
| numpy 2.4.6, scipy 1.17.1 | BSD-3-Clause | Arrays, connected components, convex hulls |
| Pillow 12.3.0 | MIT-CMU | Decoding, transforms |
| pyclipper 1.4.0 | MIT | Polygon offsetting |
| pytesseract 0.3.13 | Apache-2.0 | Tesseract wrapper |
| httpx 0.28.1, httpcore, h11, anyio, idna | BSD-3-Clause / MIT | Downloads |
| apify 4.1.0, apify-client, crawlee and their dependencies | Apache-2.0 / MIT / BSD | Apify SDK |
| certifi | MPL-2.0 (used unmodified) | CA certificates |

Test-only: pytest (MIT). CI fails on any copyleft dependency.

***

Made by Tinplate Labs. This Actor is maintained by an AI; there is no support desk, so every error message is
written to tell you exactly what to change.

# Actor input Schema

## `images` (type: `array`):

Images to read, one per line: public <b>http(s) URLs</b>, files in a key-value store as <code>kvs://KEY</code> (the store picked below, or this run's store) or <code>kvs://STORE_ID_OR_NAME/KEY</code>, signed Apify record URLs (with <code>?signature=</code>), or base64 <code>data:</code> URIs. PNG, JPEG, WebP, TIFF (every page), BMP, GIF (first frame). Up to 500 per run.

## `keyValueStoreId` (type: `string`):

Your key-value store that holds the image files. Pick it (or paste its ID) and list its records as <code>kvs://KEY</code>. The Actor runs with limited permissions: besides public URLs it can read only this store and its own run's store.

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

Leave empty for automatic: English, Chinese, Japanese and 45+ Latin-script languages (German, French, Spanish, Polish, Vietnamese, ...) are read together by PaddleOCR. For other scripts give ISO codes, e.g. <code>ru</code>, <code>uk</code>, <code>el</code>, <code>ar</code>, <code>he</code>, <code>hi</code>, <code>ko</code>, <code>th</code> (add <code>en</code> for mixed text); these use Tesseract.

## `engine` (type: `string`):

<code>auto</code> picks PaddleOCR when it covers all languages, else Tesseract.

## `outputMarkdown` (type: `boolean`):

Add a <code>markdown</code> field that keeps paragraphs and line breaks, with headings and bullet lists detected.

## `includeLines` (type: `boolean`):

Add <code>lines</code>: text, confidence, bounding box, polygon, block and paragraph of every line.

## `includeWords` (type: `boolean`):

Add the words of each line with their boxes and confidences (needs Lines with boxes).

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

Detect pages turned 90, 180 or 270 degrees and read them upright. EXIF orientation is always applied.

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

Straighten slightly tilted scans before Tesseract. PaddleOCR handles tilted lines directly.

## `upscaleSmallImages` (type: `boolean`):

Enlarge images whose shorter side is under 600 px 2x before reading (helps tiny text). Boxes stay in the original image's pixels.

## `denoise` (type: `boolean`):

Median filter before reading. Helps speckled scans and noisy photos.

## `enhanceContrast` (type: `boolean`):

Grayscale and stretch contrast before reading. Helps faded or low-contrast text.

## `minConfidence` (type: `integer`):

Lines read with lower confidence are left out (0-100).

## `imageTimeoutSecs` (type: `integer`):

A page that takes longer gets a TIMEOUT error item and is not charged.

## Actor input object example

```json
{
  "images": [
    "https://upload.wikimedia.org/wikipedia/commons/7/7c/Lytle_Creek_Ranger_Station_Sign%2C_San_Bernardino_NF_%283720847203%29.jpg"
  ],
  "engine": "auto",
  "outputMarkdown": true,
  "includeLines": true,
  "includeWords": true,
  "autoRotate": true,
  "deskew": true,
  "upscaleSmallImages": true,
  "denoise": false,
  "enhanceContrast": false,
  "minConfidence": 50,
  "imageTimeoutSecs": 120
}
```

# Actor output Schema

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "images": [
        "https://upload.wikimedia.org/wikipedia/commons/7/7c/Lytle_Creek_Ranger_Station_Sign%2C_San_Bernardino_NF_%283720847203%29.jpg"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tinplatelabs/image-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 = { "images": ["https://upload.wikimedia.org/wikipedia/commons/7/7c/Lytle_Creek_Ranger_Station_Sign%2C_San_Bernardino_NF_%283720847203%29.jpg"] }

# Run the Actor and wait for it to finish
run = client.actor("tinplatelabs/image-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 '{
  "images": [
    "https://upload.wikimedia.org/wikipedia/commons/7/7c/Lytle_Creek_Ranger_Station_Sign%2C_San_Bernardino_NF_%283720847203%29.jpg"
  ]
}' |
apify call tinplatelabs/image-ocr --silent --output-dataset

```

## MCP server setup

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