Self-Hosted Arabic OCR: Reading Documents Automatically

For clean printed Arabic, Tesseract or PaddleOCR reads a page on CPU in a second or two, while poor photos and tables need a vision model. Preprocess images first, read the e-invoice QR code before anything else, and keep customer documents on a Saudi server.
Reading Arabic documents automatically is possible today with open tools running on your own server, but the right tool depends on the type of document, not on which tool is most popular. The direct answer: if your documents are printed and clean (contracts generated from Word, official letters scanned at 300 DPI), Tesseract 5 with the best Arabic model or PaddleOCR reads a page on CPU in a second or two with high character-level accuracy. If they are phone photos at odd angles with poor lighting, or contain tables, stamps and handwriting, traditional tools fall apart, and you need a modern vision-language model such as Qwen2.5-VL that reads the image, understands its structure and returns fields directly as JSON, at a higher resource cost. In both cases, three rules improve results more than switching tools: preprocess the image before reading it (rotation, contrast, noise removal), validate extracted fields with arithmetic rules instead of trusting them blindly, and read the QR code on Saudi e-invoices before any OCR, since it carries the VAT number, total and tax amount with perfect accuracy. These documents hold IDs, account numbers and contracts, so their natural home is a server inside the Kingdom rather than an external OCR service.
Why Arabic is harder for OCR
Most OCR engines were designed for separate Latin letters, while Arabic is difficult in four ways:
- Connected script: letters join, and each letter changes shape depending on its position (initial, medial, final, isolated), so a word cannot be split into letters by clear gaps.
- Dots: the difference between ب, ت, ث, ن and ي is one or two dots, so any scanning noise or a stamp over the text turns one letter into another.
- Mixed direction: a right-to-left Arabic line may contain a commercial registration number or email address written left to right, and weak engines reverse the digit order.
- Two digit systems: one invoice can contain both ١٢٣ and 123, so you have to normalize them after reading.
There is also a big gap between printed text and handwriting. Printed Arabic is now read with character accuracy that usually exceeds 95% when the scan is good, while Arabic handwriting is still hard for every open tool, and you should expect uneven results even from large vision models.
Comparing the open tools
| Tool | Type | Printed Arabic | Tables and layout | Resources | Time per page approx. |
|---|---|---|---|---|---|
| Tesseract 5 (tessdata_best) | Classic LSTM | Very good on clean scans | Weak | CPU only | 1 to 3 seconds |
| PaddleOCR | Deep detection and recognition | Good, better with skewed images | Medium through its layout module | CPU or GPU | 1 to 4 seconds |
| EasyOCR | Deep detection and recognition | Medium | Weak | GPU preferred | 2 to 6 seconds on CPU |
| Qwen2.5-VL 7B | Vision-language model | Very good even on poor images | Strong, outputs JSON or tables | GPU with 16GB or more | 3 to 10 seconds on GPU |
These numbers are rough figures for an A4 page and vary a lot with text density and hardware. A 7B vision model does run on CPU, but slowly, at 30 to 90 seconds per page, which is fine for dozens of documents a day and not for thousands. Check each model's license before commercial use, since some recent OCR tools ship under licenses that restrict commercial use.
Preprocessing before reading
The biggest accuracy gain comes from this step, not from switching engines. A page scanned at 150 DPI and skewed by 3 degrees can lose half its accuracy. The basic pipeline with OpenCV:
import cv2, numpy as np
def preprocess(path: str):
img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
h, w = img.shape
if w < 2000: # upscale small images
img = cv2.resize(img, None, fx=2000/w, fy=2000/w,
interpolation=cv2.INTER_CUBIC)
img = cv2.fastNlMeansDenoising(img, h=15)
th = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 31, 15)
coords = np.column_stack(np.where(th == 0))
angle = cv2.minAreaRect(coords)[-1]
angle = angle - 90 if angle > 45 else angle
M = cv2.getRotationMatrix2D((w/2, h/2), angle, 1.0)
return cv2.warpAffine(th, M, (th.shape[1], th.shape[0]),
flags=cv2.INTER_CUBIC, borderValue=255)
The steps are grayscale conversion, upscaling small images, denoising, adaptive thresholding to handle shadows in phone photos, and deskewing. For blue or red stamps over text, try picking the color channel where the stamp disappears before converting to grayscale.
Tesseract in practice
sudo apt install tesseract-ocr
sudo wget -O /usr/share/tesseract-ocr/5/tessdata/ara.traineddata \
https://github.com/tesseract-ocr/tessdata_best/raw/main/ara.traineddata
tesseract page.png out -l ara+eng --psm 6 --oem 1
Use the tessdata_best model instead of the default fast one, as the difference in Arabic is noticeable. The ara+eng option is essential for documents that mix in English company names or email addresses. psm 6 assumes a single block of text and suits letters, while psm 4 works better for invoices with columns. After reading, convert Arabic-Indic digits to Western digits before any further processing:
text = text.translate(str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789"))
Extracting fields from invoices and contracts
Start with the QR code
A Saudi simplified tax invoice carries a QR code encoded as TLV and then Base64, containing the seller name, VAT number, invoice timestamp, total including VAT and the VAT amount. Reading it with pyzbar and decoding the TLV gives you those five fields with perfect accuracy in under 100 milliseconds, with no OCR at all. Make this the first step of your pipeline and fall back to text recognition only when the code is missing or damaged.
Decoding takes a few lines, since each field starts with one byte for the tag, one byte for the length, then the value:
from pyzbar.pyzbar import decode
from PIL import Image
import base64
def read_zatca_qr(path: str) -> dict:
raw = base64.b64decode(decode(Image.open(path))[0].data)
names = {1: "seller", 2: "vat_no", 3: "timestamp", 4: "total", 5: "vat"}
out, i = {}, 0
while i < len(raw):
tag, ln = raw[i], raw[i+1]
out[names.get(tag, tag)] = raw[i+2:i+2+ln].decode("utf-8")
i += 2 + ln
return out
If the function returns five valid fields, the invoice is done without an OCR engine ever touching it, and text recognition is only needed for the line items table if you want that detail.
Then validate with rules
When fields come from OCR or a vision model, do not accept them as is. Three rules catch most errors: the VAT number is 15 digits starting and ending with 3, the pre-tax amount multiplied by 1.15 equals the total within a halala or two, and the date falls in a sensible range. Any invoice that fails a rule goes to human review instead of entering the accounting system wrong.
A vision model for the hard cases
For contracts and invoices without a QR code or with complex tables, ask the vision model for structured output: "Extract from the image the supplier name, VAT number, date, line items table (description, quantity, price) and total. Return JSON only." Serve it through vLLM or Ollama on the same server and run the output through the same validation rules. The ideal pipeline is hybrid: QR first, then PaddleOCR or Tesseract for clean pages, then the vision model only for pages that fail validation, which saves around 80% of the heavy processing time.
Why documents stay on a Saudi server
The documents that go through OCR are the most sensitive in any company: ID scans, contracts, bank statements and supplier invoices. External cloud OCR services mean uploading all of this to another country, which conflicts with the Personal Data Protection Law and with the requirements of many government bodies. Running the pipeline on a cloud server in Riyadh keeps each document on a local path from upload to deletion.
On sizing, a Tesseract or PaddleOCR pipeline with QR reading handles about 2,000 pages a day on the Numuw plan with 2 cores and 4GB at 6,900 SAR per year, and the Business plan with 4 cores and 8GB at 11,500 SAR roughly doubles that. Add 100GB of SSD for 500 SAR per year for every 200 thousand or so pages archived as compressed PDF. If you want a vision model on the same infrastructure, start from the AI hosting templates, which come with Docker and Jupyter ready for testing.
A five-step rollout plan
- Classify your documents: how many are clean prints, how many are phone photos, and how many are invoices with QR codes.
- Build the pipeline with QR first, then image preprocessing, then Tesseract with the best model or PaddleOCR.
- Add validation rules for VAT numbers, totals and dates, and send failures to human review.
- Test on 200 real documents, measure the share of correct fields, then decide whether a vision model is worth its cost for the hard pages.
- Choose your server size from pricing and plans, and delete original images after extraction according to a written retention policy.


