Skip to content

Firmware Font Format

Ave Ozkal edited this page Mar 21, 2020 · 11 revisions

This page is intended to serve as an overview of the pebble font format, as deduced from font files and the SDK code.

It's not an exhaustive resource about pebble fonts as a whole.

If you're looking for information about system fonts, try the official reference, #define list, or unofficial reference

Conceptual Diagram of Font Files

+ --------------------------------- +
|             FontInfo              |
| - metadata                        |
+ --------------------------------- +
|            HashTable              |
| - hash -> offset pointer          |
| - hash -> offset pointer          |
| - ...                             |
+ --------------------------------- +
|            OffsetTable            |
| - offset                          |
|   - charcode -> data pointer      |
|   - charcode -> data pointer      |
|   - ...                           |
| - offset                          |
|   - ...                           |
| - ...                             |
+ --------------------------------- +
|             GlyphTable            |
| - 0x00 00 00 00                   |
| - glyph                           |
|   - character metadata            |
|   - character data                |
| - glyph                           |
|   - ...                           |
| - ...                             |
+ --------------------------------- +

While this format may seem complicated, the system used allows for extremely quick lookup operations. To achieve this, one must:

  1. Find the correct HashTable Entry. (Since most fonts seem to include all hash results 0..254, this is easy.)
  2. Iterate over the offset (keeping in mind the offset table size) until the correct character is found, else fallback to wildcard character (which is usually the first glyph..? further research necessary)

Font Format Documentation

FontInfo

0x00 u8 : Version. (1; 2; 3)
0x01 u8 : Line height.
0x02 u16 : Glyph Amount. (< 256)
0x04 u16 : Wildcard Codepoint
V2+ only:
0x06 u8 : Hash Table Size (usually 255; not aware of different values)
0x07 u8 : Codepoint Bytes (2 or 4. 4 for extended unicode; eg emoji/east asian scripts)
V3+ only:
0x08 u8 : Size of FontInfo (should be 10)
0x09 u8 : Features.
LSB: OffsetTable offsets sizes. 0 for 32-bit (default in earlier versions), 1 for 16-bit
2nd: Storage mode. 0 for bitmapped (default in earlier versions), 1 for RLE4

Anyone writing an implementation should take care that the size of the FontInfo struct, and thus the beginning of actual font data, varies between font format versions.

HashTable

The HashTable begins immediately after FontInfo. It repeats the following format as often as required by the Hash Table Size field in the FontInfo struct.

Offsets in this section are relative to the HashTable entry's beginning.

0x00 u8 : Value. In all examples I encountered, this simply counts 0..255
0x01 u8 : Offset Table Size.
0x02 u16 : Offset Table Offset. Measured in bytes from the beginning of the offset table.

OffsetTable

The OffsetTable is a list of entries sorted by the respective character's hash value. (By doing so, they are grouped for iteration via HashTable references)

Offsets in this section are relative to the inner list item's beginning.

0x00 u16/u32 : Unicode codepoint (for example, 0x0622 for ∆).
Codepoint width determined by Codepoint Bytes field in FontInfo struct.
0x02 or 0x04 u16/u32 : Data offset.
Field width determined by Features field in FontInfo struct for v3 or later. Always 4 bytes (32 bits) in earlier versions.

GlyphTable

The GlyphTable is a list of glyphs. Once you arrive here, you can actually start rendering. Yay!

Beware: The first four bytes of the glyph table are unused and zeroed out. The wildcard glyph's offset should therefore always be 4 (untested & further research needed)

Offsets in this section are relative to the glyph's beginning.

0x00 u8: Bitmap width
0x01 u8: Bitmap height
0x02 i8: Left offset
0x03 i8: Top offset
0x04 i8: Horizontal advance
0x05 : start of data

Visual description of the aforementioned metrics

Glyph Data

The length of stored data is always padded to 32 bits.

Bitmap Format

A glyph of size x * y will take ceil(x * y / 8 / 4) * 4 bytes in storage.

Imagine the following glyph:

..@....@..
..@@@@@@..
@@......@@
.@......@.
.@......@@
.@......@.
.@......@@
..@@@@@@..
..@....@..

(@ represents an on pixel, . is off)

We convert it to bitwise binary:

0010000100
0011111100
1100000011
0100000010
0100000011
0100000010
0100000011
0011111100
0010000100

And split it into bytes:

00100001 00
001111 1100
1100 000011
01 00000010
01000000 11
010000 0010
0100 000011
00 11111100
00100001 00

This leaves us with:

00100001 00001111 11001100 00001101
00000010 01000000 11010000 00100100
00001100 11111100 00100001 00......

The bits marked with periods are zeroed out to fill the 32-bit block.

RLE4 Format

Description coming soon.

Ligatures (Proposal)

Ligature support is, of course, awesome. This is a proposal which outlines a way to draw ligatures with extremely low storage overhead and low memory overhead.

NB: Ligatures should always be toggle-able according to developer preferences (Ideally, the replacement for GTextAttributes).

The proposed mechanism uses the three padding bytes contained in each glyph's data and an additional offset table for each ligature path. The hash table remains unchanged.

Under this proposal, the three currently unused additional bytes in each glyph table entry would contain:

0x05 u8: OffsetTable size.
0x06 u16: OffsetTable offset.

Note that all glyphs within each ligature tree would have to exist:

  fi   ffi
 /    /
f - ff 
 \    \
  fl   ffl

If the OffsetTable size is set to 0, ligatures don't exist for the given character. This is the default and ensures reverse-compatibility.

Upon receiving instructions to draw a glyph (with ligatures being enabled), the graphics library should first find the glyph table. If the OffsetTable size is nonzero, it would go to the given OffsetTable and check whether the next character in the string is therein contained. If so, it goes to the glyph and repeats the process.

For example, ffi is a three-character ligature.

Here's how it's determined:

  • We look into the hash table to find f's hash, which points us to an offset table position.
  • We iterate over the offset table (which contains f and possibly some other characters) and go to the f glyph data.
  • We notice that the ligature info is set, which points us to an offset table position.
  • We iterate over that second offset table (f, i, l) and go to the ff glyph data.
  • We notice that the ligature info is set, which points us to an offset table position.
  • We iterate over that third offset table (i, l) and go to the ffi glyph data.
  • The ligature info isn't set and ffi is drawn.

Here's how ffa is determined:

  • We look into the hash table to find f's hash, which points us to an offset table position.
  • We iterate over the offset table (which contains f and possibly some other characters) and go to the f glyph data.
  • We notice that the ligature info is set, which points us to an offset table position.
  • We iterate over that second offset table (f, i, l) and see that a isn't contained therein.
  • ff is drawn.

Edit 2017-02-12: The three padding bytes documented in the make-font.py script by Pebble seem not to exist. The above proposal has been edited to reflect this. It would be possible to simply add a flag byte to each glyph in the glyph table indicating whether ligatures for that character exist. However, this would break reverse-compatibility. Therefore, the ligatures project is currently indefinitely postponed.

Clone this wiki locally