ENG | Getting 'Retro' Bitmap Fonts in Python
How to get MS-DOS era fonts for microcontroller projects.
This post is a follow-up to my earlier article about using PCD8544 SPI display on Raspberry Pi Pico. After getting the display working, I needed fonts - and it came to my mind that I want something like 8x8 fonts used in late 80s era of MS-DOS and CGA displays or even smaller and these font tables were accessible in BIOS memory.
PCD8544 display used in Nokia 5110 phone has 84x48 resolution, which gives us:
Font Size | Characters Per Row | Rows Per Display |
---|---|---|
8x16 | 10 | 3 |
8x8 | 10 | 6 |
6x8 | 14 | 6 |
4x6 | 21 | 8 |
Font 5x3 pixels in 6x4 grid needs to be drawn by writing pixels to framebuffer, whereas 8 pixels tall font can be directly send to display as PCD8544 is organized in such a way that it has 6 rows and one byte is vertical column of 8 pixels.
By the way, SSD1306 display is organized the very same way, but displays have typically 128x32, 128x64 or 128x128 resolution and smaller pixels, so 8x8 font is hardly readable. 6x8 fonts allows to squeze 21x8 characters, for 8x16 font it’s 16x4 characters and we have to print one characters into two framebuffer rows.
The Search for Font
After some googling I came to The Ultimate Oldschool PC Font Pack and found nice 6x8 pixel font used on HP 100LX Palmtop. There is download section. Great, easy! Or … wait, there are fonts in OTB
, FON
, TTF
, WOFF
formats. The problem? Standard font formats aren’t directly usable in embedded systems and they require complex libraries to reador render. My goal was simple: a quick, no-frills solution to extract a usable bitmap font for my project.
Converting Font Files
I asked ChatGPT and it suggested using FreeType library in Python, after few partially successful iterations, I found Dan Bader -=- Monochrome font rendering with FreeType and Python which explores decoding and rendering of fonts into depth and I used few lines from his example to fix my code. Later I added upper half of ASCII table and got happy.
I experimented with various outputs:
- Bytearray - Direct use in Python code
- Image - Useful for debugging (removed later)
- Raw binary file - Ideal for microcontroller
By the way - because I’m not sure if FreeType library works on Windows, I used this script on Linux machine. You may try using Windows Subsystem for Linux (WSL).
Python Scripts
The following script converts font from OTB file format into binary file and outputs Python “include” file to console.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env python3
import freetype
import numpy
import codecs
# WARNING: program works for 6x8
font_file = 'Bm437_HP_100LX_6x8.otb'
font_width = 6
font_height = 8
chars_to_extract = 256
# Load font and set pixel size
face = freetype.Face(font_file)
face.set_pixel_sizes(0, font_height)
# Prepare output image
image = numpy.zeros((font_height, chars_to_extract*font_width), dtype=numpy.uint8)
for i in range(0, chars_to_extract):
# Get source bitmap of character
unicode_index = i
if (i > 127):
unicode_index = codecs.decode(bytes([i]), 'cp437')
face.load_char(unicode_index, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_MONO)
bitmap = face.glyph.bitmap
# Create target grayscale bitmap
target = numpy.zeros((font_height,font_width), dtype=numpy.uint8)
# Bitmap seems valid
if (bitmap.width == font_width and bitmap.rows == font_height):
# Go through scan lines
for src_row in range(0, font_height):
byte_value = bitmap.buffer[src_row]
# Go through bits and unpack them
for bit_index in range(font_width):
bit = byte_value & (1 << (7 - bit_index))
target[src_row, bit_index] = 255 if bit else 0
# Copy target (image of character) to image of whole character map
image[0:8, i*6:i*6+6] = target
print("font_data = bytearray( \\")
raw_data = bytearray()
# Go through image columns and encode them to bits again -> this matches framebuffer layout of PCD8544 display
for column in range(image.shape[1]):
value = 0
for bit_index in range(0,8):
bit_value = 1 if image[bit_index, column] != 0 else 0
bit_value = bit_value << bit_index
value |= bit_value
if (column % 6 == 0 and column != 0):
print("' \\")
if (column % 6 == 0):
print(" b'", end='')
print(f"\\x{value:02X}", end='')
raw_data.append(value)
print("')")
# Save the font file
with open(f'font-{font_width}x{font_height}.bin', 'wb') as file:
file.write(raw_data)
The following script takes bin file from the previous script and converts it to C header file.
1
2
3
4
5
6
7
8
9
10
11
12
13
with open('font-6x8.bin', 'rb') as inf:
buf = inf.read()
with open('font.h', 'w') as of:
of.write("#pragma once\n");
of.write("#include <stdint.h>\n")
of.write("static const uint8_t font_data[256] = {\n")
for i, byte in enumerate(buf):
if i % 6 == 0:
of.write(" ")
of.write(f"0x{byte:02X}, ")
if (i+1)%6 == 0:
of.write(f" // {int(i/6)}\n")
of.write("};\n");
Practical Consideration & Conclusion
This script extracts a retro bitmap font with minimum overhead. Error handling? Minimal. Flexibility? Limited. It’s not an universal tool, but it gets the job done - at least for me. For those wanting a more robust solution, I recommend starting with Dan’s Bader article.
Since there are BIOS fonts available in more reasonable formats, I won’t probably use this script again, but who knows.
Also there are likely some libraries which already make using framebuffer or display easier (actually MicroPython’s framebuffer library likely)
Links
- Dan Bader -=- Monochrome font rendering with FreeType and Python
- Using PCD8544 SPI display on Raspberry Pi Pico - here it’s already mentioned how to use extracted font.
Font sites
- The Ultimate Oldschool PC Font Pack
- BIOS ROM Fonts - 8x8, 8x14, 8x16 BIOS fonts
- A ‘packed’ 5x7 Font Table (for Nokia 5110) 5x7 and 10x14 fonts
- 5x3 font
- Nokia cellphone font
Converted font file
- Bm437 HP 100LX Font file - be careful about licence.