# Arabic PDFs in production: shaping, bidi, fonts and QA

Arabic text can look correct in a browser and still arrive broken in the PDF. Letters separate. A reference number jumps to the wrong side of a sentence. The fallback font changes between a developer's laptop and the production container. Copying a line from the finished file returns text in a different order.

These are separate failures, which is why a single `dir="rtl"` fix never holds for long. A reliable pipeline has to preserve the source text, establish direction, shape the script with the intended font, apply print rules, keep a usable structure tree and test the generated file.

## Start with semantics, not string reversal

Keep Arabic in Unicode logical order. Do not reverse strings before rendering and do not store presentation forms. The [Unicode Bidirectional Algorithm](https://www.unicode.org/reports/tr9/) is designed to compute display order while the underlying characters remain in logical order. Arabic text becomes bidirectional as soon as it contains a number, an English product name, a URL or a customer reference.

Set language and base direction on the document itself:

```html
<!doctype html>
<html lang="ar" dir="rtl">
  <head>
    <meta charset="utf-8" />
    <title>كشف حساب</title>
  </head>
  <body>
    <h1>كشف الحساب</h1>
    <p>رقم المرجع: <bdi dir="auto">INV-2048-AE</bdi></p>
  </body>
</html>
```

W3C's [guidance for right-to-left HTML](https://www.w3.org/International/questions/qa-html-dir) separates `lang` from `dir`: language metadata does not set direction, and direction does not identify the language. For an Arabic document, `dir="rtl"` belongs on the root element. Runtime text whose direction is unknown can use `dir="auto"`; an isolated value such as an invoice ID belongs in `bdi` so it does not reorder punctuation around it.

This matters most in the dull parts of a document: customer IDs, dates, currency amounts, phone numbers and filenames. Those are also the fields support teams copy into other systems, so visual correctness alone is not enough.

## Direction and shaping solve different problems

Bidirectional layout decides where directional runs appear. Shaping decides which glyphs a font should draw and where to position them. Arabic letters join according to context, while ligatures and diacritics depend on the shaping engine and the font's layout tables.

The W3C's [Arabic and Persian layout requirements](https://www.w3.org/TR/alreq/) treats bidirectional text, context-based shaping, joining, ligatures and diacritics as distinct parts of Arabic-script support. HarfBuzz describes the lower layer in its [shaping manual](https://harfbuzz.github.io/shaping-and-shape-plans.html): it uses the segment's direction, script and language, then applies font data such as GDEF, GSUB and GPOS to produce positioned glyphs.

A PDF library that merely maps each code point to a glyph can produce detached letters even when the direction is correct. Browser rendering avoids much of that low-level work because Chromium already uses a mature text stack. It does not remove the need to control fonts and markup.

## Make the font a deployment dependency

A font installed on a developer machine is not a production plan. Bundle a font that covers the Arabic characters and marks your documents require, confirm that its licence allows embedding, and pin the exact file or package version.

Use a narrow fallback list. A broad system-font stack can silently choose a different Arabic face in CI, a container or a serverless runtime. That changes line breaks and sometimes page count. If the approved font is missing, failing the job is safer than generating a document that only looks slightly wrong.

```css
@font-face {
  font-family: "Product Arabic";
  src: url("/fonts/product-arabic.woff2") format("woff2");
  font-display: block;
}

:root {
  font-family: "Product Arabic", sans-serif;
}

body {
  direction: rtl;
  text-align: start;
  margin-inline: 18mm;
}
```

Logical properties such as `margin-inline`, `padding-inline-start` and `text-align: start` adapt to direction without maintaining a second left/right stylesheet. That follows the model defined by [CSS Writing Modes](https://www.w3.org/TR/css-writing-modes-3/).

Before printing, verify the font rather than assuming `document.fonts.ready` means the intended face won. `document.fonts.check('16px "Product Arabic"', 'مرحبا')` gives you a useful gate. You can also inspect computed styles on representative nodes and keep the font files inside the same deployable unit as the template.

## Treat print CSS as an output contract

Puppeteer's [`Page.pdf()`](https://pptr.dev/api/puppeteer.page.pdf) renders with print CSS. Define the paper size, margins and page-break behavior deliberately:

```css
@page {
  size: A4;
  margin: 14mm 12mm 16mm;
}

.invoice-row,
.summary-card {
  break-inside: avoid;
}

@media print {
  body {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
}
```

Then make generation wait for the same conditions your page needs:

```ts
await page.setContent(html, { waitUntil: "networkidle0" });
await page.bringToFront();
await page.evaluate(async () => {
  await document.fonts.ready;
  const ok = document.fonts.check('16px "Product Arabic"', 'مرحبا');
  if (!ok) throw new Error("Arabic font failed to load");
});

const pdf = await page.pdf({
  preferCSSPageSize: true,
  printBackground: true,
  tagged: true,
  waitForFonts: true,
});
```

The current [Puppeteer PDF options](https://pptr.dev/api/puppeteer.pdfoptions) list `tagged` and `waitForFonts` with defaults of `true`. Set them explicitly anyway. Defaults can change, and an output contract should be visible in code review.

Avoid arbitrary sleeps. A 500 ms delay may hide a race on one machine and fail on another. Wait for fonts, images and application data with explicit promises. Block external network access during rendering unless the template genuinely needs it; a PDF job should not depend on a third-party font CDN staying fast.

## Tagged output is the start of accessibility work

A tagged PDF can preserve headings, lists, tables and reading order for assistive technology. It does not prove that the structure is correct. A visually perfect two-column invoice may expose the right column first, repeat decorative content, or omit alternative text.

The PDF Association's [Tagged PDF Best Practice Guide](https://pdfa.org/resource/tagged-pdf-best-practice-guide-syntax/) is written for developers implementing tagged PDF and PDF/UA. Use it as a structure reference, then test the actual document with an accessibility checker and a screen reader used by your audience. Do not advertise PDF/UA conformance because one library option is enabled.

The source HTML still matters. Use real headings, lists and tables. Keep visual decoration out of the accessibility tree. Give informative images useful alternative text. Set the document title and language. For bilingual sections, put `lang` and `dir` on the smallest element that changes language or base direction.

## Test the file that customers receive

A screenshot of the HTML preview is weak evidence. The released artifact is the PDF byte stream, so make it the test target.

A practical suite has four layers:

1.  **Visual rendering.** Rasterize representative pages and compare them with reviewed baselines. Include dense Arabic, Arabic-Indic and European digits, diacritics, long customer names, mixed IDs, tables and page breaks.
    
2.  **Text extraction.** Run a PDF text extractor and assert that key phrases remain present. This catches documents that look right because text was converted to outlines or encoded badly.
    
3.  **Copy and reading order.** Copy mixed Arabic and Latin lines from the PDF into a plain-text field. Inspect order around brackets, slashes, minus signs and neutral punctuation. Automated extraction helps, but a manual check is still worth keeping in release QA.
    
4.  **Document checks.** Assert page count, paper size, title, language, embedded fonts and the presence of tags. Scan for unexpected fallback fonts.
    

Use synthetic fixtures, not customer documents. Keep one short invoice, one multi-page statement and one hostile mixed-direction sample in the repository. The hostile sample should contain an English SKU at the start of an Arabic line, nested brackets, a URL, Arabic-Indic digits, European digits and a filename. If that file survives, normal documents usually stop being surprising.

![Seven engineering gates in an Arabic PDF production pipeline: semantic source, mixed-text isolation, shaping, fonts, print CSS, accessible structure and release QA.](https://cdn.hashnode.com/uploads/covers/60ecf4a0fc37a15ec15655e8/a8f5b608-a67c-47e2-9c49-df2f721e1409.png align="center")

*Seven gates to a reliable Arabic PDF. Sources:* [*W3C*](https://www.w3.org/TR/alreq/)*,* [*Unicode*](https://www.unicode.org/reports/tr9/)*,* [*HarfBuzz*](https://harfbuzz.github.io/shaping-and-shape-plans.html)*,* [*Puppeteer*](https://pptr.dev/api/puppeteer.pdfoptions) *and* [*PDF Association*](https://pdfa.org/resource/tagged-pdf-best-practice-guide-syntax/)*. Checked 19 August 2026. Credit: SultanByte original editorial artwork.*

## Ship the pipeline, not a lucky sample

Arabic PDF generation is predictable once each layer has its own contract. HTML owns language and direction. The shaping stack owns joined and positioned glyphs. The deployment owns the font files. Print CSS owns the page. Tags carry structure. Tests judge the PDF that leaves the system.

That separation also makes failures easier to diagnose. Detached letters point to shaping or font coverage. Wandering punctuation points to bidi isolation. Changed pagination points to fonts or print CSS. Broken copy and reading order point to structure. You stop patching screenshots and start fixing the layer that failed.

*Cover and infographic credit: SultanByte original editorial artwork.*
