Files
homeassistant_espscreen/tools/generate_icons.py
T
Max Gramser ef0ca8b350 Custom icon per tile: 150 MDI icons in firmware and editor
- screen_manager/app/tile_icons.py is the single icon list (12 groups, 20 media
  and music icons); tools/generate_icons.py writes it into the three icon fonts
  of both boards and builds the editor's subset font.
- Firmware 0.2.18 shows o.icon when its fonts carry the glyph, else the domain
  icon. The app sends the chosen icon, or Home Assistant's own mdi icon on auto.
- Tile sheet gets an icon picker with search; the mockup shows the icon the
  screen actually draws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:16:57 +02:00

103 lines
4.2 KiB
Python

"""Write the tile icon set into both board profiles and build the editor's icon font.
The list lives in screen_manager/app/tile_icons.py. Every Material Design Icons
font of a board profile gets exactly those glyphs (the first font carries the
list as a YAML anchor, the smaller sizes reuse it), and the editor gets a subset
of the same TTF so its mockup shows the glyphs the screen draws. Run
tools/generate_packages.py afterwards; --check changes nothing and fails when
anything is out of date.
"""
import argparse
import io
import re
import sys
from pathlib import Path
from fontTools import subset
from fontTools.ttLib import TTFont
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
import tile_icons # noqa: E402
TTF = ROOT / 'fonts/materialdesignicons-webfont.ttf'
PROFILES = [ROOT / 'home-like-2432s028.yaml', ROOT / 'guition-4848s040.yaml']
WEB_FONT = ROOT / 'screen_manager/app/static/tile-icons.woff'
FONT_BLOCK = re.compile(r"( - file: 'fonts/materialdesignicons-webfont\.ttf'\n id: \w+\n size: \d+\n) glyphs:.*\n(?: .*\n)*")
def verify():
"""Names and codepoints must match the TTF the firmware is built from."""
codepoints = {}
for code, name in TTFont(TTF).getBestCmap().items():
codepoints.setdefault(name, code)
codepoints[name] = min(codepoints[name], code)
seen = set()
for name, code in tile_icons.GLYPHS.items():
if codepoints.get(name) != int(code, 16):
raise SystemExit(f'{name}: codepoint {code} does not match {TTF.name}')
if code in seen:
raise SystemExit(f'Duplicate glyph {code}')
seen.add(code)
if len(tile_icons.ICONS) != sum(len(icons) for _, icons in tile_icons.GROUPS):
raise SystemExit('An icon appears in more than one group')
def glyph_list():
lines = [' glyphs: &tile_icons # GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py']
lines += [f' - "\\U000{code}" # {name}' for name, code in tile_icons.GLYPHS.items()]
return '\n'.join(lines) + '\n'
def profile(text):
blocks = list(FONT_BLOCK.finditer(text))
if len(blocks) != 3:
raise SystemExit(f'Expected three icon fonts, found {len(blocks)}')
out, last = [], 0
for index, block in enumerate(blocks):
out += [text[last:block.start()], block[1], glyph_list() if index == 0 else ' glyphs: *tile_icons\n']
last = block.end()
return ''.join(out) + text[last:]
def web_font():
font = TTFont(TTF)
options = subset.Options()
options.flavor = 'woff'
options.layout_features = []
options.name_IDs = []
options.notdef_outline = False
subsetter = subset.Subsetter(options)
subsetter.populate(unicodes=[int(code, 16) for code in tile_icons.GLYPHS.values()])
subsetter.subset(font)
# The source stores xMin 0 with left bearing 0; saving recalculates xMin, and browsers
# then shift each glyph left by the difference. Keep the bearing equal to the real xMin.
glyf, hmtx = font['glyf'], font['hmtx']
for name in font.getGlyphOrder():
glyph = glyf[name]
glyph.recalcBounds(glyf)
hmtx[name] = (hmtx[name][0], getattr(glyph, 'xMin', 0))
buffer = io.BytesIO()
font.flavor = 'woff'
font.save(buffer)
return buffer.getvalue()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--check', action='store_true')
args = parser.parse_args()
verify()
for path in PROFILES:
expected = profile(path.read_text())
if args.check:
if path.read_text() != expected: raise SystemExit(f'Outdated icon fonts: {path.name}')
else:
path.write_text(expected)
if args.check:
# Compare glyph coverage, not bytes: font output differs between fontTools versions.
wanted = {int(code, 16) for code in tile_icons.GLYPHS.values()}
if not WEB_FONT.exists() or set(TTFont(WEB_FONT).getBestCmap()) != wanted:
raise SystemExit(f'Outdated: {WEB_FONT.name}')
else:
WEB_FONT.write_bytes(web_font())
print(f'{len(tile_icons.ICONS)} pickable icons, {len(tile_icons.GLYPHS)} glyphs: ' + ('verified' if args.check else 'generated'))
if __name__ == '__main__':
main()