How to Extract Data from Arabic PDFs: A Complete Guide for GCC Logistics
Direct answer: Extracting data from Arabic PDFs fails in most tools because of right-to-left (RTL) text order, bidirectional (bidi) mixing with English, and ligature reshaping. pdfmux handles bilingual Arabic-English documents by classifying Arabic-heavy pages up front, routing them to a backend that can actually read Arabic script, and applying a Unicode BiDi reordering pass to whatever the extractor returns. Every page comes back with a confidence score, so you can gate which documents go straight through to a customs or e-invoicing submission and which need a human first.
The GCC logistics document pain
Freight forwarders in Dubai, Jeddah, Doha, and Riyadh handle documents that are structurally hostile to automated extraction. A single shipment produces a stack of PDFs: a Bill of Lading, commercial invoice, packing list, certificate of origin, customs declaration, insurance certificate, delivery order, and various free-zone or ministry forms. A large share of them are bilingual Arabic and English, and a large share arrive scanned rather than digital — photographed on a phone, faxed, or printed and re-scanned. The exact mix varies enough between forwarders that it is worth measuring your own rather than trusting anyone’s average.
That combination — bilingual, partly scanned, high volume, and feeding a submission portal that rejects malformed input — is the worst case for a naive text extractor. It is also the case where getting extraction right pays for itself fastest.
Two regulatory drivers
Saudi ZATCA Phase 2, Wave 24 — now in force. The integration deadline was 30 June 2026, for taxpayers whose VAT-taxable revenue exceeded SAR 375,000 in 2022, 2023, or 2024. That threshold equals the mandatory VAT registration threshold, so in practice the wave reaches essentially every registered taxpayer. This is no longer something to prepare for: invoices must already be clearing through the ZATCA Fatoora platform in XML, with QR codes, digital signatures, UUIDs, and bilingual human-readable content. If you are reading this because submissions are being rejected, that is the situation this post is about.
UAE e-invoicing. The UAE is phasing in Peppol-based e-invoicing using the PINT AE format (built on Peppol BIS 3.0) through a 5-corner Decentralised Continuous Transaction Control and Exchange model, where invoices move through Accredited Service Providers and tax data is reported to the Federal Tax Authority in near real time. Under Ministerial Decision 244 of 2025 the July 2026 window is voluntary; the mandatory phases follow by turnover band through 2027. Getting bilingual field extraction working during the voluntary phase is the cheap time to do it.
Both regimes require machine-readable Arabic and English fields — tax identifier, line items, HS codes, currency, bilingual descriptions. A manual data-entry pipeline meets them with a wall of rejected submissions.
Why Arabic PDFs break most extractors
Three structural problems compound:
1. Right-to-left (RTL) reading order
Arabic reads right to left, but PDF content streams store characters in storage order, not visual reading order. A naive text extractor returns characters in the wrong sequence, so the word comes out reversed or fragmented.
Example. The Arabic word شحنة (shipment) is stored in the content stream as a run of glyphs whose order does not match how a reader scans the line. PyMuPDF’s default get_text() returns them in stream order, which may render as ة ن ح ش. A downstream tool that splits on whitespace then sees gibberish.
2. Bidirectional (bidi) text mixing
A typical GCC Bill of Lading mixes Arabic, English, numbers, and punctuation on the same line:
Port of Loading: ميناء جبل علي (Jebel Ali) - Container MSKU1234567
The Unicode Bidirectional Algorithm (UAX #9) defines how this should render, but storage order in the PDF does not always follow it. Naive extractors produce output that looks correct on screen and parses as nonsense.
3. Arabic ligatures and contextual shaping
Arabic letters change form based on position — initial, medial, final, isolated — and frequently combine into ligatures. A single semantic letter may be encoded as one of four glyphs, or as a multi-letter ligature that has to be decomposed. Text that is not normalised back to canonical code points breaks exact matching, search, and database joins: two visually identical strings compare unequal.
On top of these three, scanned Arabic documents add the usual OCR problems: dots landing on the wrong letter (ب, ت, and ث differ only by dot count and position), broken baselines, and display fonts that were never designed for legibility at low DPI.
How pdfmux handles bilingual extraction
The mechanism is three parts: classify, route, then repair reading order.
1. Classify. Detection sets an is_arabic flag on the document and records which pages carry Arabic script (pdfmux/detect.py). That flag is the highest-priority routing signal — it is checked before “is this scanned” and before “does this have tables”.
2. Route. The classifier maps an Arabic document to the "arabic" page type, and the routing matrix has explicit chains for it, one per strategy:
Strategy (quality=) | Extractor chain for Arabic |
|---|---|
ECONOMY ("fast") | pymupdf |
BALANCED ("standard", default) | llm → pymupdf |
PREMIUM ("high") | llm → pymupdf |
llm resolves to the best available configured provider. The Gemma backend is the only one in the codebase that advertises an arabic capability (pdfmux/providers/gemma.py), so it leads wherever the budget allows, and the chain falls through to native extraction when no provider is configured. Nothing breaks without an API key — you get the free path instead of an error.
This is worth stating plainly because it was broken until recently. The classifier had always returned "arabic", but the routing matrix had no "arabic" rows, so Arabic documents fell through to the default chain and never reached an LLM at all. The route was computed and then thrown away. pdfmux 1.8.7 added the rows. If you are running an older version, Arabic documents are not being routed the way the docs describe.
3. Repair reading order. BiDi reordering is applied post-extraction for every engine, not just the LLM path (pdfmux/pipeline.py). fix_bidi_order works line by line and only touches lines that actually contain RTL characters, so English-only content passes through untouched. It is Markdown-aware: heading # prefixes stay on the left, and in pipe tables each cell is reordered independently so the table structure and any pure-English cells survive.
That split matters for what you should expect. Digital Arabic pages come out correct on the free path, because the text is already there and BiDi is what was missing. It is scanned Arabic that needs the vision model — there is no text layer to reorder.
python-bidi is a core dependency, so BiDi handling works on a plain pip install pdfmux with no extras.
Working with a bilingual Bill of Lading
import pdfmux
from pdfmux.arabic import arabic_ratio, is_arabic_text, normalize_arabic
path = "data/bl-msku1234567.pdf"
# Structured extraction. Returns the locked JSON schema: page_count,
# confidence, warnings, ocr_pages, content, pages.
data = pdfmux.extract_json(path, quality="standard")
print(f"{data['page_count']} pages, confidence {data['confidence']:.0%}")
print(f"Pages that needed OCR: {data['ocr_pages']}")
for page in data["pages"]:
text = page["text"]
if is_arabic_text(text):
print(f"page {page['page']}: {arabic_ratio(text):.0%} Arabic, ocr={page['ocr']}")
Note that confidence here is document-level. Each entry in pages tells you the page number, its text, and whether it needed OCR — but not its own score. For per-page confidence, coverage, and hallucination risk you want verify_extraction, below.
For anything you plan to index, search, or join on, normalise the Arabic first:
# Canonicalises Arabic for search and indexing: strips tashkeel (diacritics)
# and folds letter variants so two visually identical strings compare equal.
key = normalize_arabic(consignee_name_ar)
One rule that catches people: decide once whether a given string is for display or for machines, and stay consistent. BiDi reordering produces visual order, which is what a human needs to read. If you store visually-reordered text in a search index, substring search breaks. Index the logical-order string and let your display layer handle direction.
Mapping extracted fields to the compliance regimes
pdfmux extracts and scores text. It does not ship a typed BillOfLading model or a ZATCA XML writer — the mapping from extracted fields into PINT AE or UBL 2.1 is yours to write, against your own ERP’s field names. What pdfmux gives you for that job is the confidence signal that tells you which documents are safe to map automatically.
The fields the two regimes need from each invoice:
| Requirement | Where it comes from |
|---|---|
| Invoice number, issue date | Header block, usually digital text |
| Supplier and buyer identity, tax registration number | Header block, often bilingual |
| Line items with bilingual descriptions | Table body — the part most likely to be truncated |
| HS / commodity classification code | Table body |
| Tax amount, currency, totals | Footer block |
The line-item table is the risk, and you should treat it as unsolved. A table clipped from 40 rows to 3 still looks like a table, still parses, and still submits — it is just wrong. pdfmux 1.8.7 added a table_truncated verifier flag aimed at exactly this, but be clear about its reach: measured end-to-end it detects 1 of 4 seeded truncations, and it only fires when the source it compares against is itself Markdown. When the source is a PDF — which is your case here — the re-derived source text carries no Markdown table markup, so the check cannot fire at all.
So do not rely on it for customs or e-invoicing line items. Count the rows you extracted against the row count on the document, as a normal validation step in your own mapping code. That is a few lines and it actually runs on PDFs.
Gating submissions by confidence
This is the part most teams skip. Submitting a low-confidence invoice produces a clearance rejection, which the portal counts against your compliance record. Gate before you submit:
import pdfmux
data = pdfmux.extract_json("data/invoice-sa-2026-041.pdf")
CONFIDENCE_FLOOR = 0.90 # tune this against your own corpus — see below
if data["confidence"] >= CONFIDENCE_FLOOR and not data["warnings"]:
submit_to_clearance(map_to_ubl(data))
else:
queue_for_human(path, data["confidence"], data["warnings"])
To audit an extraction — yours or another engine’s — against the source PDF, verify_extraction returns a signed manifest with a verdict and the specific pages that need attention:
from pdfmux import verify_extraction
manifest = verify_extraction("source.pdf", "extracted.json", engine="pdfmux")
print(manifest.verdict) # "PASS" | "REVIEW" | "FAIL"
print(manifest.confidence) # content-weighted overall
print(manifest.silent_drops) # pages that vanished without a warning
print(manifest.review_pages) # 1-indexed, needs a human
silent_drops is the one to watch on Arabic corpora. A page that extracts to nothing is easy to catch; a page that extracts to plausible-looking text in the wrong order is not.
There is no universally correct value for CONFIDENCE_FLOOR. Set it by running a batch you have already verified by hand and finding the threshold that separates the documents you would have accepted from the ones you would have caught.
What we have and have not measured
Being direct about this, because the honest version is more useful than a table:
pdfmux has not run an Arabic-specific accuracy benchmark. There is no measured field-level accuracy figure for Bills of Lading, certificates of origin, or customs declarations, and no head-to-head against Tesseract ara+eng on a GCC document corpus. Any number you see quoted for that is not ours.
What is measured is pdfmux’s overall score on the public opendataloader-bench suite — 200 real-world PDFs: 0.903 overall, #2 of the 8 engines measured. That corpus is financial filings, academic papers, legal contracts, and government reports, in English. It says nothing about Arabic.
The benchmark that matters is yours. The document mix at a Jeddah forwarder and a Jebel Ali forwarder are different enough that a shared average would mislead both. The measurement is an afternoon of work:
- Take 50 representative documents across your actual mix — digital and scanned, BLs and invoices and COOs.
- Extract them at
quality="fast"(free path, BiDi only) and again atquality="standard"(Arabic-aware routing to an LLM backend). - Hand-check the fields you actually submit — not every field, just the ones a rejection would hinge on.
- Compare the confidence distributions. The question you are answering is not “what is the accuracy percentage” but “at what confidence threshold do the documents I would have rejected fall below the line?”
That gives you the two numbers you actually need to run the pipeline: your gate threshold, and the fraction of your corpus that clears it without a human.
What the arithmetic looks like
A worked hypothetical, not a measured customer result. No pdfmux customer’s figures are behind these numbers — plug in your own.
Suppose a forwarder handles 400 shipments a month, spends 60 minutes per shipment on document data entry and verification, and loads staff cost at $20/hour:
400 shipments × 60 min = 400 hours/month
400 hours × $20/hour = $8,000/month
The saving from automation is not the whole $8,000, and any vendor quoting you a number close to it is quoting a number they did not measure. What you actually save is bounded by the share of documents that clear your confidence gate without a human — the fraction you measured in the previous section. If 70% of your corpus clears, you are automating 70% of the 400 hours and adding review time back for the other 30%.
The second-order effect is often larger than the labour line and much harder to estimate: rejected submissions. A rejection costs re-keying, resubmission, and — under ZATCA — a mark against your compliance record. That is exactly what a confidence gate is for. Whether it is worth more than the labour saving depends on your current rejection rate, which you already have and we do not.
Integration patterns for freight forwarders
Pattern 1: watched folder to submission
Extract every PDF in a directory concurrently and split by confidence:
from pathlib import Path
import pdfmux
paths = list(Path("inbox/").glob("*.pdf"))
for result in pdfmux.batch_extract(paths, output_format="json", workers=4):
... # route by confidence, as in the gating example above
batch_extract yields results as they complete and handles non-ASCII filenames correctly — which matters when your inbox is full of Arabic-named attachments.
Pattern 2: WhatsApp to spreadsheet
Freight forwarders in the GCC receive a lot of BLs by WhatsApp. Hook pdfmux into a WhatsApp Business API webhook and write extracted fields to a shared sheet, with the confidence score in its own column so the reviewer knows what to check first.
Pattern 3: pdfmux MCP inside Claude Desktop
For ad-hoc operational work, the pdfmux MCP server lets an ops manager ask Claude:
“Open every PDF in
~/Downloads/shipments-week-16/and tell me which ones have a consignee mismatch between the BL and the commercial invoice.”
Claude calls the extraction tools, cross-checks the fields, and returns a list. No code, no pipeline, just a question.
Conclusion
Bilingual Arabic-English PDFs are not an edge case in GCC logistics. They are the default, and the e-invoicing regimes in Saudi Arabia and the UAE turn a long-standing operational nuisance into a compliance risk.
The three structural problems are known and have known fixes: RTL storage order, bidi mixing, and ligature shaping. pdfmux handles them by classifying Arabic pages before routing, sending them to a backend that can read the script, and applying a Markdown-aware BiDi pass to whatever comes back — then scoring every page so you know which documents are submission-ready and which need a human.
What it does not do is tell you an accuracy number for your documents. Run the 50-document measurement above on your own corpus. That is the test that matters, and it is the only one whose result applies to you.