Scanned PDF & Image OCR - Tables to JSON
Pricing
from $4.00 / 1,000 page with texts
Scanned PDF & Image OCR - Tables to JSON
OCR scanned PDFs, images and photos of documents into text, Markdown and cell-level JSON tables. Per-page status, English, Latin-script, Japanese and Cyrillic models, no external OCR account. Failed and blank pages are not charged.
Pricing
from $4.00 / 1,000 page with texts
Rating
0.0
(0)
Developer
Vadim Bezrukov
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 days ago
Last modified
Categories
Share
Turn scanned PDFs, phone photos and images of documents into readable text, Markdown and tables as JSON, cell by cell. Send up to 200 files per run as public URLs, from another Actor's Dataset, as base64 or as one upload. Built for n8n, Make, AI agents and back-office pipelines that receive scans and need structured data, without an external OCR account or API key.
Every document comes back as one Dataset row with an explicit status, per-page status and confidence, and every table as headers plus rows of cells. The Console prefill runs three bundled sample scans (an invoice, an employee roster and a receipt photo), so the first run shows real tables without any file of your own.
Latest notes are in the changelog.
What a scan becomes
The bundled sample:invoice-scan is an image-only PDF: no text layer, only pixels. Its line items come back as a table:
{"status": "SUCCESS","filename": "invoice-scan.pdf","tables": [{"table_id": "42dc2a009179-p1-t1","page": 1,"headers": ["Qty", "Item", "Unit price", "Amount"],"rows": [["2", "LED module", "234.97", "469.94"],["4", "Copper wire 2mm", "133.13", "532.52"],["28", "Mounting plate", "233.97", "6,551.16"],["", "Subtotal", "", "13,539.99"],["", "VAT 20%", "", "2,708.00"],["", "Total due", "", "16,247.99"]],"row_count": 10,"column_count": 4}]}
(Rows shortened here; the run returns all ten.) The same row also carries the full reading-order text and a markdown version with the table in place.
A one-page scan with text costs $0.005, plus $0.002 for each table it holds. A typical batch of 50 two-page scans with 40 tables costs about $0.54: $0.001 per document, $0.004 per page with text and $0.002 per table, plus $0.01 to start at the default 2 GB. Failed, blank and unprocessed pages are free.
Why use it
Automation platforms rarely have an OCR step you can just install, and the usual workarounds are a Docker sidecar with Tesseract, a separate cloud OCR account with its own key, or a vision LLM. Store OCR tools mostly return text lines: the table is gone.
This Actor keeps the table. On our 20-document synthetic benchmark of image-only scans (536 table cells, English, German, French, Spanish, Polish, Russian and Japanese, skew, noise and a 120 dpi scan) it recovered 535 of 536 cells with a mean character error rate of 0.003. That is a synthetic benchmark, not a guarantee for your documents: see Limits.
- Tables from scans to JSON: headers and rows of cells, plus Markdown and the model's HTML.
- PDF, PNG, JPEG, TIFF and WebP in one run; multi-page TIFF frames are pages.
- Per-page status: a failed or blank page never hides behind a successful document, and is never charged.
- Chains after other Actors: read document links from a crawler's, a downloader's or PDF Text Extractor's Dataset.
- No external account: the OCR, layout and table models run inside the Actor.
Supported input
| Input | Use it when |
|---|---|
urls | Files are on public http or https URLs, or you want a bundled sample (sample:invoice-scan, sample:employee-roster-scan, sample:receipt-photo). |
datasetId | Another Actor's run stored the document links in a Dataset column. |
base64Files | An agent, a webhook or your backend has the bytes: up to 20 files of 10 MB. A data:...;base64, prefix is accepted. |
fileUpload | You upload one file in the Console. |
One run can mix urls, base64Files and fileUpload, up to 200 documents. When the platform fills urls with the samples and your request also has base64Files or fileUpload, the samples are skipped (RUN_SUMMARY.droppedDefaultSamples).
The file type comes from the file's bytes, not its name or Content-Type. An HTML login page served with HTTP 200 is NOT_A_DOCUMENT, never an OCR of a login page. Private hosts, localhost, cloud metadata addresses and ports other than 80 and 443 are rejected.
| Option | Default | Meaning |
|---|---|---|
ocrLanguage | en | en, latin (German, French, Spanish, Polish and other Latin-script languages) and japan share one multilingual model; cyrillic selects a Cyrillic model. |
extractTables | true | Detect tables and return them cell by cell. |
includePages | true | Include each page's text in pages. |
renderDpi | 200 | Resolution for PDF pages, 150 to 300. |
adaptiveDpi | true | Render low-resolution scans (under 150 dpi) and very small text again at 300 dpi. |
maxPagesPerDocument | 100 | Pages after the limit are skipped and free; the document is PARTIAL. Up to 500. |
maxFileSizeMb | 50 | Larger files end TOO_LARGE without a download. Up to 100. |
maxConcurrency | 2 | Upper bound for parallel documents; fewer are used when the run's CPU share is smaller. |
Quick start
Run the prefill, or send your own scan:
{"urls": ["https://example.com/inbox/scan-0417.pdf"],"ocrLanguage": "latin","extractTables": true}
Open the Tables view for one row per table, Documents for one row per file, Pages for page status and confidence.
Chain after a crawler, a downloader or PDF Text Extractor
PDF Text Extractor answers a scanned PDF with NO_TEXT_LAYER; a crawler or the Parallel Bulk File Downloader ends with rows that hold file links. Pass that Dataset here:
{"datasetId": "YOUR_DATASET_ID","urlField": "source_url","extractTables": true}
urlField is the column with the link (default url; dot paths such as document.url work). PDF Text Extractor stores source_url with query values redacted, so signed links (S3, CDN) must come from the original list or another column. A run reads up to 200 rows from datasetOffset; RUN_SUMMARY.nextDatasetOffset says where the next run starts. Rows without a link are skipped and free. Each result keeps source_dataset_id and source_item_index, so it joins back to its source row.
To run automatically after another Actor, add an Actor-to-Actor integration on it with "datasetId": "{{resource.defaultDatasetId}}" in this Actor's input. The integration's payload is accepted and not read.
n8n, Make and backend
One HTTP request runs the Actor and returns the rows when it finishes (keep batches small enough to finish within the request timeout):
curl -X POST "https://api.apify.com/v2/acts/automa-flow~scanned-document-ocr-table-extractor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"urls": ["https://example.com/inbox/scan-0417.pdf"], "extractTables": true}'
In n8n use an HTTP Request node with that URL and body (or the Apify node), then map tables[0].rows to a Google Sheet and route rows whose status is not SUCCESS to a review queue. For bytes from an email attachment, send base64Files: [{"filename": "scan.pdf", "base64": "..."}] instead of urls. A schedule or webhook that sends each new batch is the natural repeat: there is no state between runs.
Sample output
Full rows for the three samples are in examples/sample_output.json. Main fields:
| Field | Meaning |
|---|---|
status, failure_reason, failure_detail | The document outcome, see Statuses. |
text, markdown | Reading-order text (pages separated by form feeds) and Markdown with tables in place. |
tables[] | headers, rows, row_count, column_count, cells_filled, bbox, detection_confidence, markdown, html. |
pages[] | Per page: status, dpi, line_count, mean_ocr_confidence, tables_count, text. |
source_id, sha256, fingerprint | SHA-256 of the bytes, and a hash of the text and cells that is equal for equal content (rows without content hash their status and reason). |
billing | What the row was charged for: document_billed, pages_billed, tables_billed. |
Statuses
| Document status | Meaning | Charged |
|---|---|---|
SUCCESS | Every processed page returned text or was blank. | yes |
PARTIAL | Text plus failed pages, the page limit or the run time limit. | for the pages with text |
NO_TEXT | The file rendered but held no text (blank scans). | no |
FAILED | Nothing usable. failure_reason says why: DOWNLOAD_FAILED, NOT_A_DOCUMENT, EMPTY_FILE, TOO_LARGE, PASSWORD_PROTECTED, INVALID_SOURCE, RENDER_FAILED, OCR_FAILED, TIMEOUT. | no |
NOT_PROCESSED | Stopped by the run time limit (NOT_PROCESSED_TIME_LIMIT) or your maximum charge (CHARGE_LIMIT_REACHED). | no |
A damaged or truncated PDF or image is NOT_A_DOCUMENT, and an image over 80 megapixels is TOO_LARGE. A run with only buyer-side failures (a 404, an HTML page, an encrypted PDF, a truncated file) still ends SUCCEEDED, because every row says what to fix. The run fails only for invalid input, a damaged model file, an OCR engine that crashes three times, an engine failure on every document it accepted, or storage that cannot be written. RUN_SUMMARY in the run's Key-Value Store has the outcome, counts per status and reason, pages and tables charged, seconds per page and the inputs a follow-up run should retry.
Pricing
Pay per event, platform usage included:
| Event | Price |
|---|---|
| Actor start | $0.005 per GB of memory (default 2 GB: $0.01) |
Document processed (SUCCESS or PARTIAL) | $0.001 |
| Page with text | $0.004 |
| Table extracted (two or more columns, one or more data rows) | $0.002 |
The prefill costs about $0.03. Retries, failed downloads, blank and failed pages, skipped pages and unprocessed documents are never charged. Set Maximum cost per run to cap a batch: a document is delivered only when its whole price fits, and the rest end NOT_PROCESSED.
Limits
- The accuracy figures come from our 20-document synthetic benchmark of clean generated scans. Real faxes, crumpled paper and phone photos at an angle have not been benchmarked yet.
cyrillicwas tested on one Russian document only. Other scripts (Chinese, Korean, Arabic, Devanagari) are not offered yet.- No handwriting, no form fields or key-value extraction, no office formats (DOCX, XLSX), no password-protected PDFs.
- Very low-resolution scans can lose table structure even though the text is read; adaptive DPI helps with scans down to about 120 dpi.
- Pages scanned sideways or upside down are not rotated before OCR; rotate them first (photos with an EXIF orientation are turned upright).
- An image over 80 megapixels ends
TOO_LARGE. A page is read at up to 40 megapixels (20 below 2 GB of memory); a larger page is scaled down to that size. - A PDF that already has a text layer is OCRed like a scan; for digital PDFs, PDF Text Extractor is cheaper.
- A page takes about 5 seconds at the default 2 GB (median on our staging basket; about 10 seconds at 1 GB). Up to 500 pages per document and 200 documents per run.
Privacy and security
Process only documents you have the right to process. Files are held in memory and a temporary folder that is deleted after each document; the original files are not stored in the Dataset or Key-Value Store. Logs and RUN_SUMMARY hold counts, statuses and timings, never document text, base64 payloads or URL query strings, and stored URLs have their query values redacted. Apify keeps the run input (including base64) as part of the run, which is platform behaviour.
If a run fails or looks wrong, open an Issue on this Actor and include the run URL. Apify shares that run with us automatically, so there is no need to paste your documents or credentials.
API and MCP
Call the Actor from any HTTP client, the Apify API clients, n8n, Make or Zapier with your own Apify token. Agents can find it through Apify MCP search; running it uses your Apify account, and maxTotalChargeUsd bounds the bill of each call. The run's RUN_SUMMARY record gives an agent the outcome, counts and the inputs to retry without reading every row.
Direct MCP endpoint:
https://mcp.apify.com?tools=automa-flow/scanned-document-ocr-table-extractor
Technical details
- OCR: RapidOCR 3.10 with PP-OCRv6 detection and multilingual recognition, PP-OCRv5 for Cyrillic (Apache-2.0, PaddleOCR models). Tables: RapidLayout table detection and RapidTable SLANet-plus (Apache-2.0). PDF rendering: pypdfium2 (Apache-2.0 / BSD-3-Clause), images: Pillow (MIT-CMU), inference: ONNX Runtime (MIT), OpenCV headless (Apache-2.0), NumPy (BSD-3-Clause).
- Models are downloaded when the image is built and verified by SHA-256 at every start; a run never downloads a model.
RUN_SUMMARY.modelChecksumslists them. - OCR runs in a worker process with a 60-second limit per page; a page that overruns is stopped, its worker replaced, and the batch continues. Downloads make at most three requests per URL within 90 seconds: network errors and HTTP 408, 429, 502, 503 and 504 are retried, other errors are final.
- Default memory 2 GB (minimum 1 GB). Same bytes and settings give the same text, tables and
fingerprint.