Arabic PDF OCR in Python: extract right-to-left text from scanned documents
Direct answer: To OCR an Arabic PDF in Python, first split the file into digital-text pages and scanned pages, then run OCR only on the scanned ones. Use pytesseract with the ara language pack for standard scans, and reshape the output with a bidirectional (bidi) pass so right-to-left runs display in the correct visual order. Tesseract 5 ships trained data for Arabic and 100+ other scripts, but it struggles with low-contrast scans, decorative fonts, and mixed Arabic-English lines — for those, a multimodal vision model is more reliable. pdfmux does this routing for you: it detects scanned pages, applies OCR where needed, and returns clean Markdown with reading order preserved.
Why Arabic PDFs break most extractors
Three properties of the Arabic script defeat tools that were built and tested on English:
- Right-to-left (RTL) base direction. Arabic reads from right to left, but the underlying character stream in a PDF is stored in logical order, not visual order. Naive extractors emit the bytes as-is, so a line that reads correctly on screen comes out reversed or scrambled in your text file.
- Bidirectional (bidi) mixing. Real documents mix Arabic with English words, Latin product codes, and Western-Arabic digits (0-9). The Unicode Bidirectional Algorithm defines how these runs should be ordered, and if you skip it, embedded English fragments land in the wrong place.
- Contextual letter shaping. Arabic letters change form depending on their position in a word — isolated, initial, medial, or final. A single letter can have up to 4 glyph forms, plus mandatory ligatures like lam-alef. OCR and copy-paste both have to normalise these back to canonical code points.
Add a scanner into the mix and you get a fourth problem: image-based pages carry no text layer at all, so there is nothing to extract until you OCR them. If you have not yet separated scanned pages from digital ones, start with detecting scanned PDFs in Python before you spend money running OCR on pages that already have selectable text.
Step 1: Extract the digital text layer first
Most “Arabic PDFs” are not fully scanned. A customs form or invoice is often a digital template with a few stamped or handwritten regions. Pull the text layer first and only OCR what is missing. PyMuPDF (fitz) reads the embedded text quickly:
import fitz # PyMuPDF
def has_text_layer(page, min_chars: int = 20) -> bool:
"""A page with fewer than 20 extractable characters is treated as scanned."""
return len(page.get_text("text").strip()) >= min_chars
doc = fitz.open("customs-declaration.pdf")
digital_pages, scanned_pages = [], []
for i, page in enumerate(doc):
(digital_pages if has_text_layer(page) else scanned_pages).append(i)
print(f"{len(digital_pages)} digital pages, {len(scanned_pages)} scanned pages")
On a typical 12-page GCC shipping packet you might find 9 digital pages and 3 scanned pages. Routing OCR to only those 3 pages cuts processing time by roughly 75% versus OCR-ing the whole document blindly.
Step 2: OCR the scanned pages with Tesseract
For the scanned pages, render each one to an image and run Tesseract with the Arabic language model. Tesseract is open source and ships Arabic trained data (ara.traineddata) — see the tesseract-ocr project on GitHub for install instructions and the full language list.
import io
import pytesseract
from PIL import Image
def ocr_arabic_page(page, dpi: int = 300) -> str:
pix = page.get_pixmap(dpi=dpi) # 300 DPI is the floor for Arabic diacritics
img = Image.open(io.BytesIO(pix.tobytes("png")))
# 'ara+eng' handles bilingual pages; psm 6 assumes a uniform block of text
return pytesseract.image_to_string(img, lang="ara+eng", config="--psm 6")
for i in scanned_pages:
text = ocr_arabic_page(doc[i])
print(f"--- page {i} ---")
print(text)
Two settings matter for Arabic accuracy:
- DPI. Render at 300 DPI or higher. Arabic relies on small dots and diacritics to distinguish letters (for example ب, ت, and ث differ only by dots), and those disappear at the 72-150 DPI defaults used for Latin text.
- Language chain. Use
ara+engfor bilingual documents so Tesseract can recognise Latin runs instead of mangling them into Arabic glyphs.
Step 3: Fix reading order with a bidi pass
Tesseract returns text in a logical order that will not display correctly in a plain text file or a left-to-right terminal. Reshape it with arabic-reshaper (which resolves contextual letter forms) and python-bidi (which applies the Unicode bidi algorithm):
import arabic_reshaper
from bidi.algorithm import get_display
def normalize_arabic(raw: str) -> str:
reshaped = arabic_reshaper.reshape(raw) # join letters into their contextual forms
return get_display(reshaped) # reorder RTL runs for visual display
clean = normalize_arabic(text)
One caveat worth remembering: get_display is for rendering. If your downstream consumer is a search index or an LLM, store the logical-order string and let the display layer handle direction. Applying get_display before indexing will break substring search. Decide once whether the output is for humans (visual order) or machines (logical order) and stay consistent.
When Tesseract is not enough
Tesseract is a strong default, but it degrades on the documents Arabic-language businesses actually deal with:
| Document condition | Tesseract result | Better option |
|---|---|---|
| Clean 300 DPI scan, standard font | Reliable | Tesseract |
| Low-contrast fax or photocopy | Frequent letter drops | Multimodal vision model |
| Decorative or Kufic display fonts | Poor | Multimodal vision model |
| Dense Arabic-English table | Column/order errors | Layout-aware extractor |
| Handwritten annotations | Not supported | Multimodal vision model |
For the hard cases, a multimodal model that reads the page as an image handles shaping and bidi implicitly, because it was trained on rendered text rather than on a glyph-by-glyph classifier. The tradeoff is cost and latency per page, which is exactly why per-page routing matters — you want the cheap path for the easy 80% of pages and the expensive path only for the pages that need it.
The Markdown output problem
Raw OCR text loses structure: headings, tables, and key-value pairs collapse into a wall of characters. For anything you plan to feed into a retrieval pipeline or an agent, you want structured Markdown, not a text dump. If your documents span several languages, the follow-on guide to multilingual PDF to Markdown covers language detection and mixed-script handling in more depth. For business-specific patterns — Bills of Lading, commercial invoices, and e-invoicing compliance — see the deep dive on Arabic PDF extraction for GCC logistics.
Doing it in one step with pdfmux
The pipeline above — detect scanned pages, OCR selectively, reshape RTL, and emit structured Markdown — is what pdfmux runs by default. You do not classify pages by hand or wire up three separate libraries:
pip install pdfmux
pdfmux convert arabic-invoice.pdf --output invoice.md
from pdfmux import convert
result = convert("arabic-invoice.pdf")
print(result.markdown) # reading order preserved, tables intact
pdfmux auto-detects which pages carry a text layer, runs OCR only on the scanned pages, normalises Arabic shaping and bidi order, and returns Markdown with headings and tables preserved. It covers 100+ languages including Arabic script variants, so a bilingual document does not need any special flags. For the general OCR workflow across all scripts, the companion guide on OCR PDF extraction in Python walks through the CPU-only path in detail.
Checklist for production Arabic OCR
- Render scanned pages at 300 DPI or higher so diacritics survive.
- Use a
ara+englanguage chain for bilingual documents. - Always run a reshape + bidi pass, and decide up front whether the output is for display or for indexing.
- Route by page: extract the digital text layer first, OCR only the scanned pages.
- Escalate low-contrast scans, decorative fonts, and handwriting to a multimodal model instead of forcing Tesseract.
- Normalise Western-Arabic and Eastern-Arabic digits to a single form before you parse numbers.
Arabic OCR is not one hard problem — it is four smaller ones (RTL order, bidi mixing, letter shaping, and scanned-page detection) that each have a known fix. Handle them in the right order and the accuracy gap between Arabic and English narrows to the quality of the scan itself.