Hello, tech enthusiasts! 👋
Have you ever tried to make an Arabic PDF with a Python PDF library? Many people try it once and never again. The letters come out separated, like “م ر ح ب ا” instead of “مرحبا”. The words go left to right. Numbers jump to the wrong place.
This happens because Arabic needs two special things. The first is shaping: each letter changes its form depending on its neighbours. The second is right-to-left (RTL) direction. Many PDF libraries do not handle these well, so you need extra packages and tricks.
But there is a program that already handles Arabic perfectly: your web browser. So today we will write the report as a simple web page (HTML), and ask Chrome to print it to PDF. No tricks needed.
What you need
- Python 3.9 or newer (https://www.python.org)
- Playwright for Python, a tool that controls a browser from code (https://playwright.dev)
- An Arabic font file, like Cairo or Noto Naskh Arabic, from Google Fonts (https://fonts.google.com)
Step 1: Install Playwright and Chromium
Headless Chrome means Chrome running with no window. It does all the work in the background. Playwright downloads its own copy of Chromium (the open source version of Chrome).
pip install playwright
python -m playwright install chromium
Step 2: Prepare the font
Download the Cairo font family and put Cairo-Regular.ttf and Cairo-Bold.ttf in a folder called fonts, next to your script. Using a local font file is better than loading it from the internet. The PDF will look the same every time, even with no connection.
Your folder should look like this:
report_project/
make_report.py
fonts/
Cairo-Regular.ttf
Cairo-Bold.ttf
Step 3: Write the HTML
Two small things make the page Arabic-friendly:
dir="rtl"tells the browser to write from right to left.lang="ar"tells it the text is Arabic.
We also use CSS @page. CSS is the styling language of web pages, and @page controls the printed page: its size and margins.
from pathlib import Path
HERE = Path(__file__).parent
rows = [
("يناير", 12500),
("فبراير", 9800),
("مارس", 14250),
]
table_rows = "\n".join(
f"<tr><td>{month}</td><td>{amount:,}</td></tr>" for month, amount in rows
)
html = f"""<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="utf-8">
<style>
@font-face {{ font-family: "Cairo"; src: url("fonts/Cairo-Regular.ttf"); font-weight: 400; }}
@font-face {{ font-family: "Cairo"; src: url("fonts/Cairo-Bold.ttf"); font-weight: 700; }}
@page {{ size: A4; margin: 20mm 15mm; }}
body {{ font-family: "Cairo", sans-serif; font-size: 12pt; color: #222; }}
h1 {{ font-weight: 700; color: #1a5f7a; }}
table {{ width: 100%; border-collapse: collapse; margin-top: 10mm; }}
th, td {{ border: 1px solid #ccc; padding: 6px 10px; text-align: right; }}
th {{ background: #eef5f8; }}
.num {{ direction: ltr; unicode-bidi: embed; }}
</style>
</head>
<body>
<h1>تقرير المبيعات الشهري</h1>
<p>هذا تقرير تجريبي يوضح مبيعات الربع الأول.</p>
<table>
<tr><th>الشهر</th><th>المبيعات</th></tr>
{table_rows}
</table>
<p>رقم الطلب: <span class="num">INV-2026-0042</span></p>
</body>
</html>"""
html_path = HERE / "report.html"
html_path.write_text(html, encoding="utf-8")
In a Python f-string, we write {{ and }} to get normal curly brackets in the CSS.
The .num class is a small helper. It keeps mixed codes like “INV-2026-0042” in left-to-right order inside Arabic text.
Step 4: Print it to PDF
Now Playwright opens the HTML file in headless Chromium and saves a PDF. We open the file from disk, so the browser can find the fonts folder next to it.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(html_path.resolve().as_uri())
# Wait until the fonts are fully loaded before printing
page.evaluate("() => document.fonts.ready.then(() => true)")
page.pdf(
path=str(HERE / "report.pdf"),
format="A4",
print_background=True, # keep background colours
prefer_css_page_size=True, # use the @page settings from CSS
)
browser.close()
print("Saved report.pdf")
Put Step 3 and Step 4 in the same file, make_report.py, and run it. Open report.pdf. The letters are joined, the text flows from the right, and the table looks clean. 🎉
Why this is easier than PDF libraries
- The browser does Arabic shaping and RTL for you.
- You style with CSS, which has many free examples online.
- You can open
report.htmlin your normal browser to check the design before printing. - Tables, colours and page breaks work like any web page.
Common problems
- Squares or a plain font instead of Cairo → The font path is wrong. Make sure the
fontsfolder sits next toreport.html, and the file names match. - Background colours are missing → Add
print_background=True. - Codes or English words appear in the wrong order → Wrap them in a
<span class="num">as shown above. - Error: Executable doesn’t exist → You skipped the browser download. Run
python -m playwright install chromium.
Wrap-up
Instead of fighting with a PDF library, you let the browser do what it already does well. Write HTML, style it with CSS, and print it with one Playwright call.
Next idea: use a template library like Jinja2 to fill the HTML from an Excel file, and create one PDF per customer in a loop.