Rendering Arabic, Hebrew, or Persian text in a PDF comes down to three things: convert HTML with a Chromium-based engine, set dir="rtl" on the container, and load a font that contains the right glyphs. Chromium applies the Unicode bidirectional algorithm and HarfBuzz shaping on its own, so Arabic letters join into words and each line reads right to left without any manual glyph handling. The two failure modes, disconnected letters and empty boxes, both trace back to skipping one of those three steps.
This guide explains what right-to-left and bidirectional text are, how to enable them in HTML, which fonts cover Arabic and Hebrew, how the main HTML-to-PDF engines compare, and how to mix directions in one document.
What is right-to-left and bidirectional text?
Right-to-left (RTL) text is a writing system where the primary reading direction runs from the right edge to the left, used by Arabic, Hebrew, Persian (Farsi), Urdu, and others. Bidirectional (bidi) text is a single line that mixes both directions, for example an Arabic sentence that contains a Western number or an English product name.
The order of characters in memory (logical order) is not the order they appear on the page (visual order). The Unicode bidirectional algorithm, defined in UAX #9, is the standard that maps one to the other. It assigns a direction to each character, groups runs, and decides where a Latin word or a digit sits inside a right-to-left line. Every modern browser engine implements it, which is why an HTML-to-PDF pipeline built on Chromium gets bidi correct by default.
Arabic adds a second requirement on top of ordering: shaping. Arabic letters change form depending on their position in a word (isolated, initial, medial, final), and adjacent letters join. Shaping is handled by HarfBuzz, the text shaping engine Chromium uses. A renderer that lacks shaping draws each code point in its isolated form, which is the source of the classic "disconnected Arabic letters" bug.
How do you enable right-to-left in HTML?
Set the base direction with the HTML dir attribute, and load an Arabic or Hebrew font. For a fully right-to-left document, put dir="rtl" on the html or body element. For a right-to-left section inside a left-to-right page, put it on that section only.
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic:wght@400;700&display=swap"
/>
<style>
body {
font-family: "Noto Naskh Arabic", serif;
font-size: 16px;
line-height: 2;
}
</style>
</head>
<body>
<h1>فاتورة</h1>
<p>شكرا لتعاملكم معنا. المبلغ الإجمالي 1,500 ريال.</p>
</body>
</html>Prefer the dir attribute over the CSS direction property when you can. The MDN reference for dir treats it as document semantics: it carries the base direction of the content rather than a visual style, so it survives copy-paste and is the correct hook for accessibility tools. Reach for the CSS direction and unicode-bidi properties when you need finer inline control, covered in the mixed-content section below.
Always set the lang attribute too (lang="ar", lang="he", lang="fa"). It does not change ordering, but it lets the font engine pick language-appropriate glyph variants and improves the tagging of the output for screen readers.
Which fonts render Arabic and Hebrew in a PDF?
Load a font whose glyph set covers the script you need. A Latin-only font like Inter or Roboto has no Arabic or Hebrew glyphs, so the text renders as empty boxes (called tofu) even when the ordering is correct. The free Noto family from Google covers every script and is the safest default.
| Script | Recommended free fonts | Notes |
|---|---|---|
| Arabic | Noto Naskh Arabic, Amiri, Cairo, Noto Kufi Arabic | Naskh and Amiri are traditional book styles, Cairo is a modern sans |
| Hebrew | Noto Sans Hebrew, Noto Serif Hebrew, Rubik, Frank Ruhl Libre | Rubik and Noto Sans are clean UI-style faces |
| Persian / Urdu | Vazirmatn, Noto Nastaliq Urdu | Nastaliq is the calligraphic style expected for Urdu |
You have two ways to supply the font. Load it over Google Fonts with a <link> tag as shown above, which is the fastest to set up. Or self-host it with an @font-face rule pointing at a WOFF2 or TTF file, which removes the network dependency at render time. Self-hosting is covered in detail in our guide on adding custom fonts to a PDF. When you self-host, wait for the font to load before printing so the first render is not missing glyphs.
How do HTML-to-PDF engines handle right-to-left text?
Chromium-based engines (Playwright, Puppeteer, and APIs built on them) give the most reliable Arabic and Hebrew output because they ship the current Unicode bidi algorithm and HarfBuzz shaping. Older or draw-based tools vary.
| Engine | Bidi ordering | Arabic shaping | Verdict for RTL |
|---|---|---|---|
| Chromium (Playwright / Puppeteer) | Full UAX #9 | HarfBuzz | Recommended, correct by default |
| WeasyPrint | Yes | Yes (HarfBuzz) | Good, verify font coverage |
| wkhtmltopdf (old Qt WebKit) | Unreliable | Unreliable | Avoid for RTL |
| Draw-based libraries (jsPDF, PDFKit) | Manual | Manual or none | Needs a bidi + shaping layer |
The split is between engines that render a full browser layout and libraries that draw glyphs one at a time. Browser-layout engines get bidi and shaping for free because that logic lives in the engine. Draw-based libraries such as jsPDF and PDFKit position glyphs directly, so unless you add a bidi reordering and Arabic shaping step yourself, they output text in logical order with isolated letter forms. wkhtmltopdf is a browser engine, but it is built on a long-unmaintained Qt WebKit fork and its RTL behavior is not dependable, which is one reason teams move off it (see our wkhtmltopdf alternatives roundup).
Engine behavior for edge cases (rare ligatures, stacked diacritics, Nastaliq) still depends on the specific font. Test with your real content and target font before shipping.
Rendering right-to-left PDFs with Playwright or Puppeteer
Set the HTML content, wait for the font, then print. Because the shaping and ordering happen inside Chromium, the code is the same as any HTML-to-PDF job: the only additions are dir="rtl" and an Arabic or Hebrew font.
import { chromium } from "playwright";
const html = `<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Noto+Naskh+Arabic:wght@400;700&display=swap" />
<style>body { font-family: "Noto Naskh Arabic", serif; font-size: 16px; }</style>
</head>
<body>
<h1>فاتورة رقم 001</h1>
<p>المبلغ الإجمالي: 1,500 ريال سعودي</p>
</body>
</html>`;
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle" });
await page.evaluate(() => document.fonts.ready);
await page.pdf({ path: "invoice-ar.pdf", format: "A4", printBackground: true });
await browser.close();The document.fonts.ready wait matters. Without it, page.pdf() can fire before the web font finishes downloading, and the first page renders with a fallback font that may lack Arabic glyphs. Waiting on the FontFaceSet ready promise removes that race.
How do you mix left-to-right and right-to-left content?
Set the base direction per element and let the bidi algorithm order each run. A right-to-left invoice can contain a left-to-right SKU, and a left-to-right report can contain a right-to-left Arabic quote. The key tools are the dir attribute, the bdi element, and dir="auto".
Western digits and short Latin runs inside an Arabic paragraph are handled automatically: the bidi algorithm keeps 1,500 and SAP in left-to-right order inside the right-to-left line. Problems appear when user-supplied content has an unpredictable direction, for example a name field that might be Arabic or English. Wrap that run in a bdi element, which isolates its direction so it cannot flip the surrounding text.
<p dir="rtl">
العميل: <bdi>Acme Corp</bdi> - الطلب رقم <bdi>#4821</bdi>
</p>For finer control, the CSS unicode-bidi: isolate property does the same isolation as bdi, and direction sets the base direction of an inline run. In practice, setting dir on containers plus bdi on unpredictable inline data covers almost every layout. Reserve the raw CSS bidi properties for the rare case where you cannot change the markup.
Common right-to-left PDF bugs and fixes
Most RTL rendering issues fall into a short list, and each maps to one of the three requirements: engine, direction, or font.
| Symptom | Cause | Fix |
|---|---|---|
| Disconnected Arabic letters | No shaping (draw-based library) | Use a Chromium-based renderer with HarfBuzz |
| Empty boxes (tofu) | Font has no Arabic/Hebrew glyphs | Load Noto Naskh Arabic, Amiri, or Noto Sans Hebrew |
| Line reads left to right | Missing base direction | Add dir="rtl" to the container |
| Text stuck to the left edge | Hardcoded text-align: left | Remove it or set text-align: right |
| Number or English word in wrong place | Unisolated bidi run | Wrap it in bdi or dir="auto" |
| First page missing glyphs | Printed before font loaded | Await document.fonts.ready before page.pdf() |
| Punctuation on the wrong side | Neutral character resolved to wrong run | Set dir on the enclosing element explicitly |
Working through this table in order (engine, then direction, then font, then load timing) resolves the large majority of Arabic and Hebrew PDF problems. If output is still wrong after all four, the remaining suspect is a font that lacks a specific ligature or diacritic, which you confirm by swapping in a broad font like Noto and re-testing.
Generate right-to-left PDFs with PDF4.dev
The Playwright and Puppeteer approach works, and for a single Arabic invoice it is fine. At production scale you inherit the operational load: a headless Chromium per instance (roughly 300 MB added to the image), a browser process to keep warm, concurrency limits when several renders arrive at once, and crash recovery when a page hangs. None of that is specific to RTL, but it is the reason many teams move HTML-to-PDF off their own servers.
PDF4.dev runs that Chromium pipeline as an API, so Arabic and Hebrew rendering is a POST request. You send HTML with dir="rtl" and pass a Google Fonts URL in the format options, and the response is the finished PDF. The same free HTML-to-PDF tool lets you paste an RTL document and download the result to confirm shaping before you wire up the API.
Try it without writing infrastructure code: paste an Arabic or Hebrew HTML document into the HTML to PDF tool, or get a free API key and pass google_fonts_url in the format options to load Noto Naskh Arabic. Same Chromium engine, none of the browser-pool ops.
The decision axis is not capability, since both paths use the same engine and produce identical shaping. It is whether you want to operate a browser pool. For RTL specifically, the important part is upstream of the choice: use a Chromium-based renderer, set the direction, and load the right font.
Related reading
For the wider HTML-to-PDF workflow, start with the complete guide to converting HTML to PDF. To self-host Arabic or Hebrew fonts instead of loading them over the network, see how to add custom fonts to a PDF. And for controlling page breaks, margins, and print layout around your RTL content, read the CSS print styles guide.
This article covers a technical topic where exact behavior depends on your renderer version and font. Test with your real content before shipping to production.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



