Case Study · Production @ Guestpedia

Rendering Arabic, CJK, and Cyrillic in PDF invoices with a custom canvas pipeline

The Stack

TypeScriptCanvas APIjsPDFExcelJSReact
warning

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.

lightbulb

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.

translate

Guest names, one export

Arabic · RTLمحمد الفارسي
Chinese王伟
Japanese山田太郎
Korean김민준
CyrillicАлександр

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.

balance

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

  • layers
    Hybrid renderingLatin via vector fonts, non-Latin via rasterized canvas, keeping file size and text quality balanced.
  • manage_search
    Script detection per runUnicode-range detection decides the rendering path per text segment, not per document.
  • share
    Shared across exportsThe same renderer serves group invoices, the Sales Additional Charges report, and Excel exports.
rocket_launch

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.