Arabic PDF API

Build invoices for Saudi, UAE, Egypt
without fighting Unicode.

Right-to-left layout, Arabic-Indic numerals, HarfBuzz shaping, currency placement — every piece of the MENA stack is built into Kamy. Drop-in HTML → PDF for teams that ship to 400M+ Arabic speakers.

Language coverage

Every RTL script we test against — and what wkhtmltopdf does instead.

LanguageDirectionNumeralswkhtmltopdfKamy
Arabic (ar)
Naskh, Kufi, Diwani
RTLArabic-Indic (٠١٢٣٤٥٦٧٨٩)Letters isolated; bidi brokenShaped + bidi correct
Hebrew (he)
Hebrew
RTLHindu-Arabic (0123456789)Direction ignored without manual CSSRTL inferred from lang attr
Persian / Farsi (fa)
Naskh
RTLPersian (۰۱۲۳۴۵۶۷۸۹)Numerals fall back to LatinPersian numerals preserved
Urdu (ur)
Naskh in image; Nastaliq via brand-kit upload
RTLUrdu (۰۱۲۳۴۵۶۷۸۹)Nastaliq joining broken even with the font installedNaskh works out of the box; upload Noto Nastaliq Urdu via brand kit for true Nastaliq
Pashto (ps)
Naskh (Arabic-extended)
RTLArabic-IndicExtra glyphs render as boxesFull Arabic + Pashto extension support
Kurdish (ckb)
Sorani Arabic-script
RTLArabic-IndicLetters do not joinSorani-specific letterforms shaped

Real-world examples

Three documents you can ship today.

Copy-paste these into your codebase. They render correctly the first time.

Saudi VAT invoice — Arabic

saudi-invoice.ts
// Saudi VAT invoice — RTL, SAR on the right, Arabic-Indic dates
import Kamy from "@kamydev/sdk";
import { SiteHeader } from "@/components/marketing/site-header";
import { SiteFooter } from "@/components/marketing/site-footer";

const kamy = new Kamy({ apiKey: process.env.KAMY_API_KEY });

await kamy.render({
  html: `
    <!DOCTYPE html>
    <html dir="rtl" lang="ar">
      <head>
        <style>
          body { font-family: 'Noto Naskh Arabic', serif; padding: 40px; }
          table { width: 100%; border-collapse: collapse; }
          th, td { padding: 12px; border: 1px solid #ddd; text-align: right; }
          .total { font-size: 20px; font-weight: bold; }
        </style>
      </head>
      <body>
        <h1>فاتورة ضريبية — INV-2026-001</h1>
        <p>التاريخ: ٢٤ أبريل ٢٠٢٦</p>
        <table>
          <thead>
            <tr><th>الوصف</th><th>الكمية</th><th>السعر</th></tr>
          </thead>
          <tbody>
            <tr><td>اشتراك Pro شهري</td><td>١</td><td>1,250.00 ر.س</td></tr>
          </tbody>
        </table>
        <p class="total">الإجمالي شامل الضريبة: <strong>1,437.50 ر.س</strong></p>
      </body>
    </html>`,
  options: { format: "a4" },
});

Israeli receipt — Hebrew

israeli-receipt.ts
// Israeli receipt — Hebrew RTL, NIS placement
await kamy.render({
  html: `
    <html dir="rtl" lang="he">
      <body style="font-family: Arial, sans-serif; padding: 32px;">
        <h2>קבלה #4288</h2>
        <p>תאריך: 24 באפריל 2026</p>
        <ul>
          <li>שירות חודשי — ₪199.00</li>
          <li>מע"מ (17%) — ₪33.83</li>
        </ul>
        <p><strong>סך הכל: ₪232.83</strong></p>
      </body>
    </html>`,
  options: { format: "a5" },
});

Iranian / Afghan contract — Persian

persian-contract.ts
// Iranian / Afghan contract — Persian numerals + RTL
await kamy.render({
  html: `
    <html dir="rtl" lang="fa">
      <body style="font-family: 'Vazirmatn', sans-serif; padding: 50px;">
        <h1>قرارداد ارائه خدمات</h1>
        <p>این قرارداد در تاریخ ۲۴ فروردین ۱۴۰۵ منعقد گردید.</p>
        <p>مبلغ ماهانه: <strong>۱۲٬۵۰۰٬۰۰۰ ریال</strong></p>
        <p>مدت قرارداد: ۱۲ ماه از تاریخ امضاء.</p>
      </body>
    </html>`,
  options: { format: "a4", margin: { top: "2cm", bottom: "2cm" } },
});

Five things wkhtmltopdf breaks

The exact failure modes — and how Kamy handles each one.

01

Letters render in isolated form

Symptom

العربية renders as ا ل ع ر ب ي ة (each letter standalone, no joining).

Why it happens

wkhtmltopdf ships an old Qt + Pango stack with HarfBuzz shaping disabled in many builds.

Kamy fix

Kamy uses Chromium's HarfBuzz integration. Initial / medial / final / isolated forms are picked from the font's GSUB table automatically.

02

Numbers flip mid-line

Symptom

1250.00 SAR inside an Arabic paragraph renders with the currency on the wrong side.

Why it happens

Unicode bidirectional algorithm needs to run per-paragraph, not per-element.

Kamy fix

Kamy applies Unicode UAX #9 bidi resolution at the paragraph level. Currency stays grouped with its number regardless of surrounding direction.

03

Persian numerals fall back to Latin

Symptom

Pages set to lang='fa' still show 0123456789 instead of ۰۱۲۳۴۵۶۷۸۹.

Why it happens

wkhtmltopdf doesn't honor the font-feature-settings 'lnum'/'pnum' selectors per locale.

Kamy fix

Kamy preserves whatever numeral system you ship in the HTML. Pass Persian or Arabic-Indic digits and they render exactly.

04

Mixed-direction lines reverse

Symptom

An English brand name inside an Arabic line scrambles the entire line's word order.

Why it happens

Naive RTL implementations reverse the whole line instead of running bidi resolution.

Kamy fix

Kamy resolves direction per Unicode bidi run. English, Arabic, and numbers can sit on the same line in any order and render correctly.

05

Nastaliq Urdu joins are broken

Symptom

Urdu text with Nastaliq fonts renders as flat Naskh-style letters.

Why it happens

Nastaliq needs OpenType contextual substitution rules (calt, init, medi, fina, liga).

Kamy fix

Upload Noto Nastaliq Urdu (or any TTF / WOFF / WOFF2) via your brand kit — Kamy injects @font-face on every render and Chromium's HarfBuzz pipeline handles the full Nastaliq contextual-substitution chain.

Beyond RTL

Every Unicode script HarfBuzz can shape — and a brand-kit upload for everything else.

Kamy renders via headless Chromium, so any Unicode script with a font available shapes correctly. Major scripts ship in the render image. For niche fonts — Nastaliq Urdu, your corporate typeface, color-emoji variants — upload a TTF / WOFF / WOFF2 once via your brand kit and Kamy injects @font-face on every render.

RTL — verified out of the box

Arabic, Hebrew, Persian, Pashto, Kurdish

Arabic-script (Naskh, Kufi), Hebrew, Persian, Sorani Kurdish.

Shaping

ICU bidi + HarfBuzz shaping run by default. dir="rtl" auto-detected when ≥1% of letter-bearing chars are RTL.

Font story

Noto Naskh Arabic + Noto Sans Hebrew ship in the render image. Ready today, zero setup.

RTL — calligraphic & niche

Urdu Nastaliq, Arabic Diwani, Sindhi

Highly contextual scripts requiring OpenType joining substitutions.

Shaping

HarfBuzz contextual substitution (calt, init, medi, fina, liga) runs the full pipeline.

Font story

Naskh-style joining is the default fallback. Upload Noto Nastaliq Urdu, Diwani, or your own font via brand kit — applied to every render.

CJK

Chinese, Japanese, Korean

Simplified + Traditional Chinese, Japanese (Kanji, Hiragana, Katakana), Korean (Hangul, Hanja).

Shaping

Chromium handles CJK shaping, vertical writing mode (writing-mode: vertical-rl), and ruby annotations.

Font story

Test a sample render against your text first. If glyphs fall back to .notdef boxes, upload Noto Sans CJK or your brand typeface via brand kit.

Indic

Devanagari, Tamil, Bengali, Gujarati

Conjunct clusters, half-forms, vowel signs (matras), virama suppression.

Shaping

HarfBuzz Indic shaper handles akhn, half, pstf, vatu, blwf substitutions per script.

Font story

Upload Noto Sans Devanagari / Tamil / Bengali / Gujarati (or your own) via brand kit. Latin parts of mixed-script docs always render correctly.

Southeast Asian

Thai, Lao, Khmer, Burmese

Tone-mark stacking, no-space line-breaking via ICU dictionary segmentation.

Shaping

ICU dictionary-based line breaker runs automatically for Thai / Khmer / Lao paragraphs — no manual word boundaries needed.

Font story

Upload Noto Sans Thai / Lao / Khmer / Myanmar via brand kit for guaranteed glyph coverage.

Custom typography & color

Brand fonts, color emoji, OpenType features

Variable fonts, kerning, ligatures, small-caps, color fonts (COLR/CPAL, SVG-in-OT).

Shaping

Full Chromium font-feature-settings, font-variation-settings, and color-font support.

Font story

Upload your corporate typeface once via brand kit, applied to every render. Or pass theme.fontUrl per render for ad-hoc overrides.

Ship your first Arabic PDF in two minutes.

Free tier includes 100 renders / month. Paid plans start at $19/mo with no per-language surcharge — Arabic, Hebrew, Persian, and Urdu are all on every plan.

Arabic PDF API — render RTL invoices, receipts & contracts | Kamy — Kamy