2026-10-15 19:43:59 +02:00
|
|
|
"""Generates src/esp32/logic/builtin_fonts.cpp: the built-in fonts of the dynamic pages.
|
2026-10-15 21:01:14 +02:00
|
|
|
Characters 32-126, and 127 as the degree sign.
|
2026-10-15 19:43:59 +02:00
|
|
|
|
|
|
|
|
python tools/make_builtin_fonts.py
|
|
|
|
|
|
|
|
|
|
Needs the Adafruit GFX library in .pio/libdeps/esp32 (run `pio run -e esp32` once):
|
|
|
|
|
Small TomThumb 3x5 (Fonts/TomThumb.h, BSD license, see that file)
|
|
|
|
|
Normal the classic 5x7 font (glcdfont.c), the font the clock always used
|
|
|
|
|
Full height digits drawn below (seven segments with rounded corners, 19 dots high)
|
|
|
|
|
"Large" is Normal drawn twice as big, so it has no data of its own.
|
|
|
|
|
|
|
|
|
|
The font format is described in src/esp32/logic/font.h. Only the Python standard
|
|
|
|
|
library is used.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
GFX = os.path.join(ROOT, ".pio", "libdeps", "esp32", "Adafruit GFX Library")
|
|
|
|
|
OUT = os.path.join(ROOT, "src", "esp32", "logic", "builtin_fonts.cpp")
|
|
|
|
|
|
|
|
|
|
FIRST = 32
|
2026-10-15 21:01:14 +02:00
|
|
|
LAST = 126 # plus character 127: the degree sign (DEGREE in each font)
|
|
|
|
|
|
|
|
|
|
# The degree sign per font height, drawn at the top of the character
|
|
|
|
|
DEGREE = {
|
|
|
|
|
6: [".#.", "#.#", ".#.", "...", "...", "..."],
|
|
|
|
|
8: [".##.", "#..#", "#..#", ".##.", "....", "....", "....", "...."],
|
|
|
|
|
19: [".####.", "##..##", "#....#", "#....#", "##..##", ".####."] + ["......"] * 13,
|
|
|
|
|
}
|
2026-10-15 19:43:59 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def numbers(text):
|
|
|
|
|
return [int(v, 0) for v in re.findall(r"0x[0-9A-Fa-f]+|-?\d+", text)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def array_body(source, name):
|
|
|
|
|
match = re.search(name + r"\[\]\s*PROGMEM\s*=\s*\{(.*?)\};", source, re.S)
|
|
|
|
|
if not match:
|
|
|
|
|
sys.exit("Not found: " + name)
|
|
|
|
|
# drop comments, they contain numbers too
|
|
|
|
|
return re.sub(r"/\*.*?\*/|//[^\n]*", "", match.group(1), flags=re.S)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def encode(name, height, spacing, glyphs):
|
|
|
|
|
"""glyphs: list of (width, rows) per character FIRST..LAST, rows = list of strings"""
|
|
|
|
|
data = bytearray(b"FDF1")
|
|
|
|
|
data += bytes([height, spacing, FIRST, len(glyphs)])
|
|
|
|
|
data += bytes(width for width, _ in glyphs)
|
|
|
|
|
for width, rows in glyphs:
|
|
|
|
|
assert len(rows) == height, name
|
|
|
|
|
for row in rows:
|
|
|
|
|
for start in range(0, width, 8):
|
|
|
|
|
byte = 0
|
|
|
|
|
for bit in range(8):
|
|
|
|
|
x = start + bit
|
|
|
|
|
if x < width and row[x] == "#":
|
|
|
|
|
byte |= 0x80 >> bit
|
|
|
|
|
data.append(byte)
|
|
|
|
|
return bytes(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def classic():
|
|
|
|
|
source = open(os.path.join(GFX, "glcdfont.c")).read()
|
|
|
|
|
font = numbers(array_body(source, "font"))
|
|
|
|
|
glyphs = []
|
|
|
|
|
for c in range(FIRST, LAST + 1):
|
|
|
|
|
columns = font[c * 5 : c * 5 + 5]
|
|
|
|
|
rows = ["".join("#" if columns[x] >> y & 1 else "." for x in range(5)) for y in range(8)]
|
|
|
|
|
glyphs.append((5, rows))
|
2026-10-15 21:01:14 +02:00
|
|
|
glyphs.append((4, DEGREE[8]))
|
2026-10-15 19:43:59 +02:00
|
|
|
return encode("Normal", 8, 1, glyphs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tom_thumb():
|
|
|
|
|
source = open(os.path.join(GFX, "Fonts", "TomThumb.h")).read()
|
|
|
|
|
bitmaps = numbers(array_body(source, "TomThumbBitmaps"))
|
|
|
|
|
table = numbers(array_body(source, "TomThumbGlyphs"))
|
|
|
|
|
glyphs = []
|
|
|
|
|
for i, c in enumerate(range(FIRST, LAST + 1)):
|
|
|
|
|
offset, w, h, advance, x_offset, y_offset = table[i * 6 : i * 6 + 6]
|
|
|
|
|
width = max(advance - 1, 1)
|
|
|
|
|
rows = [["."] * width for _ in range(6)]
|
|
|
|
|
bit = 0
|
|
|
|
|
for y in range(h):
|
|
|
|
|
for x in range(w):
|
|
|
|
|
byte = bitmaps[offset + bit // 8]
|
|
|
|
|
if byte & (0x80 >> (bit % 8)):
|
|
|
|
|
rows[5 + y_offset + y][x_offset + x] = "#"
|
|
|
|
|
bit += 1
|
|
|
|
|
if c == ord(" "):
|
|
|
|
|
rows = [["."] * 2 for _ in range(6)]
|
|
|
|
|
width = 2
|
|
|
|
|
glyphs.append((width, ["".join(r) for r in rows]))
|
|
|
|
|
# Digits all as wide as the widest one (right aligned), so a clock doesn't move
|
|
|
|
|
digits = range(ord("0") - FIRST, ord("9") - FIRST + 1)
|
|
|
|
|
widest = max(glyphs[i][0] for i in digits)
|
|
|
|
|
for i in digits:
|
|
|
|
|
width, rows = glyphs[i]
|
|
|
|
|
glyphs[i] = (widest, ["." * (widest - width) + r for r in rows])
|
2026-10-15 21:01:14 +02:00
|
|
|
glyphs.append((3, DEGREE[6]))
|
2026-10-15 19:43:59 +02:00
|
|
|
return encode("Small", 6, 1, glyphs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Full height digits: seven segments, 10 x 19 dots
|
|
|
|
|
SEGMENTS = {
|
|
|
|
|
"a": (0, 0, 10, 2), # x, y, width, height
|
|
|
|
|
"b": (7, 0, 3, 11),
|
|
|
|
|
"c": (7, 8, 3, 11),
|
|
|
|
|
"d": (0, 17, 10, 2),
|
|
|
|
|
"e": (0, 8, 3, 11),
|
|
|
|
|
"f": (0, 0, 3, 11),
|
|
|
|
|
"g": (0, 8, 10, 3),
|
|
|
|
|
}
|
|
|
|
|
DIGITS = ["abcdef", "bc", "abdeg", "abcdg", "bcfg", "acdfg", "acdefg", "abc", "abcdefg", "abcdfg"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rounded(rows):
|
|
|
|
|
"""Rounds the outer corners: clears a corner dot of the glyph box or of the middle bar
|
|
|
|
|
when it has no neighbour above or below and none left or right"""
|
|
|
|
|
height, width = len(rows), len(rows[0])
|
|
|
|
|
on = lambda x, y: 0 <= x < width and 0 <= y < height and rows[y][x] == "#"
|
|
|
|
|
out = [list(r) for r in rows]
|
|
|
|
|
for y in (0, 8, 10, height - 1):
|
|
|
|
|
for x in (0, width - 1):
|
|
|
|
|
if on(x, y) and not (on(x - 1, y) and on(x + 1, y)) and not (on(x, y - 1) and on(x, y + 1)) and (not on(x - 1, y) or not on(x + 1, y)) and (not on(x, y - 1) or not on(x, y + 1)):
|
|
|
|
|
out[y][x] = "."
|
|
|
|
|
return ["".join(r) for r in out]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def box(width, height, dots):
|
|
|
|
|
rows = [["."] * width for _ in range(height)]
|
|
|
|
|
for x, y, w, h in dots:
|
|
|
|
|
for yy in range(y, y + h):
|
|
|
|
|
for xx in range(x, x + w):
|
|
|
|
|
rows[yy][xx] = "#"
|
|
|
|
|
return ["".join(r) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def full_height():
|
|
|
|
|
special = {
|
|
|
|
|
" ": box(4, 19, []),
|
|
|
|
|
":": box(3, 19, [(0, 4, 3, 3), (0, 12, 3, 3)]),
|
|
|
|
|
".": box(3, 19, [(0, 16, 3, 3)]),
|
|
|
|
|
"-": box(6, 19, [(0, 8, 6, 3)]),
|
|
|
|
|
}
|
|
|
|
|
glyphs = []
|
|
|
|
|
for c in range(FIRST, LAST + 1):
|
|
|
|
|
ch = chr(c)
|
|
|
|
|
if ch.isdigit():
|
|
|
|
|
rows = rounded(box(10, 19, [SEGMENTS[s] for s in DIGITS[int(ch)]]))
|
|
|
|
|
glyphs.append((10, rows))
|
|
|
|
|
elif ch in special:
|
|
|
|
|
glyphs.append((len(special[ch][0]), special[ch]))
|
|
|
|
|
else:
|
|
|
|
|
glyphs.append((0, [""] * 19)) # not in this font: skipped
|
2026-10-15 21:01:14 +02:00
|
|
|
glyphs.append((6, DEGREE[19]))
|
2026-10-15 19:43:59 +02:00
|
|
|
return encode("Full height", 19, 2, glyphs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def c_array(name, data):
|
|
|
|
|
lines = []
|
|
|
|
|
for i in range(0, len(data), 16):
|
|
|
|
|
lines.append("\t" + ", ".join("0x%02x" % b for b in data[i : i + 16]) + ",")
|
|
|
|
|
return "static const uint8_t %s[] = {\n%s\n};\n" % (name, "\n".join(lines))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LOOKUP = """
|
|
|
|
|
const BuiltinFont *findBuiltinFont(const char *name)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < BUILTIN_FONT_COUNT; i++)
|
|
|
|
|
if (sameName(BUILTIN_FONTS[i].name, name))
|
|
|
|
|
return &BUILTIN_FONTS[i];
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Font builtinFont(const BuiltinFont &font)
|
|
|
|
|
{
|
|
|
|
|
Font f;
|
|
|
|
|
f.data = font.data;
|
|
|
|
|
f.size = font.size;
|
|
|
|
|
f.scale = font.scale;
|
|
|
|
|
return f;
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
fonts = [("FONT_SMALL", tom_thumb()), ("FONT_NORMAL", classic()), ("FONT_FULL", full_height())]
|
|
|
|
|
out = [
|
|
|
|
|
"// Generated by tools/make_builtin_fonts.py, do not edit.\n",
|
|
|
|
|
"// Small: TomThumb (BSD license, Brian J. Swetland, Vassilii Khachaturov, Dan Marks,\n",
|
|
|
|
|
"// Robey Pointer); Normal: Adafruit GFX glcdfont (BSD license, Adafruit Industries).\n\n",
|
|
|
|
|
'#include "logic/builtin_fonts.h"\n\n',
|
|
|
|
|
'#include "logic/bitmap_format.h"\n\n',
|
|
|
|
|
]
|
|
|
|
|
for name, data in fonts:
|
|
|
|
|
out.append(c_array(name, data) + "\n")
|
|
|
|
|
out.append("const BuiltinFont BUILTIN_FONTS[] = {\n")
|
|
|
|
|
out.append('\t{"Small", FONT_SMALL, sizeof(FONT_SMALL), 1},\n')
|
|
|
|
|
out.append('\t{"Normal", FONT_NORMAL, sizeof(FONT_NORMAL), 1},\n')
|
|
|
|
|
out.append('\t{"Large", FONT_NORMAL, sizeof(FONT_NORMAL), 2},\n')
|
|
|
|
|
out.append('\t{"Full height", FONT_FULL, sizeof(FONT_FULL), 1},\n')
|
|
|
|
|
out.append("};\n")
|
|
|
|
|
out.append("const int BUILTIN_FONT_COUNT = sizeof(BUILTIN_FONTS) / sizeof(BUILTIN_FONTS[0]);\n")
|
|
|
|
|
out.append(LOOKUP)
|
|
|
|
|
with open(OUT, "w", newline="\n") as f:
|
|
|
|
|
f.write("".join(out))
|
|
|
|
|
print("Wrote", OUT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|