Files

215 lines
7.6 KiB
Python
Raw Permalink Normal View History

"""Generates src/esp32/logic/builtin_fonts.cpp: the built-in fonts of the dynamic pages.
Characters 32-126, and 127 as the degree sign.
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
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,
}
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))
glyphs.append((4, DEGREE[8]))
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])
glyphs.append((3, DEGREE[6]))
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
glyphs.append((6, DEGREE[19]))
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()