Tactility/Modules/lvgl-module/generate-icons.py
Ken Van Hoeylandt 04ada9df87
Dynamic font loading and more (#674)
**Fonts**

Dynamically load text and icon fonts when the device boots.

Font bitdepth depends on whether the device is an ESP32 one and whether it has external RAM available.
When no external RAM is available on ESP32, only small and medium font variants are generated; large font variants refer to the medium size ones.

Implemented Appearance settings app for changing font and font size.

**Other changes**

- Added keyboard shortcuts to delete or rename files in directory views.
- Improved app-grid tile sizing to provide more room for labels.
- Reduced the default LED strip brightness.
2026-10-05 00:00:08 +02:00

258 lines
9.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Generates the Material Symbols icon resources:
- include/lvgl/icons/*.h: UTF-8 string defines per icon
- include/lvgl/icons/names.h and source/icon_names.c: icon name lists per icon font
- Data/system/fonts/MaterialSymbolsRounded.ttf: TTF subset with only the used icons
- Data/system/fonts/MaterialSymbolsRounded.codepoints: name to codepoint map of the used icons
The icon fonts are rasterized from the TTF subset on device at startup.
Requires the fonttools package: pip install fonttools
"""
import os
import urllib.request
import zlib
from fontTools import subset
from fontTools.ttLib import TTFont
from fontTools.varLib import instancer
def download_file(url: str, filename: str):
if not os.path.exists(filename):
print(f"Downloading {filename} from {url}")
urllib.request.urlretrieve(url, filename)
else:
print(f"{filename} already exists, skipping download.")
def read_code_points_map(file_path: str):
codepoints = {}
with open(file_path, 'r') as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2:
name, cp = parts[0].lower(), parts[1]
codepoints[name] = cp
if len(codepoints) == 0:
raise ValueError(f"Code points map is empty or wasn't found at {file_path}")
return codepoints
def get_code_points(codepoints, names: list) -> list:
result = []
for name in names:
safe_name = name.lower()
if not safe_name in codepoints:
raise ValueError(f"Code point '{safe_name}' not found in map")
result.append(f"0x{codepoints[safe_name].upper()}")
return result
def generate_subset_ttf(font_file: str, codepoints: list, output_path: str) -> bytes:
print(f"Generating {output_path}")
font = TTFont(font_file, recalcTimestamp=False)
if "fvar" in font:
# Pin all axes to their defaults, which are the outlines lv_font_conv used to render
font = instancer.instantiateVariableFont(font, {axis.axisTag: None for axis in font["fvar"].axes})
options = subset.Options()
options.layout_features = []
options.drop_tables += ["GSUB", "GPOS", "GDEF"]
options.hinting = False
options.notdef_outline = True
subsetter = subset.Subsetter(options)
subsetter.populate(unicodes=codepoints)
subsetter.subset(font)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
font.save(output_path)
with open(output_path, "rb") as f:
return f.read()
def generate_codepoints_file(codepoint_map: dict, names: list, output_path: str) -> bytes:
print(f"Generating {output_path}")
content = "".join(f"{name} {codepoint_map[name]}\n" for name in names)
with open(output_path, "w") as f:
f.write(content)
return content.encode("utf-8")
def generate_icon_name_lists(icon_sets: list, version_input: bytes):
version = zlib.crc32(version_input + repr(icon_sets).encode("utf-8")) & 0xFFFFFFFF
header_path = os.path.join("include", "lvgl", "icons", "names.h")
print(f"Generating {header_path}")
with open(header_path, "w") as f:
f.write("// SPDX-License-Identifier: Apache-2.0\n\n")
f.write("#pragma once\n\n")
f.write("#include <stddef.h>\n\n")
f.write("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n")
f.write("/** Changes whenever the icon TTF subset, its codepoints file or the icon name lists change */\n")
f.write(f"#define LVGL_ICON_FONT_VERSION 0x{version:08X}u\n\n")
f.write("// Icon names per icon font, resolved through the .codepoints file next to the TTF\n")
for name, _ in icon_sets:
f.write(f"extern const char* const lvgl_icon_{name}_names[];\n")
f.write(f"extern const size_t lvgl_icon_{name}_name_count;\n\n")
f.write("#ifdef __cplusplus\n}\n#endif\n")
source_path = os.path.join("source", "icon_names.c")
print(f"Generating {source_path}")
with open(source_path, "w") as f:
f.write("// SPDX-License-Identifier: Apache-2.0\n")
f.write("// Generated by generate-icons.py\n\n")
f.write("#include <lvgl/icons/names.h>\n\n")
for name, names in icon_sets:
f.write(f"const char* const lvgl_icon_{name}_names[] = {{\n")
for icon_name in names:
f.write(f" \"{icon_name.lower()}\",\n")
f.write("};\n\n")
f.write(f"const size_t lvgl_icon_{name}_name_count = sizeof(lvgl_icon_{name}_names) / sizeof(lvgl_icon_{name}_names[0]);\n\n")
def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_name: str):
filename = f"{variable_name}.h"
print(f"Generating {filename}")
output_path = os.path.join("include", "lvgl", "icons", filename)
with open(output_path, 'w') as f:
f.write("// SPDX-License-Identifier: Apache-2.0\n\n")
f.write("#pragma once\n\n")
for name in codepoint_names:
safe_name = name.lower()
if safe_name in codepoint_map:
hex_val = codepoint_map[safe_name]
# Convert hex to int
val = int(hex_val, 16)
# Convert to UTF-8 escaped string
utf8_bytes = chr(val).encode('utf-8')
escaped_str = "".join(f"\\x{b:02X}" for b in utf8_bytes)
f.write(f'#define LVGL_ICON_{variable_name.upper()}_{safe_name.upper()} "{escaped_str}"\n')
else:
print(f"Warning: {safe_name} not found in codepoint map")
# --------------- Symbol Fonts ---------------
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
shared_symbol_code_point_names = [
"add",
"apps",
"area_chart", # System Info
"app_registration", # App Settings app
"calendar_month",
"cable",
"circle",
"close",
"cloud", # Web server
"check",
"delete",
"devices", # Developer app
"display_settings", # Display Settings app
"edit_note",
"electric_bolt", # Power (settings) app
"folder",
"deployed_code", # 3D cube
"download",
"forum", # Chat app
"gamepad",
"help", # Diceware help
"hub", # App Hub
"image", # Screenshot app
"keyboard_arrow_up",
"lightbulb",
"language", # Globe
"lists", # Chat app toolbar
"mail",
"menu",
"mop",
"more_vert",
"music_note",
"note_add",
"palette",
"power_settings_new", # Power off for T-Lora Pager
"refresh", # e.g. App Hub reload button
"search",
"settings",
"toolbar", # Apps without custom icon
"navigation", # GPS (settings) app
"keyboard_alt", # Keyboard (settings) app
"usb", # Power (settings) app
"wifi", # WiFi (settings) app
"bluetooth", # Bluetooth (settings) app
"terminal",
"logo_dev",
"camera",
"lightstrip",
]
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
statusbar_symbol_code_point_names = [
# Location tracking
"location_on",
# Development server
"cloud",
# Low on available memory
"memory",
# SD Card
"sd_card",
"sd_card_alert",
# Wi-Fi
"signal_wifi_0_bar",
"network_wifi_1_bar",
"network_wifi_2_bar",
"network_wifi_3_bar",
"signal_wifi_4_bar",
"signal_wifi_off",
"signal_wifi_bad",
# Battery
"battery_android_frame_1",
"battery_android_frame_2",
"battery_android_frame_3",
"battery_android_frame_4",
"battery_android_frame_5",
"battery_android_frame_6",
"battery_android_frame_full",
"battery_android_frame_bolt",
# Bluetooth
"bluetooth",
"bluetooth_searching",
"bluetooth_connected",
"bluetooth_disabled",
"usb",
]
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
launcher_symbol_code_point_names = [
"apps",
"folder",
"settings"
]
# Resolve file path relative to this script so it can be executed from any CWD
base_dir = os.path.dirname(__file__)
if base_dir:
os.chdir(base_dir)
codepoints_url = "https://github.com/google/material-design-icons/raw/refs/heads/master/variablefont/MaterialSymbolsRounded%5BFILL,GRAD,opsz,wght%5D.codepoints"
ttf_url = "https://github.com/google/material-design-icons/raw/refs/heads/master/variablefont/MaterialSymbolsRounded%5BFILL,GRAD,opsz,wght%5D.ttf"
codepoints_filename = "MaterialSymbolsRounded.codepoints"
ttf_filename = "MaterialSymbolsRounded.ttf"
subset_ttf_path = os.path.join("..", "..", "Data", "system", "fonts", "MaterialSymbolsRounded.ttf")
subset_codepoints_path = os.path.join("..", "..", "Data", "system", "fonts", "MaterialSymbolsRounded.codepoints")
download_file(codepoints_url, codepoints_filename)
download_file(ttf_url, ttf_filename)
codepoints_map_path = codepoints_filename
codepoints_map = read_code_points_map(codepoints_map_path)
icon_sets = [
("shared", shared_symbol_code_point_names),
("statusbar", statusbar_symbol_code_point_names),
("launcher", launcher_symbol_code_point_names),
]
for variable_name, names in icon_sets:
generate_icon_names(codepoints_map, names, variable_name)
all_names = sorted({name.lower() for _, names in icon_sets for name in names})
all_codepoints = [int(cp, 16) for cp in get_code_points(codepoints_map, all_names)]
subset_data = generate_subset_ttf(ttf_filename, all_codepoints, subset_ttf_path)
codepoints_data = generate_codepoints_file(codepoints_map, all_names, subset_codepoints_path)
generate_icon_name_lists(icon_sets, subset_data + codepoints_data)