ee9c2bf1b5
- process.py: _dragonpilot_chars()改为直接解析.po文件提取字符 - 添加dragonpilot_zh-CHS.mo编译产物 - 重新生成所有字体(.fnt+.png),zh-CHS图集包含DP翻译全部中文字符 - 修复DP设置菜单汉字显示为?的问题
209 lines
7.3 KiB
Python
Executable File
209 lines
7.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from pathlib import Path
|
|
import gettext
|
|
import json
|
|
|
|
import pyray as rl
|
|
|
|
FONT_DIR = Path(__file__).resolve().parent
|
|
SELFDRIVE_DIR = FONT_DIR.parents[1]
|
|
TRANSLATIONS_DIR = SELFDRIVE_DIR / "ui" / "translations"
|
|
LANGUAGES_FILE = TRANSLATIONS_DIR / "languages.json"
|
|
|
|
GLYPH_PADDING = 6
|
|
EXTRA_CHARS = "–‑✓×°§•X⚙✕◀▶✔⌫⇧␣○●↳çêüñ–‑✓×°§•€£¥"
|
|
UNIFONT_LANGUAGES = {"th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
|
|
|
|
|
def _languages():
|
|
if not LANGUAGES_FILE.exists():
|
|
return {}
|
|
with LANGUAGES_FILE.open(encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def _dragonpilot_chars(code: str) -> set[str]:
|
|
"""Characters used by dragonpilot's own translations (dragonpilot_{code}.po).
|
|
|
|
dp settings translate via a separate catalog from openpilot's app_{code}.po,
|
|
so their glyphs must be baked too — otherwise translated dp settings render
|
|
as '?'. We parse the .po file directly to get translated strings, avoiding
|
|
.mo encoding issues with GNUTranslations."""
|
|
po_path = TRANSLATIONS_DIR / f"dragonpilot_{code}.po"
|
|
if not po_path.exists():
|
|
return set()
|
|
import re
|
|
text = po_path.read_text(encoding="utf-8")
|
|
chars: set[str] = set()
|
|
# Extract all msgstr values
|
|
for m in re.finditer(r'^msgstr\s+"(.*)"', text, re.MULTILINE):
|
|
chars |= set(m.group(1))
|
|
# Also handle multi-line msgstr
|
|
in_msgstr = False
|
|
for line in text.splitlines():
|
|
if line.startswith('msgstr "') or (in_msgstr and line.startswith('"')):
|
|
in_msgstr = True
|
|
content = line.removeprefix('msgstr "').removesuffix('"') if line.startswith('msgstr "') else line.strip('"')
|
|
content = content.replace('\\\\n', '\n').replace('\\n', '\n').replace('\\"', '"')
|
|
chars |= set(content)
|
|
else:
|
|
in_msgstr = False
|
|
return chars
|
|
|
|
|
|
def _char_sets():
|
|
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
|
|
labels = set(base)
|
|
per_lang: dict[str, tuple[int, ...]] = {}
|
|
|
|
for language, code in _languages().items():
|
|
labels.update(language)
|
|
po_path = TRANSLATIONS_DIR / f"app_{code}.po"
|
|
try:
|
|
chars = set(po_path.read_text(encoding="utf-8"))
|
|
except FileNotFoundError:
|
|
continue
|
|
chars |= _dragonpilot_chars(code)
|
|
if code in UNIFONT_LANGUAGES:
|
|
lang_chars = set(base) | chars
|
|
per_lang[code] = tuple(sorted(ord(c) for c in lang_chars))
|
|
else:
|
|
base.update(chars)
|
|
|
|
base_cp = tuple(sorted(ord(c) for c in base))
|
|
labels_cp = tuple(sorted(ord(c) for c in labels))
|
|
return base_cp, labels_cp, per_lang
|
|
|
|
|
|
def _glyph_metrics(glyphs, rects, glyph_count: int):
|
|
entries = []
|
|
min_offset_y, max_extent = None, 0
|
|
for idx in range(glyph_count):
|
|
glyph = glyphs[idx]
|
|
rect = rects[idx]
|
|
width = int(round(rect.width))
|
|
height = int(round(rect.height))
|
|
offset_y = int(round(glyph.offsetY))
|
|
min_offset_y = offset_y if min_offset_y is None else min(min_offset_y, offset_y)
|
|
max_extent = max(max_extent, offset_y + height)
|
|
entries.append({
|
|
"id": glyph.value,
|
|
"x": int(round(rect.x)),
|
|
"y": int(round(rect.y)),
|
|
"width": width,
|
|
"height": height,
|
|
"xoffset": int(round(glyph.offsetX)),
|
|
"yoffset": offset_y,
|
|
"xadvance": int(round(glyph.advanceX)),
|
|
})
|
|
|
|
if min_offset_y is None:
|
|
raise RuntimeError("No glyphs were generated")
|
|
|
|
line_height = int(round(max_extent - min_offset_y))
|
|
base = int(round(max_extent))
|
|
return entries, line_height, base
|
|
|
|
|
|
def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_height: int, base: int, atlas_size, entries):
|
|
# TODO: why doesn't raylib calculate these metrics correctly?
|
|
if line_height != font_size:
|
|
print("using font size for line height", atlas_name)
|
|
line_height = font_size
|
|
lines = [
|
|
f"info face=\"{face}\" size=-{font_size} bold=0 italic=0 charset=\"\" unicode=1 stretchH=100 smooth=0 aa=1 padding=0,0,0,0 spacing=0,0 outline=0",
|
|
f"common lineHeight={line_height} base={base} scaleW={atlas_size[0]} scaleH={atlas_size[1]} pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4",
|
|
f"page id=0 file=\"{atlas_name}\"",
|
|
f"chars count={len(entries)}",
|
|
]
|
|
for entry in entries:
|
|
lines.append(
|
|
("char id={id:<4} x={x:<5} y={y:<5} width={width:<5} height={height:<5} " +
|
|
"xoffset={xoffset:<5} yoffset={yoffset:<5} xadvance={xadvance:<5} page=0 chnl=15").format(**entry)
|
|
)
|
|
path.write_text("\n".join(lines) + "\n")
|
|
|
|
|
|
def _init_window():
|
|
"""Initialize raylib window (required for font rendering with GPU)."""
|
|
# Reuse the lib loaded once
|
|
if not hasattr(_init_window, '_done'):
|
|
lib = rl.ffi.dlopen(None)
|
|
lib.InitWindow(64, 64, b'fontgen')
|
|
lib.SetWindowState(0x00000080) # FLAG_WINDOW_HIDDEN
|
|
lib.SetTargetFPS(0)
|
|
_init_window._lib = lib
|
|
_init_window._done = True
|
|
return _init_window._lib
|
|
|
|
|
|
def _process_font(font_path: Path, codepoints: tuple[int, ...], output_name: str | None = None):
|
|
stem = output_name or font_path.stem
|
|
font_size = 48 if font_path.stem.lower().startswith("opfont") else 200
|
|
print(f"Processing {font_path.name} -> {stem} ({len(codepoints)} glyphs @ {font_size}px)...")
|
|
|
|
lib = _init_window()
|
|
|
|
load_font_ex = rl.ffi.cast('struct Font(*)(const char *, int, int *, int)', lib.LoadFontEx)
|
|
gen_atlas = rl.ffi.cast('struct Image(*)(struct GlyphInfo *, struct Rectangle **, int, int, int, int)', lib.GenImageFontAtlas)
|
|
export_img = rl.ffi.cast('bool(*)(struct Image, const char *)', lib.ExportImage)
|
|
unload_font = rl.ffi.cast('void(*)(struct Font)', lib.UnloadFont)
|
|
|
|
fnt_path = font_path.as_posix()
|
|
cp_buffer = rl.ffi.new("int[]", codepoints)
|
|
glyph_count = len(codepoints)
|
|
font = load_font_ex(fnt_path.encode('utf-8'), font_size, cp_buffer, glyph_count)
|
|
if font.glyphCount == 0:
|
|
raise RuntimeError("raylib failed to load font data")
|
|
|
|
rects_ptr = rl.ffi.new("struct Rectangle **")
|
|
image = gen_atlas(font.glyphs, rects_ptr, font.glyphCount, font_size, GLYPH_PADDING, 0)
|
|
if image.width == 0 or image.height == 0:
|
|
unload_font(font)
|
|
raise RuntimeError("raylib returned an empty atlas")
|
|
|
|
rects = rects_ptr[0]
|
|
atlas_name = f"{stem}.png"
|
|
atlas_path = FONT_DIR / atlas_name
|
|
entries, line_height, base = _glyph_metrics(font.glyphs, rects, font.glyphCount)
|
|
|
|
if not export_img(image, atlas_path.as_posix().encode('utf-8')):
|
|
unload_font(font)
|
|
raise RuntimeError("Failed to export atlas image")
|
|
|
|
_write_bmfont(FONT_DIR / f"{stem}.fnt", font_size, stem, atlas_name, line_height, base, (image.width, image.height), entries)
|
|
unload_font(font)
|
|
|
|
|
|
def main():
|
|
base_cp, labels_cp, per_lang = _char_sets()
|
|
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
|
|
opfonts: list[Path] = []
|
|
|
|
for font in fonts:
|
|
if "emoji" in font.name.lower() or font.name == "unifont.otf":
|
|
continue
|
|
if font.stem.lower().startswith("opfont"):
|
|
opfonts.append(font)
|
|
continue
|
|
_process_font(font, base_cp)
|
|
|
|
if not opfonts:
|
|
raise RuntimeError("OpFont not found (expected OpFont-*.otf in fonts dir)")
|
|
|
|
for opfont_path in opfonts:
|
|
weight = opfont_path.stem # e.g. "OpFont-Regular"
|
|
|
|
# Labels atlas: language display names + ASCII (for language selector)
|
|
_process_font(opfont_path, labels_cp, output_name=f"{weight}-Labels")
|
|
|
|
# Per-language atlases: ASCII + that language's .po chars
|
|
for lang_code, lang_cp in per_lang.items():
|
|
_process_font(opfont_path, lang_cp, output_name=f"{weight}-{lang_code}")
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|