What breaks when generating Hebrew PDFs: font embedding, bidi reordering, numbers beside Hebrew, column order and page breaks, plus a visual check list.
Key takeaways
- The font failure is silent. In the run I made, calling `insert_text` in PyMuPDF with the base font `helv` and Hebrew text returned a success value, and extracting the text back gave meaningless characters. Nothing was raised.
- A browser applies the Unicode bidirectional algorithm itself. PyMuPDF does not, so Hebrew has to pass through python-bidi's `get_display` before it is drawn.
- That reordering produces a file that looks right but no longer stores the text in logical order. In my run, the string `שלום 5 ש"ח` came back from text extraction in a different order entirely.
- Puppeteer's header and footer templates do not inherit the document's font. In my run, Hebrew in the page footer fell back to ArialMT until the `@font-face` declaration was moved inside the footer template itself.
- `insert_textbox` returns a negative number when the text does not fit the rectangle. That return value is the only signal that lets you shrink the font automatically instead of losing text.
Three faults produce almost every broken Hebrew document: a font with no Hebrew glyphs, text that never went through bidi reordering, and a number or punctuation mark landing on the wrong side. Each one yields a technically valid file that looks wrong to a reader, which is why the check has to be visual.
Which font is actually embedded in the file?
That is the first question, because a font failure is not reported. The PDF standard's base fonts contain no Hebrew glyphs. In my run, drawing Hebrew text with helv raised nothing and returned no error, and extraction from the resulting file returned characters that were not the text passed in. The same fault in a browser shows up as empty boxes, or as letters in a font you did not choose.
What to do instead:
- Embed a font file that covers the Hebrew range and register it explicitly before drawing. On the PyMuPDF side that is an
insert_fontcall with a path to a TTF file. - On the HTML side, attach the font as an
@font-face, and prefer base64 inside the document over an external URL, because at conversion time there is no guarantee an external resource loads. - Add a fallback in the
font-familylist, but do not rely on it. A fallback is what hides the fault, not what fixes it. - Inspect the produced file: open it and list the fonts it embeds. A font name you did not intend means something fell back.
Who does the reordering, the engine or you?
The answer differs between the two routes, and that is the root of most confusion. A browser implements the Unicode bidirectional algorithm itself, so in HTML it is enough to declare a direction and let it work. PyMuPDF does not implement it, so it draws characters in the order it received them, and a Hebrew string passed in as-is appears reversed.
The usual fix is to pass the string through python-bidi's get_display before drawing, which is what the system these measurements came from does. It is worth understanding the price: the function returns the string in display order, so what the file stores is no longer the logical order.
In my run, שלום 123 ABC came back from get_display with the Latin run and the digits in place and the Hebrew letters reversed. And when I extracted text from a file stamped that way, the string שלום 5 ש"ח did not come back as it went in. A check that relies on copying text out of the file will therefore fail even when the page is perfectly correct. The practical conclusion: the decisive check is visual, and text extraction serves only to catch a font fallback.
Symptom, likely cause, and the check
| What you see | Likely cause | The check |
|---|---|---|
| Empty boxes instead of letters | The font has no Hebrew range | List the fonts embedded in the file |
| Hebrew words reversed | No bidi reordering before drawing | Compare one line against the source on screen |
| A number landing on the wrong side | A mixed value with no explicit direction | Wrap the cell in a direction declaration and compare |
| A full stop or comma at the start of a line | Punctuation taking the surrounding direction | Check the sentence end in every paragraph |
| Text disappearing mid-string | The rectangle is too small and a negative was returned | Check the value returned by insert_textbox |
| A different font at the bottom of the page | The footer template inherits no font from the document | Declare the font inside the template itself |
Numbers, currency and punctuation beside Hebrew
This is the fault a client notices instantly, because a money figure that looks wrong destroys trust in the whole document. Three practical rules:
- Declare an explicit direction on every numeric value inside a Hebrew paragraph rather than relying on context. In CSS that is
direction: ltrcombined withunicode-bidi: embedon the same element. - In tables, attach the declaration to the quantity, price and total cells. A cell holding only a number and a currency sign is exactly where contextual direction produces something unexpected.
- Use tabular figures for numeric columns. That is unrelated to direction, but it removes the ragged look that price tables otherwise get.
The forgotten case: a punctuation mark ending a Hebrew sentence that contains a Latin term takes its direction from the surrounding context. Check the full stop at the end of every paragraph that ends in an English term, because that is where it tends to appear on the wrong side.
Tables, page furniture and page breaks
In a table, column order follows the element's direction. Declaring dir="rtl" on the table flips the columns right to left, which is what a Hebrew document wants. The common defect is a document that declares direction on the body but not on the table, or the reverse, so headers and values end up ordered against each other.
For headers and footers there is a finding worth knowing in advance. Puppeteer takes separate HTML templates for the header and the footer, and in my run an @font-face declared in the document's head did not apply to them: the Hebrew footer fell back to ArialMT. When I moved that same declaration inside the footer template, the embedded font was used. If your footer contains Hebrew, that is its own check.
For page breaks, two rules:
- Mark every block that must not split, such as a signature line or a client details card, with
page-break-inside: avoid. - Do not rely on margins alone. Set the page size and margins in
@pageand ask the converter to honour them, or the footer area can overlap content.
Always test on a document of at least three pages. A one-page document exposes none of these faults. More on Hebrew interfaces generally is in Hebrew and RTL in business software, and on documents sent out for signing in electronic signatures for business and evidence handling for a digitally signed quote.
The visual check list before you send
- Open the file in a PDF reader, not in the preview of the system that produced it.
- Compare one Hebrew line against the source, letter by letter, and confirm it is not reversed.
- Check one money figure that sits beside Hebrew text, including where the currency sign lands.
- Check the full stop at the end of a paragraph that ends in a Latin term.
- Render a three-page document and confirm the footer is identical on every page.
- Confirm the footer renders in the same font as the body.
- Look for text clipped at the edge of a box, especially in long names.
- Confirm the table column order matches between the header row and the data rows.
- Produce the file once with a long client name and once with a short one, and check both.
- List the embedded fonts and confirm none of them is a font you did not intend.
Sources
Frequently asked questions
Why does the text look fine in the browser and broken in the PDF?
Because the browser applies the Unicode bidirectional algorithm and finds a fallback font on its own, while a PDF library draws exactly what it was handed. If the font is not embedded or the reordering never happened, the screen hides the fault and the file exposes it. That is why the check belongs on the file.
Can you verify Hebrew correctness by extracting text from the file?
Not as the deciding check. In my run, a Hebrew string stamped after bidi reordering came back from extraction in a different order than the source, and a Hebrew line produced by the browser came back in display order. Text extraction is useful for catching a font fallback, not for approving the wording.
What do you do when Hebrew text is clipped inside a box?
Detect it in code rather than by eye. `insert_textbox` returns a negative value when the text does not fit; in my run the same rectangle returned about minus 336 for a long string and a positive value for a short one. Shrinking the font in a loop until the value is non-negative is the usual implementation.
Which Hebrew font should you embed?
Any font that covers the Hebrew range, is licensed for embedding, and can ship alongside your code. The system these measurements came from embeds Noto Sans Hebrew as a file inside the repository, so the document renders identically on the server and on a local machine regardless of installed fonts.
Keep reading
Related service
Business Automation
I build custom automations that remove repetitive work end to end.
About the author
Yehonatan Saadia
Freelance automation, web & MVP developer
I'm Yehonatan Saadia, a senior developer who builds business automation, custom websites, and MVPs for small and mid-sized companies across the US, Europe, and Israel. These guides come from real client work, not theory.
Work with meHave a project like this?
Tell me what you're trying to automate or build. I reply within 24 business hours with a few targeted questions, then we walk through it on a free 30-minute call, with no commitment. You come away with a scope, a timeline and a fixed price - or a straight answer that it isn't worth building.
