Rendering Arabic, CJK, and Cyrillic in PDF invoices with a custom canvas pipeline
The Stack
The Problem
Guestpedia's PMS serves hotels with international guests. Group invoices and reports are exported as PDF and Excel, and guest names arrive in Arabic, Chinese, Japanese, Korean, and Cyrillic. jsPDF ships with the standard 14 PDF fonts that only cover Latin scripts. Embedding full CJK font files would add megabytes per export, and Arabic needs right-to-left layout with contextual letter shaping that jsPDF's text API doesn't perform. The naive result: invoices full of garbled boxes, sent to real hotel guests.
The Approach
Instead of fighting PDF font embedding, I moved text rendering to the browser itself. A custom canvas-based renderer draws each text run using the browser's native text engine, which already handles RTL bidirectional layout, Arabic shaping, and CJK glyphs through system fonts, then composites the rasterized output into the PDF via jsPDF's image API. The renderer detects script per text run, so Latin text still uses jsPDF's native vector fonts (crisp and selectable), and only non-Latin runs go through the canvas path.
Guest names, one export
Under the hood
Detection
A single regex per text run (not per-script) asks whether a run contains anything outside Latin Extended. If it's pure Latin, the function returns null and the caller falls back to jsPDF's vector doc.text(). A separate RTL check (Arabic & Hebrew ranges) sets ctx.direction = 'rtl' and anchors text to the right edge of the canvas.
/[^\u0020-\u024F]/.test(run)Resolution
2× supersampling: the canvas is rendered at twice the target size, then placed into the PDF at dimensions divided by the scale factor, so the rasterized text stays sharp when the invoice is printed.
The painful edge case
Canvas font metrics (px, system sans-serif) don't match jsPDF's (pt, Helvetica), so rasterized text looked a different size from vector text on the same line. The fix: a 0.82 calibration factor on the canvas font size, arrived at by visual trial and error.
The honest trade-off:canvas-rendered text becomes an image (non-selectable and non-searchable in the PDF) and depends on the browser's system fonts. That's acceptable for guest names, but not for body text, which is exactly why Latin runs stay on the vector path.
Key decisions
- layersHybrid renderingLatin via vector fonts, non-Latin via rasterized canvas, keeping file size and text quality balanced.
- manage_searchScript detection per runUnicode-range detection decides the rendering path per text segment, not per document.
- shareShared across exportsThe same renderer serves group invoices, the Sales Additional Charges report, and Excel exports.
Status
Shipped to production. Runs in every Guestpedia client deployment that exports invoices. Source is proprietary, but I'm happy to walk through the architecture in detail. Ask me about bidi text.