PaperPony

Right-to-left text and layout in a PDF

Every glyph in the document is correct and the invoice is laid out backwards. The columns are in the wrong order, or the page number is on the wrong edge, or half the margins moved and half did not.

The same three headings, measured
table columns, left to right
  default   Item@2  Qty@138  Price@250
  dir=rtl   Price@2  Qty@152  Item@264

This page is about the second half of the problem, once the characters are right. If yours are still boxes or the letters look wrong, Hebrew not rendering correctly and Arabic not showing in PDF are the ones to read first, because no amount of layout work fixes a missing glyph.

Direction reverses your columns, and that is the correct answer

Item, Qty, Price becomes Price, Qty, Item. Nothing is wrong: a right-to-left reader starts at the right edge, so the first column belongs there. What catches people is that the fix for a table that looks reversed is almost never to reorder the cells.

Author the row in the order a reader meets it and let direction place it. Reordering the markup instead produces a table that looks right in one direction and is doubly reversed in the other, and you find out when the same template is used for an English customer.

Half your CSS flips and half of it does not

This is where a document ends up half converted. Two boxes inside the same right-to-left container, one given a physical margin and one a logical margin:

Measured off the layout
inside dir="rtl", a 400px box
  margin-left:40px          left=40 right=400
  margin-inline-start:40px  left=0  right=360

margin-left means the left, always, and it stayed there. margin-inline-start means the edge the text starts at, so it moved to the right and took 40 pixels off that side instead.

Every physical property has a logical twin, and a stylesheet that is going to serve both directions wants the second column of this list and none of the first:

Two ways to write the same rule
/* Flips with the document. */
.cell { margin-inline-start: 40px; padding-inline-end: 8px; text-align: start; }

/* Does not, whatever direction says. */
.cell { margin-left: 40px; padding-right: 8px; text-align: left; }

text-align: left deserves its own mention because it looks harmless and is the single most common reason a right-to-left invoice has its amounts on the wrong side. start and end are what you want in a document that can be either.

What you can stop worrying about

The common fear is that identifiers get chopped up by the bidirectional algorithm, so people wrap every order number in markup to protect it. Measured, on a Latin product code sitting in the middle of a right-to-left line:

Read off the layout, left to right
a product code inside a right-to-left line
  in the file   חשבונית INV-2024-42 סופית
  on the page   תיפוס INV-2024-42 תינובשח

The code came through in one piece and in its own direction, because a run of Latin letters and digits is strongly left-to-right and the algorithm keeps it together. The Hebrew on either side reversed, which is what it is supposed to do.

Isolation is worth reaching for when a value could start or end with something neutral, a bare number or a string beginning with punctuation or a bracket, because those take their direction from whatever is next to them and can move. For an identifier with a letter at each end, the algorithm already does the right thing.

How we handle this

PaperPony renders PDFs from HTML through an API, so the direction half of this page is yours and the glyph half is ours. Arabic and Hebrew faces are installed in the render container with fontconfig rules that prefer them, and the build fails if either resolves anywhere else:

Two rows of the build check
ok   fallback ar      -> Noto Sans Arabic (want Noto Sans Arabic)
ok   fallback he      -> Noto Sans Hebrew (want Noto Sans Hebrew)

Header and footer templates are passed through to Chromium as written, which is why the dir attribute above is something you set rather than something we infer. The template guide covers the rest of what those bands can do, and the quickstart is where to start.