Skip to main content

Arabic Text Rendering

Rendering Arabic correctly is the display stack's hardest requirement and the original stack's most visible failure (disconnected, left-to-right letters). This chapter explains the two problems, shaping and bidirection, from zero, then the pipeline that solves them in firmware.

Problem one: contextual shaping​

Arabic is cursive by definition: a letter's glyph depends on its position in the word. The letter ع renders four different ways (isolated ع, initial عـ, medial ـعـ, final ـع), and letters join to their neighbors. A renderer that draws each Unicode code point's nominal glyph produces the "typewriter Arabic" of the original bug report: ص ا ف ي instead of صافي. Some pairs additionally form mandatory ligatures, most famously lam + alef becoming لا.

Unicode encodes Arabic as abstract letters (block U+0600..U+06FF) precisely so text processing stays simple; turning them into positional glyphs is the renderer's job, called shaping. Desktop systems run HarfBuzz for this; a microcontroller needs something smaller.

The escape hatch Unicode provides is the presentation forms blocks (U+FB50..U+FDFF, U+FE70..U+FEFF): every positional variant and required ligature as its own code point. Shaping then reduces to a table-driven substitution: classify each letter's joining behavior, look at its neighbors, and emit the right presentation form. This is exactly what LVGL implements when LV_USE_ARABIC_PERSIAN_CHARS is on: a compact shaper that handles joining and the lam-alef ligature, sufficient for UI text like surah names. (What it does not do: complex Quranic typography with stacked diacritics; surah names carry no diacritics, so the trade holds. Full tashkeel rendering would need a HarfBuzz-class shaper and is out of scope by design.)

Problem two: bidirectional layout​

Arabic runs right to left, but embedded numerals and Latin fragments run left to right within the same line: "سورة 114" contains both directions. The Unicode Bidirectional Algorithm (UAX #9) resolves each character's direction and reorders runs for display. LVGL implements a practical subset (LV_USE_BIDI), with per-label base direction; Safi sets RTL as the base for Arabic labels so punctuation and mixed runs fall correctly.

The critical rule this imposes on application code: strings live in logical order (the order you type and store), and reordering happens only at render time, inside LVGL. Nothing in Safi ever stores visually-reordered text; that way search, comparison, and the REST API all operate on sane strings.

The font pipeline​

Shaping needs glyphs for the presentation forms, which most fonts and most font converters silently omit. Safi's pipeline (tools/generate_lvgl_fonts.sh):

  1. Take Noto Naskh Arabic (the Arabic body face, chosen for print-like readability at small sizes) and Montserrat (Latin and digits, LVGL's default look).
  2. Run lv_font_conv, merging both into a single LVGL font per size, with an explicit range list: ASCII 0x20-0x7E, Arabic 0x600-0x6FF, and crucially the presentation forms 0xFB50-0xFDFF and 0xFE70-0xFEFF. Without those two ranges the shaper produces code points the font cannot draw, and you get boxes exactly where you expected joined script.
  3. Emit at 4 bits per pixel (16-level antialiasing, half the size of 8bpp with no visible loss at these sizes), in three sizes: 14 for body, 18 for secondary headings, 22 for surah titles.
  4. Commit the generated C files, so building the firmware never requires node or the fonts toolchain; regeneration is a deliberate act.

One Kconfig consequence worth knowing: merged fonts of this size trip LVGL's default limits and need LV_FONT_FMT_TXT_LARGE, which widens the internal glyph index types. The generated .c files live in flash (const), costing zero RAM; at roughly 150 KB total for three sizes, flash is the right home.

Where to see it verified​

Because development ran on a bare board with no panel attached, Arabic rendering was verified through the screenshot endpoint: BMP captures of the live framebuffer, checked for correctly joined صافي and shaped UI strings. The same endpoint remains the fastest way to eyeball shaping after any font regeneration.