1
0
mirror of git://f0xx.org/ac/ac-be-hub synced 2026-07-29 04:18:53 +03:00
Files
ac-be-hub/assets/build_ttf_lcd_alphabet.py
Anton Afanasyeu 6013b848c5 initial
2026-06-23 12:29:37 +02:00

197 lines
5.8 KiB
Python

#!/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 = [
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" '
'style="position:absolute;width:0;height:0;overflow:hidden">',
f" <!-- {spec.comment} -->",
" <defs>",
]
for ch in CHARS:
sid = symbol_id(spec.prefix, ch)
if ch == " ":
width = max(8.0, TARGET_HEIGHT * 0.35)
lines.append(
f' <symbol id="{sid}" viewBox="0 0 {width:.2f} {TARGET_HEIGHT:.2f}"></symbol>'
)
continue
parsed = glyph_path(font, ch)
if parsed is None and ch.isalpha():
parsed = glyph_path(font, ch.lower())
if parsed is None:
width = char_width(font, "0", 20.0) * (TARGET_HEIGHT / units_per_em)
lines.append(
f' <symbol id="{sid}" viewBox="0 0 {width:.2f} {TARGET_HEIGHT:.2f}"></symbol>'
)
continue
path, x_min, y_min, x_max, y_max = parsed
glyph_w = max(x_max - x_min, 1.0)
glyph_h = max(y_max - y_min, 1.0)
scale = (TARGET_HEIGHT - PADDING_Y * 2) / glyph_h
tx = PADDING_X - x_min * scale
ty = TARGET_HEIGHT - PADDING_Y - y_max * scale
width = max(glyph_w * scale + PADDING_X * 2, char_width(font, ch, glyph_w) * scale * 0.55)
# Flip Y: font coords are bottom-up; SVG is top-down.
transform = f"translate({tx:.3f} {ty:.3f}) scale({scale:.6f} {scale:.6f})"
lines.append(f' <symbol id="{sid}" viewBox="0 0 {width:.2f} {TARGET_HEIGHT:.2f}">')
lines.append(f' <path class="lcd-active" d="{path}" transform="{transform}" />')
lines.append(" </symbol>")
lines.extend([" </defs>", "</svg>", ""])
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()