Tactility/Buildscripts/generate-fonts.py
Ken Van Hoeylandt 0ebd471cc0
Reduce internal RAM use: embedded assets, fonts in flash, PSRAM stacks, file_ops (#701)
- The `/system` FAT volume is removed. Its files are embedded in the firmware as generated C arrays.
- Default-size fonts are generated at build time and used from flash.
- The LVGL task and the Launcher run on PSRAM-preferred stacks. A new kernel `file_ops` API takes the file work off those threads.
- `MainDispatcher` is reimplemented as an on-demand task.
- More long-lived state is allocated with the PSRAM-preferring helpers.
- Other fixes and improvements
2026-10-18 17:30:43 +02:00

231 lines
9.5 KiB
Python

#!/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 <device-folder> <simulator: 0 or 1> <output-folder>
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]} <device-folder> <simulator: 0 or 1> <output-folder>")
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 <stddef.h>",
"#include <stdint.h>",
"",
"#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()