better looking fonts

This commit is contained in:
Rick Lan
2026-02-11 20:14:06 +08:00
parent 542a25ca7b
commit baa80ac90d
7 changed files with 153 additions and 36 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+64 -22
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
from pathlib import Path
import gettext
import json
import pyray as rl
@@ -21,21 +22,46 @@ def _languages():
return json.load(f)
def _dragonpilot_chars(code: str) -> set[str]:
"""Characters used by dragonpilot's own translations (dragonpilot_{code}.mo).
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 '?' (the catalog ships compiled, so we read the .mo, not a .po source)."""
mo_path = TRANSLATIONS_DIR / f"dragonpilot_{code}.mo"
if not mo_path.exists():
return set()
with mo_path.open("rb") as fh:
catalog = gettext.GNUTranslations(fh)._catalog
chars: set[str] = set()
for value in catalog.values():
if isinstance(value, str):
chars |= set(value)
return chars
def _char_sets():
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
unifont = set(base)
labels = set(base)
per_lang: dict[str, tuple[int, ...]] = {}
for language, code in _languages().items():
unifont.update(language)
for prefix in ("app_", "dragonpilot_"):
po_path = TRANSLATIONS_DIR / f"{prefix}{code}.po"
try:
chars = set(po_path.read_text(encoding="utf-8"))
except FileNotFoundError:
continue
(unifont if code in UNIFONT_LANGUAGES else base).update(chars)
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)
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
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, codepoints):
@@ -87,12 +113,10 @@ def _write_bmfont(path: Path, font_size: int, face: str, atlas_name: str, line_h
path.write_text("\n".join(lines) + "\n")
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
print(f"Processing {font_path.name}...")
font_size = {
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
}.get(font_path.name, 200)
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)...")
data = font_path.read_bytes()
file_buf = rl.ffi.new("unsigned char[]", data)
@@ -108,24 +132,42 @@ def _process_font(font_path: Path, codepoints: tuple[int, ...]):
raise RuntimeError("raylib returned an empty atlas")
rects = rects_ptr[0]
atlas_name = f"{font_path.stem}.png"
atlas_name = f"{stem}.png"
atlas_path = FONT_DIR / atlas_name
entries, line_height, base = _glyph_metrics(glyphs, rects, codepoints)
if not rl.export_image(image, atlas_path.as_posix()):
raise RuntimeError("Failed to export atlas image")
_write_bmfont(FONT_DIR / f"{font_path.stem}.fnt", font_size, font_path.stem, atlas_name, line_height, base, (image.width, image.height), entries)
_write_bmfont(FONT_DIR / f"{stem}.fnt", font_size, stem, atlas_name, line_height, base, (image.width, image.height), entries)
def main():
base_cp, unifont_cp = _char_sets()
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():
if "emoji" in font.name.lower() or font.name == "unifont.otf":
continue
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
_process_font(font, glyphs)
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
+1
View File
@@ -95,6 +95,7 @@ class DeviceLayout(Widget):
if result == DialogResult.CONFIRM and self._select_language_dialog:
selected_language = multilang.languages[self._select_language_dialog.selection]
multilang.change_language(selected_language)
gui_app.on_language_changed(selected_language)
self._update_calib_description()
self._select_language_dialog = None
+88 -14
View File
@@ -99,19 +99,45 @@ class FontWeight(StrEnum):
MEDIUM = "Inter-Medium.fnt"
BOLD = "Inter-Bold.fnt"
SEMI_BOLD = "Inter-SemiBold.fnt"
UNIFONT = "unifont.fnt"
UNIFONT = "OpFont-Regular-Labels.fnt"
# Small UI fonts
DISPLAY_REGULAR = "Inter-Regular.fnt"
ROMAN = "Inter-Regular.fnt"
DISPLAY = "Inter-Bold.fnt"
_OPFONT_WEIGHT = {
"Inter-Light.fnt": "Regular",
"Inter-Regular.fnt": "Regular",
"Inter-Medium.fnt": "Medium",
"Inter-SemiBold.fnt": "SemiBold",
"Inter-Bold.fnt": "Bold",
}
def _opfont_filename(inter_filename: str, lang_code: str) -> str:
"""Map an Inter font filename to the equivalent OpFont filename for a language."""
weight_name = _OPFONT_WEIGHT.get(inter_filename, "Regular")
return f"OpFont-{weight_name}-{lang_code}.fnt"
def font_fallback(font: rl.Font) -> rl.Font:
"""Fall back to unifont for languages that require it."""
if multilang.requires_unifont():
return gui_app.font(FontWeight.UNIFONT)
return font
"""Ensure the font is from the current language's font set.
Widgets may cache rl.Font references. After a language switch, those references
are stale (freed GPU texture). This catches them and returns the current equivalent.
"""
if not gui_app._font_remap:
return font # no language switch has occurred
# Check if this is a currently loaded font (handles texture ID reuse)
for f in gui_app._fonts.values():
if font.texture.id == f.texture.id:
return f
# Stale reference — look up the original weight and return current font for it
weight = gui_app._font_remap.get(font.texture.id)
if weight:
return gui_app.font(weight)
return gui_app.font(FontWeight.NORMAL)
class MousePos(NamedTuple):
@@ -196,6 +222,8 @@ class GuiApplication:
self._fonts: dict[FontWeight, rl.Font] = {}
self._width = width if width is not None else GuiApplication._default_width()
self._height = height if height is not None else GuiApplication._default_height()
self._active_lang_code: str = ""
self._font_remap: dict[int, FontWeight] = {} # old texture ID → weight (for stale references)
if PC and os.getenv("SCALE") is None:
self._scale = self._calculate_auto_scale()
@@ -531,6 +559,7 @@ class GuiApplication:
for font in self._fonts.values():
rl.unload_font(font)
self._fonts = {}
self._active_lang_code = ""
if self._render_texture is not None:
rl.unload_render_texture(self._render_texture)
@@ -640,6 +669,21 @@ class GuiApplication:
pass
def font(self, font_weight: FontWeight = FontWeight.NORMAL) -> rl.Font:
if font_weight not in self._fonts:
# For languages need unifont, load OpFont instead of Inter (except labels font)
if multilang.requires_unifont() and font_weight != FontWeight.UNIFONT:
filename = _opfont_filename(font_weight.value, self._active_lang_code)
else:
filename = font_weight.value
with as_file(FONT_DIR) as fspath:
fnt_path = fspath / filename
# Fall back to Regular weight if requested weight doesn't exist
if not fnt_path.exists() and multilang.requires_unifont():
filename = f"OpFont-Regular-{self._active_lang_code}.fnt"
fnt_path = fspath / filename
font = rl.load_font(fnt_path.as_posix())
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
self._fonts[font_weight] = font
return self._fonts[font_weight]
@property
@@ -651,15 +695,45 @@ class GuiApplication:
return self._height
def _load_fonts(self):
for font_weight_file in FontWeight:
with as_file(FONT_DIR) as fspath:
fnt_path = fspath / font_weight_file
font = rl.load_font(fnt_path.as_posix())
if font_weight_file != FontWeight.UNIFONT:
rl.gen_texture_mipmaps(font.texture)
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_TRILINEAR)
self._fonts[font_weight_file] = font
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
# for font_weight_file in FontWeight:
# with as_file(FONT_DIR) as fspath:
# fnt_path = fspath / font_weight_file
# font = rl.load_font(fnt_path.as_posix())
# if font_weight_file != FontWeight.UNIFONT:
# rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
# self._fonts[font_weight_file] = font
# rl.gui_set_font(self._fonts[FontWeight.NORMAL])
# dp - lazy, language-aware font loading. Set the active language then load
# the NORMAL weight via font(), which picks OpFont (CJK-capable) over Inter
# when the current language requires unifont. Loading all weights eagerly
# here (upstream behavior) would bypass the OpFont remapping in font() and
# render tofu boxes for non-Latin languages at startup.
#
# Original upstream implementation (intentionally replaced — do not restore
# on rebase; it pre-fills self._fonts with Inter and defeats font()'s OpFont
# selection):
self._active_lang_code = multilang.language
rl.gui_set_font(self.font(FontWeight.NORMAL))
def on_language_changed(self, lang_code: str):
# Map old texture IDs → weights so we can remap stale references
old_weight_by_texture = {f.texture.id: w for w, f in self._fonts.items()}
old_fonts = list(self._fonts.values())
self._fonts = {}
self._active_lang_code = lang_code
# Load new fonts for all weights that were active
for weight in old_weight_by_texture.values():
self.font(weight)
# Carry forward existing remap + add new entries (weights are stable across switches)
self._font_remap = dict(self._font_remap) | {tid: w for tid, w in old_weight_by_texture.items()}
# Now safe to unload old fonts
for f in old_fonts:
rl.unload_font(f)
rl.gui_set_font(self.font(FontWeight.NORMAL))
from openpilot.system.ui.lib import text_measure, wrap_text
text_measure._cache.clear()
wrap_text._cache.clear()
def _set_styles(self):
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0)