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):
- 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).
- Run
lv_font_conv, merging both into a single LVGL font per size, with an explicit range list: ASCII0x20-0x7E, Arabic0x600-0x6FF, and crucially the presentation forms0xFB50-0xFDFFand0xFE70-0xFEFF. Without those two ranges the shaper produces code points the font cannot draw, and you get boxes exactly where you expected joined script. - 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.
- 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.