Arabic PDF Reports from HTML with Headless Chrome


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

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.html in 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 fonts folder sits next to report.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.