#!/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 \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 \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 "list_alt", # Settings apps: opens a selection dialog "hourglass", # App Hub: an operation for the app is pending "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 # System menu toggles "wifi_off", "bluetooth_disabled", "location_on", "location_off", "terminal", "logo_dev", "camera", "lightstrip", "phone_android", # Display settings: orientation # Files app "draft", "description", "link", "sd_card", "content_copy", "content_cut", "content_paste", "edit", "play_arrow", "eject", "create_new_folder", # Notes app "save", "save_as", "folder_open", ] # 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", # Mesh networking "hub", # App Hub installing an app "download", ] # 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)