# ESP Compact Font — Fast, Compact Font Rendering for ESP32
[](https://components.espressif.com/components/espressif/esp_compact_font)
[中文版](./README_CN.md)
**ESP Compact Font is a lightweight font rendering component for ESP32 devices with displays.** It helps you reduce font storage and render text efficiently without requiring LVGL.
Designed for resource-constrained embedded products, it supports UTF-8 text, runtime font scaling, optional glyph caching, and karaoke-style progressive text highlighting. It is particularly useful for Chinese-language interfaces and other applications that require extensive CJK character coverage.
## Key Features
- **Compact font storage** — GFH uses Huffman coding to compress glyph coverage with 1-, 2-, and 4-bit grayscale formats.
- **Fast glyph decoding** — Efficient decoding reduces the time needed to render text.
- **Runtime font scaling** — Scale a single font atlas to different pixel sizes instead of storing separate font files for every size.
- **Karaoke-style highlighting** — Progressively reveal text in a highlight color, synchronized with audio playback or other timed events.
- **Optional glyph caching** — Reuse decoded glyphs to reduce repeated decoding.
- **Flexible font sources** — Load fonts from memory or filesystem paths, with an option to copy font data into RAM.
- **No LVGL dependency** — Use the renderer in standalone applications or alongside an existing graphics framework.
- **Multiple output formats** — Render to RGB565, RGB888, grayscale, or monochrome surfaces.
## Target Applications
ESP Compact Font is suitable for embedded products that need efficient text rendering with limited flash, RAM, or processing resources.
- **Smart displays and control panels:** menus, settings, status information, and device labels.
- **Chinese-language interfaces:** CJK characters and punctuation without a heavyweight runtime font engine.
- **E-book readers and AI assistants:** display extensive text on memory-constrained devices.
- **Karaoke and synchronized text:** highlight lyrics progressively in sync with playback.
- **Embedded dashboards and UI systems:** render frequently updated text efficiently.
## Supported Font Formats
| Format | Description |
| --- | --- |
| **GFH** | Built-in Huffman-coded font atlas supporting 1-, 2-, and 4-bpp grayscale glyphs. Recommended for most applications. |
| **LVGL binary (`.bin`)** | Optional support for font binaries generated by `lv_font_conv`. Does not require linking the LVGL library. |
| **GFZ** | Optional 2-bpp font format using per-glyph Zstandard compression. Disabled by default and requires additional decoder code. |
GFH is the recommended choice when compact storage and fast decoding are priorities. GFZ provides an alternative compression strategy when file size is a priority.
See [GFH / GFZ Formats and Algorithms](./format.md) for technical details.
## Performance Comparison
The following host benchmark compares GFH with LVGL font binaries containing the same glyphs. It was built with `-O2`, with the glyph cache off.
**Test corpus:** approximately 9,620 CJK ideographs and punctuation, using a 36-pixel Noto Sans CJK subset.
| Format | LVGL Size | GFH Size | LVGL Decode | GFH Decode |
| --- | ---: | ---: | ---: | ---: |
| 1-bpp | 1,335 KB | 788 KB | 2.71 µs/glyph | 1.97 µs/glyph |
| 2-bpp | 1,778 KB | 1,474 KB | 9.14 µs/glyph | 2.88 µs/glyph |
| 4-bpp | 3,066 KB | 2,970 KB | 9.90 µs/glyph | 3.66 µs/glyph |
### ESP32-S3 board results
The following results were measured on an ESP32-S3 at 240 MHz with 80 MHz PSRAM. The embedded font source was copied into RAM (`with_ram=1`) before decoding, and the glyph cache was off. Each run decoded all 9,620 glyphs at 36 px with `bad=0`.
| Format | LVGL Open | GFH Open | LVGL Full Decode | GFH Full Decode | LVGL Decode | GFH Decode |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| 1-bpp | 164.131 ms | 88.408 ms | 1,757.046 ms | 1,242.523 ms | 182.64 µs/glyph | 129.16 µs/glyph |
| 2-bpp | 194.160 ms | 135.454 ms | 4,203.172 ms | 1,808.119 ms | 436.92 µs/glyph | 187.95 µs/glyph |
| 4-bpp | 279.157 ms | 233.920 ms | 4,392.224 ms | 2,158.726 ms | 456.57 µs/glyph | 224.39 µs/glyph |
Open time includes preparing the copied RAM source. Full-decode time is the complete uncached pass.
For this corpus, GFH reduces file size by approximately 41% at 1 bpp. It also achieves faster glyph decoding across all three tested formats.
**Optional GFZ benchmark:** On the same 2-bpp corpus, GFZ measured 1,623 KB and 3.89 µs/glyph. It was larger and slower than GFH in this test.
## Resource Usage
These figures use the same 9,620-glyph, 36 px corpus. RAM is what the library keeps after `esp_compact_font_set_mem_src()` when the file stays in flash (`prefer_ram` left off). Instruction size is `.text` from `riscv32-esp-elf-gcc -O2` after unreferenced functions are removed.
### RAM
Each open font keeps a glyph index of 16 bytes per glyph. For this corpus that is `9,618 × 16 = 153,888` bytes. The on-disk index is the same size and is released once the RAM index is built. GFH also builds a 12-bit Huffman table (16,384 bytes) plus a short secondary table. A 32,768-byte scratch buffer used to build that table is freed before open returns.
| Format | Index | Decode tables | Steady RAM |
| --- | ---: | ---: | ---: |
| GFH 1-bpp | 153,888 | 17,760 | 171,776 |
| GFH 2-bpp | 153,888 | 16,736 | 170,752 |
| GFH 4-bpp | 153,888 | 16,384 | 170,400 |
| GFZ 2-bpp | 153,888 | 41,108 | 195,124 |
| LVGL `.bin` | 307,776 | 0 | 307,944 |
Steady RAM includes the 108-byte font handle and the codec object (20 bytes for GFH and GFZ, 60 bytes for LVGL). The GFH decode tables are the Huffman lookup. GFZ's table is the zstd workspace built with `ZSTD_DEC_LIT_CAP` of 4,096. LVGL keeps the shared index and a second 16-byte record per glyph. A streamed GFZ file also retains its 16,384-byte dictionary. A memory source uses the dictionary in the file image.
A smaller glyph count shrinks the index by 16 bytes per glyph (32 bytes per glyph for an LVGL `.bin`). The 16,384-byte Huffman lookup and the 41,108-byte zstd workspace stay allocated for any glyph count.
`prefer_ram` adds a copy of the font file on top of this table. One decoded glyph needs a caller buffer of at most `80 × 80 = 6,400` bytes. The optional cache is 48 bytes per slot (24,576 bytes at the default 512 slots) plus one A8 mask (`width × height`) for each cached glyph. One measured 36 px glyph mask was 35×34 = 1,190 bytes.
### Instruction memory
| Build | `.text` | `.rodata` |
| --- | ---: | ---: |
| GFH (default) | 15,186 | 50 |
| GFH on ESP32-P4, with PIE helpers | 15,586 | 50 |
| GFH + LVGL `.bin` | 20,378 | 129 |
| GFH + GFZ | 28,356 | 891 |
GFH is always linked. LVGL and GFZ follow `ESP_COMPACT_FONT_LVGL` and `ESP_COMPACT_FONT_GFZ`. The ESP32-P4 row adds the scale and blend helpers (208 and 192 bytes).
## Quick Start
The basic workflow is straightforward: open the renderer, load a font, select the pixel size, and draw UTF-8 text onto your display buffer.
```c
#include <stdlib.h>
#include "esp_compact_font.h"
static void *my_malloc(size_t size, void *ctx)
{
(void)ctx;
return malloc(size);
}
static void my_free(void *ptr, void *ctx)
{
(void)ctx;
free(ptr);
}
void draw_text(uint16_t *framebuffer, int width, int height,
const uint8_t *gfh_data, size_t gfh_size)
{
esp_compact_font_cfg_t cfg = {0};
esp_compact_font_handle_t font = NULL;
cfg.alloc.malloc = my_malloc;
cfg.alloc.free = my_free;
if (esp_compact_font_open(&cfg, &font) != ESP_COMPACT_FONT_OK) {
return;
}
if (esp_compact_font_set_mem_src(font, gfh_data, gfh_size)
== ESP_COMPACT_FONT_OK) {
esp_compact_font_surface_t dst = {
.buffer = framebuffer,
.stride = width * 2,
.width = width,
.height = height,
.format = ESP_COMPACT_FONT_SURFACE_RGB565,
};
esp_compact_font_set_pixel(font, 24);
esp_compact_font_draw_utf8(
font, &dst, 16, 40, "Hello World",
ESP_COMPACT_FONT_MAKE_COLOR_RGB565(0, 0, 0), NULL);
}
esp_compact_font_close(font);
}
```
Replace the sample font data, framebuffer, and allocator with those provided by your application. The example uses an RGB565 destination surface and a 24-pixel rendering size.
**Important:** The buffer passed to `esp_compact_font_set_mem_src()` must remain valid while the font uses it.
For frequently rendered text, consider enabling a glyph cache to reduce repeated decoding. See the [font benchmark example](./examples/font_benchmark) for a more complete configuration.
## Karaoke-Style Text Highlighting
ESP Compact Font supports progressive horizontal glyph drawing, making it possible to implement karaoke lyrics, read-along highlighting, and synchronized text animations without maintaining separate font atlases for different highlight states.
The basic approach is to draw the complete glyph in a normal color, then redraw the sung portion in a highlight color as playback advances.
```c
int advance = 0;
uint32_t normal = ESP_COMPACT_FONT_MAKE_COLOR_RGB565(0xB4, 0xAE, 0xA4);
uint32_t highlight = ESP_COMPACT_FONT_MAKE_COLOR_RGB565(0xC4, 0x3A, 0x2A);
/* Draw the complete glyph in its normal color. */
esp_compact_font_draw(font, &dst, x, baseline, cp, normal, &advance);
/* sung_percent ranges from 0 to 100. */
esp_compact_font_set_draw_range(font, 0, sung_percent);
esp_compact_font_draw(font, &dst, x, baseline, cp, highlight, NULL);
```
`esp_compact_font_set_draw_range()` sets a horizontal drawing window as a percentage of the glyph bitmap's width: `0` is the left edge and `100` is the right edge.
The draw range is **one-shot**: the next `esp_compact_font_draw()` call consumes it and then clears it. Set the range again for every partial-glyph draw.
- `esp_compact_font_draw()` applies the configured range to the next glyph draw.
- `esp_compact_font_draw_utf8()` applies one range to every codepoint in the string.
- `esp_compact_font_draw_glyph()` ignores the draw range.
For synchronized highlighting across a complete line, map playback progress to the current glyph and draw only the newly revealed portion. This avoids unnecessarily redrawing the already highlighted text.
See the [font benchmark example](./examples/font_benchmark) for related rendering techniques.
## Memory and Caching
ESP Compact Font supports both memory-backed and filesystem-backed font sources.
- **Memory source:** `esp_compact_font_set_mem_src()` uses a caller-provided buffer. The buffer must remain valid while in use.
- **Filesystem source:** `esp_compact_font_set_file_src()` reads font data from a file. GFH, GFZ, and LVGL sources can be read glyph by glyph without copying the entire atlas into memory.
- **RAM loading:** The `prefer_ram` option requests a copy of the font image into the configured allocator's memory when possible. This uses additional RAM and can reduce filesystem access during rendering.
- **Glyph caching:** An optional shared cache stores decoded glyphs for reuse. Choose its capacity according to available memory and the application's rendering workload.
### Typical workflow
1. Prepare the font atlas and glyph data required by your application.
2. Run the corresponding generation tool to produce a GFH or GFZ file, or `lv_font_conv` for an LVGL `.bin`.
3. Include the generated file in your firmware or store it on a filesystem.
4. Load the font using `esp_compact_font_set_mem_src()` or `esp_compact_font_set_file_src()`.
**Which format should I choose?**
- **GFH:** Start here for a good balance of file size and decoding speed.
- **GFZ:** Consider this when file size smaller than `GFH` and the additional decoder footprint is acceptable.
See [GFH / GFZ Formats and Algorithms](./format.md) for the file layouts, encoding details, and compression algorithms.
## Font Generation Tools
Generate compact font files on your development computer, then package them with your ESP-IDF application or load them from a filesystem at runtime.
| Tool | Output | Description |
| --- | --- | --- |
| [`tools/mk_gfh.py`](./tools/mk_gfh.py) | GFH | Generate Huffman-coded font atlases with 1-, 2-, or 4-bpp glyph coverage. Recommended for most applications. |
| [`tools/mk_gfz.py`](./tools/mk_gfz.py) | GFZ | Generate 2-bpp font atlases using per-glyph Zstandard compression. |
| [`lv_font_conv`](https://github.com/lvgl/lv_font_conv) | LVGL `.bin` | Official LVGL converter. This component decodes that file and does not reimplement the encoder. |
## Examples and Documentation
| Resource | Description |
| --- | --- |
| [Font Benchmark](./examples/font_benchmark) | Compare font size and decoding performance on a display. |
| [Test Application](./test_apps) | Unity tests covering caching, decoding, drawing, and filesystem loading. |
| [GFH / GFZ Format Documentation](./format.md) | Detailed format layouts, Huffman coding, XOR encoding, and compression algorithms. |
## License
Espressif Modified MIT. See [LICENSE](./LICENSE) for details.
The optional LVGL binary decoder retains the original MIT notice from LVGL Kft. The optional Zstandard decoder retains its BSD-3-Clause notices.
## Technical Support
For technical support, use the links below:
- Technical support: [esp32.com](https://esp32.com/viewforum.php?f=20) forum
- Issue reports and feature requests: [GitHub issue](https://github.com/espressif/esp-adf/issues)
We will reply as soon as possible.
fb7e4cad6d91e7d7ed978ba3725e53ea62e97708
idf.py add-dependency "espressif/esp_compact_font^1.0.0"