PaperPony

Hebrew not rendering correctly in a PDF

The Hebrew in your PDF is not in the order you wrote it. Every letter is there, none of them are boxes, and the line reads backwards.

Measured off the layout
in the file   חשבונית 42
on the page   42 תינובשח

That output is correct and there is nothing to fix in it. Hebrew is stored in the order it is read and drawn in the opposite direction, so the file and the page always disagree, and comparing the two is not a test of anything. Before changing a font, find out which of these you actually have.

dir changes one of these lines and not the other

The advice you will find is to add dir="rtl". Here is that attribute doing nothing at all:

Hebrew followed by a number
no dir attribute
  on the page   42 תינובשח

dir="rtl"
  on the page   42 תינובשח

Identical. Hebrew letters carry strong right-to-left direction of their own, so a run of them lays itself out correctly whatever the paragraph says, and the trailing number comes along. Most single fields on an invoice look like this, which is why a document can go a long way with no direction set and no sign of trouble.

Then a Latin word arrives at the end of the line:

The same attribute on a total
no dir attribute
  in the file   סה"כ 1,250.00 ILS
  on the page   1,250.00 כ"הס ILS

dir="rtl"
  in the file   סה"כ 1,250.00 ILS
  on the page   ILS 1,250.00 כ"הס

Now it decides everything. Without the attribute the paragraph is left-to-right, so ILS keeps the right-hand end and the Hebrew is squeezed between the amount and the currency. With it, the line runs right to left the whole way and the currency lands where a reader expects it. Same characters, same font, same three tokens, and only one of the two is the document you meant.

So the answer to whether you need dir is that you cannot tell from a field that works. Set it on the document and stop thinking about it.

The font is a smaller problem here than in the other scripts

Hebrew is the script where the fallback is usually fine. Reading DejaVu's own character map, which is what a Debian image hands you when nothing else is configured:

Coverage from the cmap table
DejaVu Sans
  Hebrew letters                   22/22
  Final forms                      5/5
  Niqqud (vowel points)            12/12
  Cantillation marks               0/6   missing: ֑ ֒ ֓ ֠ ֡ ֯
  Punctuation                      5/5

Letters, final forms, vowel points and punctuation all complete. The only gap is cantillation, the marks used in biblical text, which no invoice has ever needed. If your Hebrew is legible and merely disordered, the font is not what you are looking for.

What your own image can draw
docker run --rm your-image:tag sh -c 'fc-list :lang=he family | sort -u'

An empty answer there is the one case where the font really is the problem, and it means the image has no Hebrew at all. That happens when a browser package is installed with --no-install-recommends and leaves only Liberation behind. Missing fonts in headless Chrome covers that case for every script.

Naming the Hebrew face is how you lose your digits

The obvious move once you know the family exists is to put it in your CSS. Its character map is worth reading first:

The same probe, run on the Hebrew face
Noto Sans Hebrew
  Hebrew letters                   22/22
  Latin letters                    0/6    missing: A B C a b c
  Digits                           0/10   missing: 0 1 2 3 4 5 6 7 8 9
  Currency and punctuation         1/6    missing: € $ . , :

Twenty-two letters and no digits. No Latin, no full stop, no comma, no dollar or euro sign. It is a script face and it carries the script, which means font-family: 'Noto Sans Hebrew' on its own hands every amount on the invoice to whatever the browser finds next.

You can watch that happen in the finished file. One line of text, three tokens, two embedded fonts:

A single line of Hebrew and a number
$ pdffonts one-line.pdf
name                                 type              encoding         emb sub uni object ID
------------------------------------ ----------------- ---------------- --- --- --- ---------
AAAAAA+LiberationSerif               CID TrueType      Identity-H       yes yes yes      4  0
BAAAAA+NotoSansHebrew-Regular        CID TrueType      Identity-H       yes yes yes      5  0

The Hebrew came from Noto Sans Hebrew and the digits came from Liberation Serif, which is the default serif and not a choice anybody made. On a well-populated image that fallback is invisible. On a thin one your Hebrew is perfect and your totals are boxes, which is the reverse of every other guide here.

A stack, not a family
/* The Hebrew face cannot draw a digit or a full stop. Give it
   somewhere to fall through to, or let fontconfig do it. */
body { font-family: 'Noto Sans Hebrew', 'Noto Sans', sans-serif; }

How to check it without reading Hebrew

The instinct is to extract the text and compare it against the string you sent. That check cannot work, and here is why:

The line whose source is: total 1,250.00 ILS
$ pdftotext one-line.pdf -
ILS 1,250.00 ‫סה"כ‬

The runs come back in the order they sit on the page rather than the order you wrote them, with bidi embedding marks around the Hebrew. So the extracted string legitimately differs from your input and always will, in both the correct and the broken document.

Two checks that do work. Look at which token is furthest left in the extracted output: for a right-to-left line it should be the last thing you wrote, and if it is the first, the paragraph is running the wrong way. And run pdffonts, because a Hebrew document that embeds no Hebrew face is a font problem, while one that embeds two faces is the fallback above and is fine.

How we handle this

PaperPony renders PDFs from HTML through an API. Noto Sans Hebrew is installed in the render container with a fontconfig rule that prefers it over DejaVu, and the build asks for it by language and then checks that the face it named is the one that drew the string:

The Hebrew rows of the build check
ok   fallback he      -> Noto Sans Hebrew (want Noto Sans Hebrew)
ok   render   hebrew      default=81.3 named=77.4 drift=5.0%

Direction is yours to set, in the document, because it is a property of what you are writing rather than of the renderer. The quickstart is where to start.