RTL languages in PDFs: Arabic, Hebrew, and Urdu without the headaches
If you've ever tried to render an Arabic invoice or a Hebrew contract as a PDF, you know the pain. Letters appear disconnected, numbers flip in the wrong direction, mixed English-Arabic strings explode, and the whole document looks like it was assembled by someone who has never seen the language. Most PDF libraries treat RTL as an afterthought, and the result shows.
This post walks through what actually goes wrong with RTL text in PDFs, what you need to make it work, and how to keep your sanity when generating documents in Arabic, Hebrew, or Urdu.
Why RTL is hard in PDFs
PDFs are visual containers. They don't store "this paragraph is Arabic" — they store glyphs at coordinates. That means by the time text reaches the PDF, three separate problems must already be solved:
- Bidirectional ordering (BiDi): Unicode strings are stored in logical order (the order you type), but they must be rendered in visual order. The Unicode Bidirectional Algorithm (UAX #9) handles this.
- Shaping: Arabic and Urdu letters change form based on their position in a word (initial, medial, final, isolated). The string
ع ر ب يis not the same asعربي. - Font support: The font must contain the required glyphs and the OpenType shaping tables (
GSUB,GPOS) that drive ligatures and contextual forms.
Skip any of these and your output will be wrong. Hebrew is the easiest of the three because it doesn't require shaping — but BiDi is still mandatory. Arabic and Urdu need everything.
The font problem
The single most common failure: using a font that lacks proper RTL support. Helvetica, Times, and the default fonts in most PDF libraries do not include Arabic glyphs at all. Even fonts that include Arabic Unicode codepoints often lack the shaping tables.
Reliable choices:
- Noto Naskh Arabic or Noto Sans Arabic (Google) — full shaping support
- Amiri — high-quality traditional Arabic
- Noto Nastaliq Urdu — required for proper Urdu rendering (Nastaliq script)
- Noto Sans Hebrew or Frank Ruhl Libre — Hebrew
Always embed the font in the PDF. Never rely on system fonts on the rendering machine.
A minimal example: pdf-lib + fontkit
If you're rolling your own with pdf-lib, you need @pdf-lib/fontkit to register OpenType fonts, and you need to handle BiDi yourself. Here's a working Hebrew example:
tsimport { PDFDocument, rgb } from "pdf-lib"; import fontkit from "@pdf-lib/fontkit"; import fs from "node:fs/promises"; import bidi from "bidi-js"; const pdf = await PDFDocument.create(); pdf.registerFontkit(fontkit); const fontBytes = await fs.readFile("./NotoSansHebrew-Regular.ttf"); const font = await pdf.embedFont(fontBytes, { subset: true }); const page = pdf.addPage([595, 842]); const logical = "שלום עולם"; const visual = bidi().getReorderedString(logical, { direction: "rtl" }); page.drawText(visual, { x: 500, y: 800, font, size: 24, color: rgb(0, 0, 0), }); await fs.writeFile("hebrew.pdf", await pdf.save());
Notice two things: the text is right-aligned (you compute x from the right edge), and you must run BiDi reordering before drawing. Without it, "שלום עולם" renders backwards.
For Arabic, add a shaping step:
tsimport ArabicReshaper from "arabic-reshaper"; const logical = "مرحبا بالعالم"; const shaped = ArabicReshaper.convertArabic(logical); const visual = bidi().getReorderedString(shaped, { direction: "rtl" }); page.drawText(visual, { x: 500, y: 750, font: arabicFont, size: 24 });
The ArabicReshaper step converts the isolated-form codepoints into their contextual forms. Without it, every Arabic letter renders as a disconnected isolated glyph.
Mixed-direction text
This is where most homemade solutions collapse. Consider an invoice line:
txtInvoice #INV-2024-001 — فاتورة رقم
The Latin text flows left-to-right, the Arabic flows right-to-left, and the entire paragraph has a base direction. UAX #9 defines exactly how this should render, but applying it manually with bidi-js per line works for simple cases. For paragraphs with line wrapping, embedded numerals, and punctuation, you really want a layout engine that understands all of this.
The HTML shortcut
Browsers already implement BiDi, shaping, and font fallback correctly. So one pragmatic approach: render your document as HTML, set dir="rtl" or dir="auto", and convert to PDF with a headless browser.
html<!doctype html> <html lang="ar" dir="rtl"> <head> <meta charset="utf-8" /> <style> @font-face { font-family: "Noto Naskh"; src: url("NotoNaskhArabic-Regular.ttf") format("truetype"); } body { font-family: "Noto Naskh", sans-serif