PDF Text Extractor — OCR for scanned PDFs (Arabic + English)
Pricing
Pay per event
PDF Text Extractor — OCR for scanned PDFs (Arabic + English)
Turn scanned or image-only PDFs into structured text that keeps its reading order: per-block bounding boxes, per-block OCR confidence, column-aware ordering, and a spatial plain-text rendering. Arabic and English OCR built in. Pay per document plus per page.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Dos
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
20 hours ago
Last modified
Categories
Share
Extract text from scanned and image-only PDFs by URL — the OCR half of amanatools' PDF toolkit, and the only extractor here that keeps the layout. Arabic, Persian and Urdu included.
Feed it PDF links. Get back text that still knows where it came from: per-block bounding boxes, per-block OCR confidence, column-aware reading order, and a spatial plain-text rendering of every page. Arabic and English are both first-class.
Three things this does that the alternatives don't
1. Layout survives
Every other PDF text extractor hands you one flat blob per page. This one returns blocks[] — each with bbox, confidence, column and order — sorted into true reading order using whitespace-gutter column detection, plus a text_layout field that reproduces the page on a character grid the way pdftotext -layout does.
Two-column papers, brochures, invoices with a side panel, and ruled-line-free tables come out readable instead of interleaved. The gutter detector is tuned to real column geometry, not to a comfortable round number: a standard two-column paper (US Letter, 1 in margins, LaTeX's default 10 pt \columnsep — columns at 72–301 pt and 311–540 pt) has a gutter just 1.6% of the page wide, and that is detected. See Limits for what still defeats it. Downstream LLMs and agents get something they can actually parse.
2. Arabic is a first-class citizen
A store-wide search for "arabic" returns no OCR or document-processing actors at all. This one ships the ara model, right-to-left reading order (bands ordered right→left, RTL lines joined in logical order), and automatic Latin / Arabic / Cyrillic script detection per document. Built for MENA invoices, receipts and government PDFs, by an operator based in Abu Dhabi.
It is also honest about what tesseract Arabic can and cannot do — see Limits.
3. It knows when not to OCR
ocr_mode: "auto" (the default) reads the digital text layer when there is one — fast, cheap, character-perfect — and only rasterizes and OCRs the pages that actually need it. A born-digital report with three scanned annexes is handled page by page, and the invoice reflects that: OCR pages cost $0.02, text-layer pages cost $0.005.
Which actor do I want?
- Born-digital PDF with real tables? Use amanatools/pdf-table-extractor — it returns
{headers, rows}. - Scanned PDF, image-only pages, or you need coordinates and confidence? Start here.
Input
{"pdf_urls": ["https://raw.githubusercontent.com/ocrmypdf/OCRmyPDF/v16.0.0/tests/resources/ccitt.pdf","https://example.com/invoice-scan.pdf"],"pages": "all","ocr_mode": "auto","language": "auto","preserve_layout": true,"dpi": 200,"auto_rotate": true,"min_word_confidence": 0,"max_pages_per_document": 20,"include_failed_documents": true}
| Field | Default | Notes |
|---|---|---|
pdf_urls | — | Required. Up to 50 direct PDF links per run, 50 MB each. Repeated links are processed once. |
pages | all | all, 1-5, or 1,3,7-9. Same syntax as pdf-table-extractor. |
ocr_mode | auto | auto | always | never. auto OCRs only pages with under 100 digital characters. |
language | auto | auto, eng, ara, ara+eng, fra, deu, spa, por, ita, nld, tur, rus, fas, urd. |
preserve_layout | true | Off = no blocks, no text_layout, much smaller items. |
dpi | 200 | 72–400. Use 300 for dense or small print. |
auto_rotate | true | Detects and fixes sideways / upside-down scans. |
min_word_confidence | 0 | Drop OCR words below this score. 0 keeps everything. |
max_pages_per_document | 20 | Also your cost ceiling — see Pricing. Raise it (up to 500) for long scans. |
include_failed_documents | true | Failed or empty documents still appear, with status and error. |
Output
One dataset item per PDF, with pages nested — the same shape philosophy as pdf-table-extractor, so pipelines can branch on document status identically for both actors.
{"url": "https://example.com/scan.pdf","status": "ok","error": null,"n_pages_in_document": 1,"n_pages": 1,"n_ocr_pages": 1,"n_text_pages": 0,"truncated_pages": false,"language_used": "eng","script": "latin","mean_confidence": 93.9,"n_words": 729,"text": "page one text\fpage two text","truncated_fields": [],"pages": [{"page": 1,"source": "ocr","status": "ok","error": null,"width": 612.0,"height": 792.0,"rotation_applied": 0,"n_words": 729,"mean_confidence": 93.9,"n_columns": 2,"charged": true,"text": "block one\n\nblock two","text_layout": " INVOICE 2026-07-31\n Total 1,240","blocks": [{"order": 0,"column": 0,"bbox": [140.4, 31.3, 472.0, 73.1],"text": "The LinnSequencer\n32 Track MIDI Sequence Recorder","n_words": 7,"confidence": 95.2,"script": "latin"}]}]}
textjoins pages with a form feed (\f), thepdftotextconvention.bboxis[x0, top, x1, bottom]in PDF points, origin at the top-left of the rendered page.columnis always the visual left→right band index. Onlyorderflips on right-to-left pages.sourceis"ocr"or"text-layer"per page, so you can always see what you paid for.
Pricing
Pay-per-event, no start fee, no subscription:
| Event | Price | When |
|---|---|---|
document-processed | $0.05 | Once per PDF that delivered at least one page of text. |
ocr-page | $0.02 | Per page that had to be rasterized and OCR'd. |
text-page | $0.005 | Per page read straight from the digital text layer. |
You never pay for undelivered work. Failed downloads, unreadable and password-protected files, and pages that produce zero words are not charged at all — not even the per-document fee. Your maximum possible spend is fixed before you press Start:
max spend = $0.05 x documents + $0.02 x max_pages_per_document x documents
If you cap the run with Max total charge (maxTotalChargeUsd), the actor stops as soon as that budget is spent: it logs the reason, keeps everything already pushed and finishes successfully. It does not carry on processing the rest of your list for free.
Limits — read these before you buy
- OCR is statistical, not exact. Expect roughly 95–99% character accuracy on clean 300-DPI printed scans, and materially worse on low-resolution faxes, skewed or shadowed phone photos, stamped or watermarked pages, and dot-matrix print. Every block and every page carries a confidence score precisely so you can gate on it instead of trusting blindly.
- Handwriting is not supported. Tesseract is a printed-text engine; handwritten notes, signatures and annotations come back as noise or nothing at all.
- Arabic, stated honestly. The
aramodel is trained on modern printed Arabic and does well on clean naskh-style print. It is weak on calligraphic and ligature-heavy display fonts, on Quranic typesetting with full harakat, and on old or degraded scans. Diacritics are frequently dropped or misread. Lines that mix Arabic, Latin and digits — typical of MENA invoices — may order imperfectly at the boundaries, because bidirectional reordering from a bitmap is genuinely ambiguous. We ship Arabic because nobody else in the store does, not because it is solved. - Column detection works on whitespace gutters — including tight ones. A gutter counts when a clear vertical channel at least 1.5% of the page width (9.2 pt on US Letter; LaTeX's default
\columnsepis 10 pt) runs inside the middle 70% of the page. Channels narrower than 5% of the page must also be corroborated: at least three text lines on each side, and the channel clear across at least 90% of the page's line rows. That is what lets a normal two-column paper be detected while an incidental wide space in ordinary prose does not split the page in half. Up to two gutters survive per horizontal stripe, so at most three columns. - What still defeats column detection. Pages with no clean vertical channel at all, rotated or nested layouts, magazine-style wrapping around images, scattered labels on a diagram, and — the honest one — tables: a table whose columns are more than 9 pt apart looks exactly like a two-column page, so its cells are read column by column rather than row by row. Two- or three-line fragments with a wide gap (an invoice header, an address block) are deliberately not split. When the ordering comes out wrong, use
text_layout— it reproduces the page spatially and does not depend on the ordering heuristic. - This actor does not recover table structure. It returns blocks, coordinates and spatial text, not
{headers, rows}. For born-digital PDFs with real tables use amanatools/pdf-table-extractor. For scanned tables,text_layoutplus block bounding boxes is the raw material; the parsing is yours. - No searchable-PDF output. This actor returns text and JSON; it does not write a new PDF with an invisible text layer.
- Password-protected and encrypted PDFs are reported as errors, not cracked.
- Bounding boxes are in the rendered frame. If
auto_rotatefired (rotation_appliedis non-zero), the coordinates are in the rotated frame and will not line up with the original page orientation. - Limits per run: 50 URLs, 50 MB per PDF, 500 pages per document (default cap 20, lowered from 50 so a default run fits inside a default timeout). Anything beyond is truncated with
truncated_pages: truerather than silently dropped. - Very large dataset items are trimmed. If a document would exceed Apify's ~9 MB item limit,
text_layoutis dropped first, thenblocks, thentextis truncated — and every stage is recorded intruncated_fields. - The run stops before its timeout rather than dying inside it. The actor reads the run deadline, tracks how long a page actually costs, and stops queueing documents when the remaining time will not cover the next one. It logs the reason, pushes what exists and exits successfully — you never lose output you were charged for. Same for a migration: Apify restarts long-running containers, and this actor checkpoints processed URLs to the run's key-value store, so a migrated run resumes instead of re-pushing and re-charging documents it already delivered. Charges are raised only after an item reaches the dataset.
- Very tall or very wide pages are rendered coarser, never dropped. The render scale is clamped on both axes at 10000 px, so a 612 x 14400 pt page (the PDF maximum — long receipt scans, web-to-PDF exports, plotter output) renders at reduced DPI instead of exhausting memory. OCR quality on such a page suffers accordingly; use
pagesto work through it in slices if you need full DPI. Thetext_layoutgrid is likewise capped at 400 columns by widening the character cell, so wide engineering drawings come out coarse rather than cropped. - Persian and Urdu keep their ZWNJ. U+200C (zero-width non-joiner) and U+200D are orthography, not junk, and are preserved verbatim; only true invisibles (U+200B, U+200E/F, the bidi overrides, U+FEFF, soft hyphen) are stripped. Tatweel and harakat are preserved too.
- Speed: roughly 1–4 seconds per OCR page at 200 DPI, more at 300+. A 100-page scan is minutes, not seconds. Text-layer pages are effectively instant, which is why
ocr_mode: autois the default.
Developer notes
Local test run (no network, no Apify platform, tesseract binary optional):
pip install -r requirements.txt reportlabpython test_main.py # or: pytest test_main.py
test_main.py covers the pure logic — page specs, OCR TSV parsing, RTL word order (including ZWNJ preservation), script detection, column ordering on real two-column paper geometry plus the negative cases that must NOT split, spatial rendering of oversized pages, render-scale clamping on both axes, input clamping, language resolution, the size guard, the charging plan, the charge-limit and run-deadline guards and the migration resume — plus an end-to-end text-layer run on a reportlab-generated fixture, and a real OCR round-trip that skips itself when tesseract is not on PATH.
Hostile QA fixtures worth running by hand (all under the OCRmyPDF v16.0.0 resources path, deliberately not in the input prefill):
| File | Exercises |
|---|---|
invalid.pdf | 44-byte corrupt file → clean status: "error" |
blank.pdf | blank page → zero words, delivered but uncharged |
rotated_skew.pdf, skew.pdf | auto_rotate |
francais.pdf | non-English OCR |
graph_ocred.pdf | existing invisible GlyphLessFont text layer → auto reuses it |
Runtime notes: the Docker image installs the tesseract binary plus 13 language packs and tesseract-ocr-osd, and sets OMP_THREAD_LIMIT=1 (without it, tesseract's OpenMP parallelism makes single-page OCR slower in a container). TESSDATA_PREFIX is deliberately left unset — the Debian packages already configure the right path. main.py logs pytesseract.get_languages() at startup, which is the fastest way to diagnose a broken image build from the run log. Pages are rasterized with pypdfium2, not PyMuPDF (AGPL) and not pdf2image + poppler (an extra system dependency for no benefit).
Recommended run options: 4096 MB memory, 3600 s timeout. Apify allocates roughly one CPU core per 4 GB, so at 2048 MB OCR runs on half a core — the same compute-unit cost for twice the wall clock.
Other actors by amanatools
- PDF Text Extractor — tables and text from PDF files into clean JSON rows
- ATS Job Scraper — open jobs straight from Greenhouse, Lever, Workday and more
- Data Cleaner — messy CSV, Excel and JSON into clean, typed data
- Doc to Markdown — DOCX, PDF and web pages into clean markdown