From ASCII Codes to Pixels: How Bitmap Fonts Work
When a computer prints a character such as A, i, or 0, the display does not draw the ASCII value directly. ASCII is only a numeric code. To show a visible character on screen, the system needs a mapping from that code to a shape. That shape comes from a font.
For simple text systems, embedded devices, bootloaders, terminals, and old video hardware, a font can be represented as a small bitmap table. Each character is stored as a grid of pixels, and each bit tells the renderer whether a pixel should be on or off.
ASCII Is a Code, Not a Shape
ASCII maps characters to numbers. For example:
| Character | ASCII Decimal | ASCII Hex |
|---|---|---|
A |
65 | 0x41 |
i |
105 | 0x69 |
0 |
48 | 0x30 |
But the number 0x69 does not describe how the letter i looks. It only identifies the character. To render it, software uses the ASCII value as an index into a font table.
Conceptually:
ASCII code -> font table lookup -> glyph bitmap -> pixels on screen
Bitmap Font Tables
A bitmap font stores each character as a fixed-size matrix. A common early format is an 8x8 font, where every glyph is 8 pixels wide and 8 pixels tall.
Because each row has 8 pixels, one row can be stored in one byte:
- 8 rows
- 1 byte per row
- 8 bytes per character
So an 8x8 bitmap font for 128 ASCII characters needs:
\[128 \times 8 = 1024 \text{ bytes}\]That is only 1 KiB, which is why bitmap fonts are very common in low-level systems.
One Byte Represents One Row
In an 8x8 font, each byte describes one horizontal row of the character. Each bit controls one pixel position.
For example, the byte 0x03 is:
00000011
If 1 means “draw a pixel” and 0 means “leave it blank”, then this row turns on the two rightmost pixels, assuming the most significant bit is the leftmost pixel.
Different systems may choose different bit orders. Some treat bit 7 as the leftmost pixel, while others treat bit 0 as the leftmost pixel. The font data and renderer must agree on the convention.
Example: A Simple 8x8 Glyph for i
A simple lowercase i can be represented as eight bytes:
uint8_t glyph_i[8] = {
0x18,
0x00,
0x18,
0x18,
0x18,
0x18,
0x18,
0x18,
};
Expanded as binary rows:
00011000
00000000
00011000
00011000
00011000
00011000
00011000
00011000
Rendered with # for set pixels and . for unset pixels, it looks like this:
...##...
........
...##...
...##...
...##...
...##...
...##...
...##...
This is a very minimal glyph. A real font designer may center the character, change the stroke width, or use a different dot shape.
Here is a simple uppercase A in the same 8x8 format:
uint8_t glyph_A[8] = {
0x18,
0x24,
0x42,
0x7E,
0x42,
0x42,
0x42,
0x00,
};
Expanded as binary rows:
00011000
00100100
01000010
01111110
01000010
01000010
01000010
00000000
Rendered with # and .:
...##...
..#..#..
.#....#.
.######.
.#....#.
.#....#.
.#....#.
........
ASCII Indexing Into the Font Table
A full font table can be stored as a two-dimensional array:
uint8_t font8x8[128][8];
The first index is the ASCII code, and the second index is the row inside the glyph.
For example:
uint8_t *glyph = font8x8['i'];
Since 'i' has ASCII value 0x69, this selects:
font8x8[0x69]
The renderer then reads the 8 bytes row by row and draws pixels according to the bits.
Rendering Logic
A simple renderer may look like this:
void draw_glyph_8x8(int x, int y, const uint8_t glyph[8]) {
for (int row = 0; row < 8; row++) {
uint8_t bits = glyph[row];
for (int col = 0; col < 8; col++) {
if (bits & (1 << (7 - col))) {
draw_pixel(x + col, y + row);
}
}
}
}
This version assumes bit 7 is the leftmost pixel and bit 0 is the rightmost pixel. If the font uses the opposite order, the test would change to:
if (bits & (1 << col)) {
draw_pixel(x + col, y + row);
}
Larger Font Sizes
An 8x8 bitmap is small and efficient, but it has limited detail. Other fixed-size bitmap fonts may use larger grids, such as:
- 8x16: common in text-mode consoles
- 10x10: useful for small square icons or compact UI text
- 16x16: common for CJK bitmap fonts and higher-resolution glyphs
- 24x24 or larger: better readability, more memory cost
The storage requirement grows with width and height.
For a 16x16 glyph:
- 16 pixels per row
- 2 bytes per row
- 16 rows
- 32 bytes per character
For 128 ASCII characters:
\[128 \times 32 = 4096 \text{ bytes}\]For CJK character sets, the table can become much larger because there are thousands of characters.
Bitmap Fonts vs Vector Fonts
Bitmap fonts directly store pixels. Modern desktop and web systems usually use vector fonts such as TrueType or OpenType, where glyphs are described by curves. The renderer then rasterizes those curves into pixels at the desired size.
Bitmap fonts are still useful when:
- The environment has no complex font engine
- Memory and CPU usage must be predictable
- Text size is fixed
- The display is very small
- The system runs before the operating system is fully loaded
Examples include firmware setup screens, bootloaders, microcontrollers, simple LCD/OLED displays, and retro game engines.
Takeaway
ASCII gives each character a number, but a font gives that number a visible shape. In a bitmap font, the mapping is simple and direct:
character code -> glyph bytes -> bit pattern -> pixels
An 8x8 font is especially easy to understand: each character uses 8 bytes, each byte represents one row, and each bit decides whether one pixel is drawn. Larger formats such as 10x10 or 16x16 follow the same idea, trading more memory for better visual detail.