Post

ENG | Getting 'Retro' Bitmap Fonts in Python

How to get MS-DOS era fonts for microcontroller projects.

ENG | Getting 'Retro' Bitmap Fonts in Python

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 SizeCharacters Per RowRows Per Display
8x16103
8x8106
6x8146
4x6218

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.

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 Script

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)

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)

Font sites

This post is licensed under CC BY 4.0 by the author.