Tactility/Modules/lvgl-module/generate-icons.py
Ken Van Hoeylandt 24ef79070a
App UX/UI updates and more (#678)
- Refactored many apps' UI/UX
- Improve material themes
- Fix for Tab5 flickering on v1
- Extend dts parser for i2c address/reg support in code
- Fix for I2C Scanner glitch where it would show devices that aren't connected
- Add name labels to I2C Scanner's scanned devices
2026-10-08 22:02:18 +02:00

264 lines
9.6 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",
"hard_disk", # System Info
"help", # Diceware help
"hub", # App Hub
"info", # System Info
"image", # Screenshot app
"keyboard_arrow_up",
"lightbulb",
"language", # Globe
"lists", # Chat app toolbar
"mail",
"menu",
"memory", # System Info
"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",
"select_window_2", # System Info
"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",
"phone_android", # Display settings: orientation
]
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
statusbar_symbol_code_point_names = [
# Location tracking
"location_on",
"location_off",
# 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)