#!/usr/bin/env python3 """Vectorize LCD TTF fonts into SVG symbol sets for the hub clock widget.""" from __future__ import annotations import argparse import re from dataclasses import dataclass from pathlib import Path from fontTools.pens.boundsPen import BoundsPen from fontTools.pens.svgPathPen import SVGPathPen from fontTools.ttLib import TTFont CHARS = ( [" "] + [str(d) for d in range(10)] + [chr(c) for c in range(ord("A"), ord("Z") + 1)] + [":", "+", "-", "/", "."] ) TARGET_HEIGHT = 32.0 PADDING_X = 1.5 PADDING_Y = 1.0 @dataclass(frozen=True) class FontSpec: ttf: str prefix: str outfile: str comment: str FONTS: dict[str, FontSpec] = { "dseg14-classic": FontSpec( ttf="DSEG14Classic-Regular.ttf", prefix="d14c", outfile="dseg14-classic-alphabet.svg", comment="DSEG14 Classic (OFL) — vectorized for hub clock", ), "dseg14-modern": FontSpec( ttf="DSEG14Modern-Regular.ttf", prefix="d14m", outfile="dseg14-modern-alphabet.svg", comment="DSEG14 Modern (OFL) — vectorized for hub clock", ), "ds-digital": FontSpec( ttf="DS-Digital.ttf", prefix="dsd", outfile="ds-digital-alphabet.svg", comment="DS-Digital by Dusit Supasawat (shareware) — vectorized for hub clock", ), "digital-7-mono-italic": FontSpec( ttf="digital-7-mono-italic.ttf", prefix="d7mi", outfile="digital-7-mono-italic-alphabet.svg", comment="Digital-7 Mono Italic by Style-7 (personal use) — vectorized for hub clock", ), } def symbol_id(prefix: str, ch: str) -> str: if ch == " ": return f"{prefix}-space" if ch == ":": return f"{prefix}-colon" if ch == "+": return f"{prefix}-plus" if ch == "-": return f"{prefix}-minus" if ch == "/": return f"{prefix}-slash" if ch == ".": return f"{prefix}-dot" if len(ch) == 1 and "0" <= ch <= "9": return f"{prefix}-{ch}" if len(ch) == 1 and "A" <= ch <= "Z": return f"{prefix}-{ch}" return f"{prefix}-u{ord(ch):04x}" def glyph_path(font: TTFont, ch: str) -> tuple[str, float, float, float, float] | None: cmap = font.getBestCmap() code = ord(ch) if code not in cmap: return None glyph_name = cmap[code] glyph_set = font.getGlyphSet() if glyph_name not in glyph_set: return None glyph = glyph_set[glyph_name] pen = SVGPathPen(glyph_set) bounds_pen = BoundsPen(glyph_set) glyph.draw(bounds_pen) if bounds_pen.bounds is None: return None x_min, y_min, x_max, y_max = bounds_pen.bounds glyph.draw(pen) path = pen.getCommands() if not path.strip(): return None return path, x_min, y_min, x_max, y_max def char_width(font: TTFont, ch: str, fallback: float) -> float: cmap = font.getBestCmap() code = ord(ch) if code not in cmap: return fallback glyph_name = cmap[code] if glyph_name in font["hmtx"].metrics: adv, _ = font["hmtx"].metrics[glyph_name] return float(adv) return fallback def build_alphabet(spec: FontSpec, fonts_dir: Path, out_dir: Path) -> None: ttf_path = fonts_dir / spec.ttf font = TTFont(ttf_path) units_per_em = float(font["head"].unitsPerEm or 1000) lines = [ '", ""]) out_path = out_dir / spec.outfile out_path.write_text("\n".join(lines), encoding="utf-8") print(f"Wrote {out_path}") def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--fonts-dir", type=Path, default=Path(__file__).resolve().parent / "fonts", ) parser.add_argument( "--out-dir", type=Path, default=Path(__file__).resolve().parent, ) parser.add_argument( "--only", choices=sorted(FONTS.keys()), action="append", help="Build one font (repeatable). Default: all.", ) args = parser.parse_args() selected = args.only or sorted(FONTS.keys()) for key in selected: build_alphabet(FONTS[key], args.fonts_dir, args.out_dir) if __name__ == "__main__": main()