GitHub - waruyama/flattype-cyd-demo: A demo for rendering OpenType vector fonts on a Cheap Yellow Display using Flattype

6 min read Original article ↗

Flattype on a Cheap Yellow Display (ESP32-2432S028R)

Full OpenType text shaping and rendering, live on a $10 microcontroller: Arabic, Nastaliq, Devanagari, Khmer, ligatures, bidi. 320 KB of RAM, 170 KB of code, no pre-baked assets.

flattype-cyd-demo-cHtd6wyP.mp4

What is this?

This repository is a ready-to-flash demo of the Rust port of Flattype, a very compact OpenType shaper and renderer, combined with a tiny rasterizer. It runs on the Cheap Yellow Display (ESP32-2432S028R), the popular ~$10 ESP32 dev board: 320 KB of RAM, 4 MB of flash, no PSRAM.

All the text in this demo is rendered live on the chip from TrueType fonts. No prerendered bitmaps, no glyph atlases, nothing pre-baked.

Put any standard OpenType or TrueType font on flash, and everything below happens live at render time:

  • Bidirectional text ordering.
  • Substitution: ligatures, contextual rules, alternates, reverse contextual.
  • Positioning: kerning, mark-to-base, mark-to-mark, cursive attachment.
  • Script-specific shaping: Arabic joining, Devanagari and other Indic reordering, Khmer, Myanmar, Hangul, Thai, and universal shaping for less common scripts.
  • Both TrueType (glyf) and PostScript (CFF) outlines.
  • Anti-aliased, gamma-corrected rendering, drawn to the LCD in horizontal strips.

Why this matters

Embedded displays have traditionally meant prerendered bitmaps (inflexible, storage-heavy) or commissioned custom fonts. This appears to be the first demonstration of full OpenType shaping with complex script support on hardware this constrained. The shaper itself is about 135 KB of code; the entire pipeline up to anti-aliased pixels is about 170 KB.

Demos

Touch navigation: the top row of the screen switches between the six demos (left half = back, right half = forward), the middle row cycles examples within a demo, and the bottom row is a swipe / slider area.

Demo Image What it shows
Typography
Four typographic samples, drawn as a grey "ghost" and revealed character by character with the bottom-row slider.
Latin with ligatures: 'The first conflict' Latin ligatures: Th, fi, st, fl, and ct form as the slider crosses them.
Nastaliq Urdu sample Nastaliq's diagonal stacking, one of the most demanding scripts to shape.
Khmer sample Khmer consonant clusters: characters stack below the baseline in subjoined forms.
Devanagari sample Devanagari conjuncts joined under the shirorekha (top bar) into a single visual unit.
Latin text
A long Lorem Ipsum paragraph in nine fonts. Middle row switches font; bottom row scales 14–120 px.
Lorem Ipsum in a serif face Word wrapping, kerning, and contextual substitutions.
Lorem Ipsum in a handwritten display face A variety of display faces (Bonbon, Peralta, Henny Penny) shows the shaper handles more than standard serif and sans-serif fonts.
Complex scripts
The same Lorem in Arabic, Devanagari, and Hebrew. Middle row cycles scripts; bottom row scales.
Arabic Lorem Ipsum Cursive joining: each letter takes its initial, medial, final, or isolated form depending on its neighbours.
Devanagari Lorem Ipsum Reordering: vowel marks visually precede the consonants they logically follow.
Hebrew Lorem Ipsum Right-to-left layout with Latin punctuation and digits embedded inline (live bidi).
Emoji
~250 colour emoji, shuffled. Middle row reshuffles; bottom row scales.
Grid of emoji glyphs Emoji are very curve-heavy: a single 90 px emoji can flatten to ~900 line segments, stressing the rasterizer.
Random char
One large character at 90 px from a pool of decorative Latin faces. Bottom row scrolls through letters and fonts.
Large lowercase g with construction arrows Switching fonts is essentially free: fonts take <1 KB of RAM each, so many can be open at once.
Colour on dark
A short title with per-glyph colour over a dark background.
'The End' in colour gradients on a dark background Rendering isn't locked to black-on-light: each glyph can take its own colour over an arbitrary background.

Hardware

You need a Cheap Yellow Display (ESP32-2432S028R): 320 × 240 ILI9341 LCD, XPT2046 resistive touchscreen, 4 MB flash, ~320 KB internal SRAM, no PSRAM. Variants without the resistive touch panel will most likely not work. The demo drives the LCD over SPI at 80 MHz with DMA and the touchscreen at 2 MHz.

Install the demo

Web flasher (recommended): open https://waruyama.github.io/flattype-cyd-demo-install/ in Chrome or Edge, plug your CYD into USB, and click Connect. Flashing takes about 30 seconds.

Manual install: dist/firmware.bin is a merged 4 MB image (bootloader + partition table + app) that flashes at offset 0x0, with esptool.py or espflash:

esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash 0x0 firmware.bin
# or
espflash write-bin 0x0 firmware.bin

Performance

Per-stage timings on the CYD, measured at the smallest font size with the maximum number of visible glyphs (13 lines, roughly 200-800 glyphs depending on font and script). Shaping runs once per text change; all other stages run on every font-size change. So the refresh subtotal is the steady-state cost of a size change, and the total is the cost of the first frame after a text change.

Stage Single glyph Latin Arabic Devanagari Hebrew Emoji
Number of glyphs 1 360-548 809 618 623 208
Shaping 0 ms 12-46 ms 79 ms 156 ms 34 ms 9 ms
Line-breaking 1 ms 7-10 ms 14 ms 14 ms 13 ms 0 ms
Rendering 0 ms 25-97 ms 47 ms 30 ms 18 ms 95 ms
Flattening 0 ms 13-65 ms 35 ms 21 ms 14 ms 56 ms
Rasterizing 1-2 ms 64-187 ms 121 ms 111 ms 77 ms 199 ms
Pushing 18 ms 5 ms 5 ms 5 ms 4 ms 5 ms
Other 0 ms 1 ms 1 ms 1 ms 1 ms 1 ms
Refresh subtotal 21 ms 114-314 ms 224 ms 183 ms 130 ms 360 ms
Total 21 ms 135-350 ms 303 ms 339 ms 164 ms 369 ms

What's in the binary

The flashed image is ~2.6 MB: ~1.93 MB of embedded fonts and ~610 KB of code and data. Sizes are from xtensa-esp32-elf-size and a per-symbol breakdown via xtensa-esp32-elf-nm on a release build.

Binary percentages

Component Size
Flattype code and data (bidi, shaper, renderer) ~135 KB
Rasterizer path (flattener, rasterizer, LCD pushing) ~35 KB
Text shaping + rendering + rasterizing subtotal ~170 KB
Demo logic (layout, touch, display, intro, the 6 demos, main) ~30 KB
ESP-IDF C runtime (FreeRTOS, drivers, ROM glue) ~145 KB
Rust (core, std + alloc) ~155 KB
Backtrace machinery (gimli, addr2line, rustc_demangle, etc) ~85 KB
compiler_builtins, esp-idf-hal, mipidsi, other small crates ~8 KB
Misc rodata not attributable above (vtables, format strings, etc.) ~20 KB
Code + data subtotal ~610 KB
Fonts (16 .ttf files embedded via include_bytes!) ~1.93 MB
Grand total ~2.6 MB

The headline number: the entire OpenType pipeline, from shaping to anti-aliased rasterizing, compiles to about 170 KB. The largest non-font cost, the ESP-IDF C runtime at ~145 KB, is fixed overhead for any Rust ESP32 program with std support.

The 16 fonts are static monochrome vector fonts from Google Fonts, embedded via include_bytes! and not subsetted (except for emoji); for a real product they would be slimmed down. The largest are Noto Nastaliq Urdu (517 KB), Hind (285 KB), Noto Sans Arabic (188 KB), subsetted Noto Emoji (186 KB), and Roboto Regular (167 KB).

Credits

  • Flattype: the OpenType shaper and renderer, ported to Rust from the JavaScript original.
  • mipidsi: ILI9341 driver.
  • esp-idf-hal: ESP-IDF Rust bindings.
  • Fonts: Roboto, the Noto families, Hind, Almendra, Assistant, and the display faces, from Google Fonts under the SIL Open Font License; Droid Serif under the Apache License 2.0.