Handling Right-to-Left Text in an AI-Generated PDF
10 min read · updated August 11, 2026
The Arabic looked correct in the browser, in the API response and in the log. It is wrong in the PDF. That is not a rendering regression: a PDF content stream positions glyphs, and glyph positioning is what happens after the bidi algorithm and the shaper have already run. If nothing in your stack ran them, the file records that.
Three symptoms, two different causes
- Letters are disconnected. Arabic appears as a row of isolated, unjoined shapes, like English written with a space between every letter. This is a shaping failure and has nothing to do with direction.
- Words are in the wrong order, letters within them correct. A reordering failure. The text was written in logical order into a format that expects visual order.
- Both at once, which is the usual case, because a library that does not shape usually does not reorder either.
A fourth symptom — boxes, tofu, or nothing at all — is the font not containing the glyphs, and is a font-embedding problem rather than either of these. Solve that first; the other two are invisible until characters render.
Why PDF has no direction
A PDF page is a content stream of drawing operators. Text is drawn with operators such as Tj and TJ, which take a string of glyph codes and paint them at the current text position, advancing by each glyph’s width. The advance is to the right. There is no “direction” parameter, no paragraph, and no notion of a bidi class anywhere in the imaging model.
So a right-to-left line in a PDF is a sequence of glyphs written in visual order: rightmost glyph first in the drawing order only if the producer also moved the text position, or — more usually — the string contains the glyphs in reverse of their logical sequence and is painted left to right as normal. Either way, whoever produced the file made the decision. The viewer does not reorder. Adobe Acrobat, pdf.js and your operating system’s preview all faithfully draw what is there.
This is also why text extraction from an Arabic PDF gives you visual order and needs to be undone before indexing — the topic of reordering extracted Arabic text and, for retrieval pipelines, RAG over RTL PDFs.
Shaping is a separate job from reordering
Arabic script is cursive. A letter takes one of four contextual forms — isolated, initial, medial, final — depending on what it joins to, and certain sequences form obligatory ligatures, the lam-alef being the one everybody notices. Deciding which glyph to draw for a given character in a given context is shaping, and it is performed by a text shaping engine reading the font’s OpenType GSUB and GPOS tables. HarfBuzz is the shaper almost everything uses.
Reordering and shaping are independent and both are required. Reordering without shaping gives you correctly ordered disconnected letters. Shaping without reordering gives you beautifully joined nonsense. The order matters too: the bidi algorithm operates on characters, the shaper on the reordered runs, and running them the other way round produces subtly wrong joining at run boundaries.
The historical workaround — mapping characters to the Arabic Presentation Forms blocks (U+FB50 onward and U+FE70 onward) before writing them — exists because those blocks contain one code point per contextual form, so a naive renderer draws the right shape. It works, and it produces a PDF whose extracted text is presentation-form characters rather than real Arabic letters, which is useless for search, copy-paste and accessibility. Unicode itself describes those blocks as present for compatibility only. Use it when the toolchain leaves no alternative and know what you gave up.
Which layer does the work in your stack
The practical question is whether your generator delegates to a real text engine or writes glyphs itself.
- Headless browser rendering — Puppeteer, Playwright or an equivalent printing an HTML page to PDF. The browser runs the full bidi algorithm and HarfBuzz before printing, so if the HTML renders correctly the PDF does too. This is the least work and the answer for most AI-generated documents, because the model is already good at emitting HTML.
- CSS engines with a text stack — WeasyPrint and similar route through Pango or HarfBuzz and handle both jobs.
- Direct PDF writers — ReportLab, jsPDF, pdfmake, PDFKit and their kin. These position glyphs. They generally do not shape and do not reorder, so you must do both before handing them a string: a bidi implementation to produce visual order, and a reshaper to produce the contextual forms. In Python that pairing is conventionally
python-bidiwitharabic-reshaper; in JavaScript, a bidi library plus a font pipeline that shapes. - Template fillers — filling form fields in an existing PDF. The field’s own settings and the embedded font decide, and a template authored in an LTR tool will have neither the right font nor a right-aligned field.
Getting it right in one pass
- Embed a font that actually covers the script and has the OpenType tables. A font with Arabic code points but no GSUB will render isolated forms forever. Subset carefully: aggressive subsetting that drops the layout tables reintroduces the disconnection.
- Decide where reordering happens and make it the only place. Two layers each doing it produces the double-reversal described in why numbers look reversed.
- Set the paragraph direction explicitly rather than relying on first-strong detection. A paragraph that opens with an invoice number or an English brand name will be detected as left-to-right and every neutral in it will resolve wrongly.
- Use logical-direction layout for the page itself — right-aligned columns, mirrored margins, the total in the correct corner. Text direction and layout mirroring are separate settings and a library that fixes one will not fix the other.
- Verify by extracting text from the finished PDF and comparing it to the source string after normalisation. If extraction returns presentation-form characters or reversed text, the file will look right and be unsearchable.
If you are producing something structured rather than prose, the layout decisions are as important as the text ones — building an RTL invoice template works one all the way through.