Fonts ship in firmware resources as binary PBF files, generated from TTF
input by tools/font/fontgen.py and parsed at runtime by
fw/applib/graphics/text_resources.c. The on-disk structs live in
fw/applib/fonts/fonts_private.h and
fw/applib/graphics/text_resources.h; tools/font/ also contains
standalone readers (dump_font.py, pbf_extract.py, pbf_repack.py).
A PBF file is four consecutive sections, all multi-byte fields little-endian:
FontInfo header
hash table
offset tables
glyph table
“FontInfo” is the generator’s name for the on-disk header; the firmware
calls the struct FontMetaDataV3 (FontInfo in fonts_private.h is an
unrelated runtime struct).
Field |
Type |
Since |
Notes |
|---|---|---|---|
|
|
v1 |
1, 2 or 3 |
|
|
v1 |
tallest glyph in the font |
|
|
v1 |
|
|
|
v1 |
rendered for missing glyphs; default U+25AF |
|
|
v2 |
bucket count; the generator always writes 255 |
|
|
v2 |
2, or 4 if any codepoint exceeds U+FFFF |
|
|
v3 |
size of this header (10), for future extension |
|
|
v3 |
see below |
features bits (FEATURE_OFFSET_16 / FEATURE_RLE4 in
fonts_private.h):
bit 0: glyph-table offsets are uint16_t if set, uint32_t if clear.
The generator sets it when the glyph table fits in 64 KiB.
bit 1: glyph bitmaps are RLE4-compressed if set, plain bitmaps if clear.
bit 2: glyphs are color glyphs (see Color glyphs below). Cannot be combined with bit 1, and only loads on color displays.
bits 3–7: reserved.
hash_table_size entries of 4 bytes each (FontHashTableEntry):
uint8_t hash, uint8_t count, uint16_t offset. A codepoint hashes to
bucket codepoint % hash_table_size; offset is the byte offset of the
bucket’s entries within the offset-tables section, and count is the
number of entries there (collisions are chained contiguously, at most 128
entries per bucket).
Each bucket is a run of {codepoint, glyph_offset} entries, sorted by
codepoint so the firmware can binary-search. Field widths vary per file:
codepoints are 2 or 4 bytes (codepoint_bytes), glyph offsets 2 or 4
bytes (features bit 0), giving entry sizes of 4–8 bytes
(OffsetTableEntry_2_2 … _4_4 in text_resources.h).
The table begins with one 32-bit zero word, so offset 0 means “glyph not present”. Offsets are byte offsets from the start of the glyph table (version 1 counted 32-bit words instead). Glyphs are deduplicated: several codepoints may share one glyph offset.
Each glyph is a packed 5-byte header (GlyphHeaderData) followed
immediately by bitmap data:
Field |
Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
With RLE4 enabled, the height byte instead stores the number of RLE
units (the decoder recovers the height from the decompressed size).
Version 1 used a different 8-byte header (GlyphHeaderDataV1).
Bitmap data is 1 bit per pixel: rows are concatenated unaligned into one continuous bit stream, packed LSB-first into 32-bit words and zero-padded to a multiple of 4 bytes.
RLE4 compression is a stream of 4-bit units, two per byte (low nibble
first): each unit is [symbol:1][length:3], emitting length + 1
(1–8) copies of the symbol bit. An odd number of units is padded with a
(0,1) unit, and the stream is zero-padded to a multiple of 4 bytes. The
firmware decompresses in place in the glyph cache; the generator verifies
at build time that every glyph is in-place decodable. The decoder-side
description lives in text_resources.c.
With features bit 2 set, height in the glyph header is always the
height, and the 1-bit bitmap is replaced by a color body:
Field |
Type |
Notes |
|---|---|---|
|
|
bits 0–1: log2 of bpp (1/2/4/8), bits 2–3: mode |
|
|
0–16 |
|
|
bytes of |
|
|
ARGB2222, alpha 0 = transparent |
|
|
pixels, see mode |
Modes:
0, raw: rows of bpp-bit palette indices, MSB-first, each row padded
to a byte. With 8 bpp there is no palette and each byte is a GColor8.
1, RLE: one byte per run, high nibble run - 1 (1–16 pixels), low
nibble the palette index. Runs continue across rows.
2, tinted: raw 1 bpp coverage with no palette; set pixels are drawn in the text color, like a regular glyph.
The body is zero-padded to a multiple of 4 bytes. The firmware streams it
from the resource while drawing instead of caching it, so glyph size is not
bound by the glyph cache. tools/font/pbf_color.py implements the
encoding; pbf_extract.py and pbf_repack.py round-trip color fonts
through RGBA PNGs.
Generated from PebbleOS 91af4a22c. This page is maintained in the pebbleos repository: docs/reference/formats/font.md.