#!/usr/bin/env python3 """ Generates the binfont files that a device needs at its default font size, as C byte arrays. Usage: generate-fonts.py The fonts are generated by Buildscripts/BinfontTool, built with the host compiler. It uses the same generator as the firmware, so the result is identical to generating the fonts on the device. Writes generated_fonts.c and generated_fonts.h to the output folder. The entries are found by the file name that the firmware uses for its font cache. The table is empty when no host compiler is available. The fonts, sizes and bits per pixel must match Tactility/Source/lvgl/Fonts.cpp, FontSizes.cpp and FontCache.cpp. """ import os import re import shutil import struct import subprocess import sys import tempfile ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) FONTS = os.path.join(ROOT, "Data", "system", "fonts") DEFAULT_FONT_SIZE = 14 def read_text(path: str) -> str: with open(path, "r", encoding="utf-8") as file: return file.read() def read_properties(path: str) -> dict: properties = {} for line in read_text(path).splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: key, value = line.split("=", 1) properties[key.strip()] = value.strip().strip('"') return properties def float32(value: float) -> float: return struct.unpack("f", struct.pack("f", value))[0] def scale(size: int, factor: float) -> int: # Same as lround(size * factor) with a float factor, in float arithmetic product = float32(size * float32(factor)) return int(product + 0.5) if product >= 0 else int(product - 0.5) def scale_double(size: int, factor: float) -> int: # Same as lround(size * factor) for a double factor product = size * factor return int(product + 0.5) def get_defines(path: str) -> dict: return {name: int(value.rstrip("uU"), 0) for name, value in re.findall(r"#define\s+(\w+)\s+(0x[0-9A-Fa-f]+u?|\d+u?)", read_text(path))} def get_icon_names() -> dict: source = read_text(os.path.join(ROOT, "Modules", "lvgl-module", "source", "icon_names.c")) names = {} for key in ("shared", "statusbar", "launcher"): body = re.search(r"lvgl_icon_%s_names\[\]\s*=\s*\{(.*?)\};" % key, source, re.S).group(1) names[key] = re.findall(r'"([^"]+)"', body) return names def get_icon_codepoints(names: list) -> list: codepoints = {} for line in read_text(os.path.join(FONTS, "MaterialSymbolsRounded.codepoints")).splitlines(): parts = line.split(" ") if len(parts) == 2: codepoints[parts[0]] = int(parts[1], 16) return [codepoints[name] for name in names if name in codepoints] def get_device_settings(device_folder: str, simulator: bool) -> tuple: """@return the default font size and the bits per pixel""" properties = read_properties(os.path.join(device_folder, "device.properties")) font_size = int(properties.get("font.defaultSize") or properties.get("lvgl.fontSize") or DEFAULT_FONT_SIZE) has_psram = properties.get("hardware.spiRam", "false") == "true" and not simulator monochrome = any( re.search(r"ssd1306|st7305", read_text(os.path.join(device_folder, name))) for name in os.listdir(device_folder) if name.endswith(".dts") ) # Same choice as getGeneratedFontBpp() bpp = 1 if monochrome else (4 if has_psram else 2) return font_size, bpp def get_jobs(font_size: int, bpp: int) -> list: """@return a list of (file name, ttf file, size, codepoints or None)""" text_version = get_defines(os.path.join(ROOT, "Tactility", "Private", "Tactility", "lvgl", "FontVersions.h")) icon_version = get_defines(os.path.join(ROOT, "Modules", "lvgl-module", "include", "lvgl", "icons", "names.h"))["LVGL_ICON_FONT_VERSION"] icon_names = get_icon_names() def file_name(prefix: str, name: str, size: int, version: int) -> str: return f"{prefix}{name}_{size}_{bpp}bpp_{version:08x}.bin" jobs = [] sans = os.path.join(FONTS, "AdwaitaSans.ttf") text_fonts = [("small", scale(font_size, 0.75)), ("default", font_size), ("large", scale(font_size, 1.25))] for name, size in text_fonts: jobs.append((file_name("text_", name, size, text_version["TT_TEXT_FONT_VERSION"]), sans, size, None)) jobs.append((file_name("mono_", "mono", font_size, text_version["TT_MONO_FONT_VERSION"]), os.path.join(FONTS, "AdwaitaMono.ttf"), font_size, None)) shared_size = scale_double(font_size, 1.25) icon_fonts = [ ("statusbar", scale(font_size, 1.15), icon_names["statusbar"]), ("launcher", scale(font_size, 2.6), icon_names["launcher"]), ("shared_default", shared_size, icon_names["shared"]), ("shared_large", shared_size * 2, icon_names["shared"]), ] for name, size, names in icon_fonts: jobs.append((file_name("material_symbols_", name, size, icon_version), os.path.join(FONTS, "MaterialSymbolsRounded.ttf"), size, get_icon_codepoints(names))) return jobs def build_tool(work_folder: str): """@return the path of the tool, or None when there's no host compiler""" compiler = next((found for found in (shutil.which(name) for name in ("c++", "g++", "clang++")) if found), None) if compiler is None: return None tool = os.path.join(work_folder, "binfont_gen") command = [ compiler, "-O2", "-std=c++20", "-I" + os.path.join(ROOT, "Modules", "binfont-module", "include"), "-I" + os.path.join(ROOT, "Modules", "binfont-module", "private"), "-I" + os.path.join(ROOT, "TactilityKernel", "include"), os.path.join(ROOT, "Buildscripts", "BinfontTool", "binfont_gen.cpp"), os.path.join(ROOT, "Modules", "binfont-module", "source", "generator.cpp"), os.path.join(ROOT, "TactilityKernel", "source", "memory_posix.cpp"), "-o", tool, ] if subprocess.run(command).returncode != 0: return None return tool def write_if_changed(path: str, content: str): if os.path.exists(path) and read_text(path) == content: return with open(path, "w", encoding="utf-8", newline="") as file: file.write(content) def main(): if len(sys.argv) != 4: print(f"Usage: {sys.argv[0]} ") sys.exit(1) device_folder, simulator, output_folder = sys.argv[1], sys.argv[2] == "1", sys.argv[3] os.makedirs(output_folder, exist_ok=True) font_size, bpp = get_device_settings(device_folder, simulator) jobs = get_jobs(font_size, bpp) fonts = [] with tempfile.TemporaryDirectory() as work_folder: tool = build_tool(work_folder) if tool is None: print("WARNING: no host C++ compiler found, fonts are not embedded in the firmware") else: lines = [] for index, (name, ttf, size, codepoints) in enumerate(jobs): codepoints_path = "-" if codepoints is not None: codepoints_path = os.path.join(work_folder, f"{index}.codepoints") with open(codepoints_path, "w") as file: file.write("".join(f"{codepoint}\n" for codepoint in codepoints)) lines.append(f"{index}\t{ttf}\t{size}\t{bpp}\t{codepoints_path}\n") jobs_path = os.path.join(work_folder, "jobs.txt") with open(jobs_path, "w") as file: file.writelines(lines) if subprocess.run([tool, jobs_path, work_folder]).returncode != 0: sys.exit(1) for index, (name, _, _, _) in enumerate(jobs): with open(os.path.join(work_folder, f"{index}.bin"), "rb") as file: fonts.append((name, file.read())) header = [ "// Generated by Buildscripts/generate-fonts.py. Do not edit.", "#pragma once", "", "#include ", "#include ", "", "#ifdef __cplusplus", 'extern "C" {', "#endif", "", "/** A binfont file that is part of the firmware, found by the file name that the font cache uses */", "struct GeneratedFont {", " const char* file_name;", " const uint8_t* data;", " size_t size;", "};", "", "extern const struct GeneratedFont generated_fonts[];", "extern const size_t generated_fonts_count;", "", "#ifdef __cplusplus", "}", "#endif", "", ] source = ['// Generated by Buildscripts/generate-fonts.py. Do not edit.', '#include "generated_fonts.h"', ""] for index, (name, data) in enumerate(fonts): source.append(f"static const uint8_t font_{index}[] = {{") for offset in range(0, len(data), 24): source.append(" " + ", ".join(f"0x{byte:02x}" for byte in data[offset:offset + 24]) + ",") source.append("};") source.append("") source.append("const struct GeneratedFont generated_fonts[] = {") for index, (name, data) in enumerate(fonts): source.append(f' {{ "{name}", font_{index}, {len(data)} }},') if not fonts: source.append(" { 0, 0, 0 }") source.append("};") source.append(f"const size_t generated_fonts_count = {len(fonts)};") source.append("") write_if_changed(os.path.join(output_folder, "generated_fonts.h"), "\n".join(header)) write_if_changed(os.path.join(output_folder, "generated_fonts.c"), "\n".join(source)) print(f"Generated {len(fonts)} fonts ({sum(len(data) for _, data in fonts)} bytes)") if __name__ == "__main__": main()