PDF Text Extractor — OCR for scanned PDFs (Arabic + English) avatar

PDF Text Extractor — OCR for scanned PDFs (Arabic + English)

Pricing

Pay per event

Go to Apify Store
PDF Text Extractor — OCR for scanned PDFs (Arabic + English)

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

Dos

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

20 hours ago

Last modified

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
}
FieldDefaultNotes
pdf_urlsRequired. Up to 50 direct PDF links per run, 50 MB each. Repeated links are processed once.
pagesallall, 1-5, or 1,3,7-9. Same syntax as pdf-table-extractor.
ocr_modeautoauto | always | never. auto OCRs only pages with under 100 digital characters.
languageautoauto, eng, ara, ara+eng, fra, deu, spa, por, ita, nld, tur, rus, fas, urd.
preserve_layouttrueOff = no blocks, no text_layout, much smaller items.
dpi20072–400. Use 300 for dense or small print.
auto_rotatetrueDetects and fixes sideways / upside-down scans.
min_word_confidence0Drop OCR words below this score. 0 keeps everything.
max_pages_per_document20Also your cost ceiling — see Pricing. Raise it (up to 500) for long scans.
include_failed_documentstrueFailed 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"
}
]
}
]
}
  • text joins pages with a form feed (\f), the pdftotext convention.
  • bbox is [x0, top, x1, bottom] in PDF points, origin at the top-left of the rendered page.
  • column is always the visual left→right band index. Only order flips on right-to-left pages.
  • source is "ocr" or "text-layer" per page, so you can always see what you paid for.

Pricing

Pay-per-event, no start fee, no subscription:

EventPriceWhen
document-processed$0.05Once per PDF that delivered at least one page of text.
ocr-page$0.02Per page that had to be rasterized and OCR'd.
text-page$0.005Per 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 ara model 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 \columnsep is 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_layout plus 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_rotate fired (rotation_applied is 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: true rather than silently dropped.
  • Very large dataset items are trimmed. If a document would exceed Apify's ~9 MB item limit, text_layout is dropped first, then blocks, then text is truncated — and every stage is recorded in truncated_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 pages to work through it in slices if you need full DPI. The text_layout grid 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: auto is the default.

Developer notes

Local test run (no network, no Apify platform, tesseract binary optional):

pip install -r requirements.txt reportlab
python 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):

FileExercises
invalid.pdf44-byte corrupt file → clean status: "error"
blank.pdfblank page → zero words, delivered but uncharged
rotated_skew.pdf, skew.pdfauto_rotate
francais.pdfnon-English OCR
graph_ocred.pdfexisting 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