{"id":60,"date":"2026-01-25T11:30:00","date_gmt":"2026-01-25T08:30:00","guid":{"rendered":"https:\/\/fsaeed.blog\/?p=60"},"modified":"2026-10-31T00:18:51","modified_gmt":"2026-10-30T21:18:51","slug":"arabic-pdf-reports-headless-chrome","status":"publish","type":"post","link":"https:\/\/fsaeed.blog\/index.php\/2026\/01\/25\/arabic-pdf-reports-headless-chrome\/","title":{"rendered":"Arabic PDF Reports from HTML with Headless Chrome"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Hello, tech enthusiasts! \ud83d\udc4b<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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 &#8220;\u0645 \u0631 \u062d \u0628 \u0627&#8221; instead of &#8220;\u0645\u0631\u062d\u0628\u0627&#8221;. The words go left to right. Numbers jump to the wrong place.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This happens because Arabic needs two special things. The first is <strong>shaping<\/strong>: each letter changes its form depending on its neighbours. The second is <strong>right-to-left<\/strong> (RTL) direction. Many PDF libraries do not handle these well, so you need extra packages and tricks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What you need<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Python 3.9 or newer (<a href=\"https:\/\/www.python.org\">https:\/\/www.python.org<\/a>)<\/li>\n\n\n<li>Playwright for Python, a tool that controls a browser from code (<a href=\"https:\/\/playwright.dev\">https:\/\/playwright.dev<\/a>)<\/li>\n\n\n<li>An Arabic font file, like Cairo or Noto Naskh Arabic, from Google Fonts (<a href=\"https:\/\/fonts.google.com\">https:\/\/fonts.google.com<\/a>)<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Install Playwright and Chromium<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Headless Chrome<\/strong> 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).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pip install playwright\npython -m playwright install chromium<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Prepare the font<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Download the Cairo font family and put <code>Cairo-Regular.ttf<\/code> and <code>Cairo-Bold.ttf<\/code> in a folder called <code>fonts<\/code>, 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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your folder should look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>report_project\/\n  make_report.py\n  fonts\/\n    Cairo-Regular.ttf\n    Cairo-Bold.ttf<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Write the HTML<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Two small things make the page Arabic-friendly:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>dir=\"rtl\"<\/code> tells the browser to write from right to left.<\/li>\n\n\n<li><code>lang=\"ar\"<\/code> tells it the text is Arabic.<\/li>\n\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">We also use CSS <code>@page<\/code>. CSS is the styling language of web pages, and <code>@page<\/code> controls the printed page: its size and margins.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from pathlib import Path\n\nHERE = Path(__file__).parent\n\nrows = [\n    (\"\u064a\u0646\u0627\u064a\u0631\", 12500),\n    (\"\u0641\u0628\u0631\u0627\u064a\u0631\", 9800),\n    (\"\u0645\u0627\u0631\u0633\", 14250),\n]\n\ntable_rows = \"\\n\".join(\n    f\"&lt;tr&gt;&lt;td&gt;{month}&lt;\/td&gt;&lt;td&gt;{amount:,}&lt;\/td&gt;&lt;\/tr&gt;\" for month, amount in rows\n)\n\nhtml = f\"\"\"&lt;!DOCTYPE html&gt;\n&lt;html dir=\"rtl\" lang=\"ar\"&gt;\n&lt;head&gt;\n&lt;meta charset=\"utf-8\"&gt;\n&lt;style&gt;\n  @font-face {{ font-family: \"Cairo\"; src: url(\"fonts\/Cairo-Regular.ttf\"); font-weight: 400; }}\n  @font-face {{ font-family: \"Cairo\"; src: url(\"fonts\/Cairo-Bold.ttf\"); font-weight: 700; }}\n  @page {{ size: A4; margin: 20mm 15mm; }}\n  body {{ font-family: \"Cairo\", sans-serif; font-size: 12pt; color: #222; }}\n  h1 {{ font-weight: 700; color: #1a5f7a; }}\n  table {{ width: 100%; border-collapse: collapse; margin-top: 10mm; }}\n  th, td {{ border: 1px solid #ccc; padding: 6px 10px; text-align: right; }}\n  th {{ background: #eef5f8; }}\n  .num {{ direction: ltr; unicode-bidi: embed; }}\n&lt;\/style&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;h1&gt;\u062a\u0642\u0631\u064a\u0631 \u0627\u0644\u0645\u0628\u064a\u0639\u0627\u062a \u0627\u0644\u0634\u0647\u0631\u064a&lt;\/h1&gt;\n  &lt;p&gt;\u0647\u0630\u0627 \u062a\u0642\u0631\u064a\u0631 \u062a\u062c\u0631\u064a\u0628\u064a \u064a\u0648\u0636\u062d \u0645\u0628\u064a\u0639\u0627\u062a \u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644.&lt;\/p&gt;\n  &lt;table&gt;\n    &lt;tr&gt;&lt;th&gt;\u0627\u0644\u0634\u0647\u0631&lt;\/th&gt;&lt;th&gt;\u0627\u0644\u0645\u0628\u064a\u0639\u0627\u062a&lt;\/th&gt;&lt;\/tr&gt;\n    {table_rows}\n  &lt;\/table&gt;\n  &lt;p&gt;\u0631\u0642\u0645 \u0627\u0644\u0637\u0644\u0628: &lt;span class=\"num\"&gt;INV-2026-0042&lt;\/span&gt;&lt;\/p&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\"\"\"\n\nhtml_path = HERE \/ \"report.html\"\nhtml_path.write_text(html, encoding=\"utf-8\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In a Python f-string, we write <code>{{<\/code> and <code>}}<\/code> to get normal curly brackets in the CSS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>.num<\/code> class is a small helper. It keeps mixed codes like &#8220;INV-2026-0042&#8221; in left-to-right order inside Arabic text.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Print it to PDF<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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 <code>fonts<\/code> folder next to it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from playwright.sync_api import sync_playwright\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch()\n    page = browser.new_page()\n    page.goto(html_path.resolve().as_uri())\n\n    # Wait until the fonts are fully loaded before printing\n    page.evaluate(\"() =&gt; document.fonts.ready.then(() =&gt; true)\")\n\n    page.pdf(\n        path=str(HERE \/ \"report.pdf\"),\n        format=\"A4\",\n        print_background=True,    # keep background colours\n        prefer_css_page_size=True,  # use the @page settings from CSS\n    )\n    browser.close()\n\nprint(\"Saved report.pdf\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Put Step 3 and Step 4 in the same file, <code>make_report.py<\/code>, and run it. Open <code>report.pdf<\/code>. The letters are joined, the text flows from the right, and the table looks clean. \ud83c\udf89<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why this is easier than PDF libraries<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The browser does Arabic shaping and RTL for you.<\/li>\n\n\n<li>You style with CSS, which has many free examples online.<\/li>\n\n\n<li>You can open <code>report.html<\/code> in your normal browser to check the design before printing.<\/li>\n\n\n<li>Tables, colours and page breaks work like any web page.<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Common problems<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Squares or a plain font instead of Cairo<\/strong> \u2192 The font path is wrong. Make sure the <code>fonts<\/code> folder sits next to <code>report.html<\/code>, and the file names match.<\/li>\n\n\n<li><strong>Background colours are missing<\/strong> \u2192 Add <code>print_background=True<\/code>.<\/li>\n\n\n<li><strong>Codes or English words appear in the wrong order<\/strong> \u2192 Wrap them in a <code>&lt;span class=\"num\"&gt;<\/code> as shown above.<\/li>\n\n\n<li><strong>Error: Executable doesn&#8217;t exist<\/strong> \u2192 You skipped the browser download. Run <code>python -m playwright install chromium<\/code>.<\/li>\n\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Wrap-up<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to make clean Arabic PDF reports by writing simple HTML and printing it with headless Chrome from Python.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[20,11,53,12,54],"class_list":["post-60","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-arabic","tag-pdf","tag-playwright","tag-python","tag-reports"],"_links":{"self":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/60","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/comments?post=60"}],"version-history":[{"count":1,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/60\/revisions"}],"predecessor-version":[{"id":73,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/posts\/60\/revisions\/73"}],"wp:attachment":[{"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/media?parent=60"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/categories?post=60"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/fsaeed.blog\/index.php\/wp-json\/wp\/v2\/tags?post=60"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}