"""Generates src/esp32/logic/builtin_fonts.cpp: the built-in fonts of the dynamic pages. 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 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)) 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]) 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 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()