mirror of
https://gitlvb.teallvbs.xyz/IQ.Lvbs/IQ.Pilot.git
synced 2026-07-21 19:42:13 +08:00
197 lines
7.3 KiB
Python
Executable File
197 lines
7.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import os
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
import json
|
|
|
|
|
|
@contextmanager
|
|
def _quiet_fds(*fds):
|
|
# mute the raylib load banner (printed on first native use, before any log level applies)
|
|
saved = [os.dup(fd) for fd in fds]
|
|
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
try:
|
|
for fd in fds:
|
|
os.dup2(devnull, fd)
|
|
yield
|
|
finally:
|
|
for fd, s in zip(fds, saved):
|
|
os.dup2(s, fd)
|
|
os.close(s)
|
|
os.close(devnull)
|
|
|
|
|
|
with _quiet_fds(1, 2):
|
|
import pyray as rl
|
|
rl.set_trace_log_level(rl.TraceLogLevel.LOG_ERROR) # first native call: triggers the banner
|
|
|
|
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 = {"ar", "th", "zh-CHT", "zh-CHS", "ko", "ja"}
|
|
|
|
|
|
def _load_font_data_arity() -> int:
|
|
# The C signature of LoadFontData differs between raylib builds; the pyray wrapper is a
|
|
# *args shim, so ask cffi for the real function type instead of catching arity errors.
|
|
import raylib as _raylib
|
|
return len(_raylib.ffi.typeof(_raylib.rl.LoadFontData).args)
|
|
|
|
|
|
def _languages():
|
|
if not LANGUAGES_FILE.exists():
|
|
return {}
|
|
with LANGUAGES_FILE.open(encoding="utf-8") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def _char_sets():
|
|
base = set(map(chr, range(32, 127))) | set(EXTRA_CHARS)
|
|
unifont = set(base)
|
|
|
|
for language, code in _languages().items():
|
|
unifont.update(language)
|
|
po_path = TRANSLATIONS_DIR / f"app_{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)
|
|
|
|
return tuple(sorted(ord(c) for c in base)), tuple(sorted(ord(c) for c in unifont))
|
|
|
|
|
|
def _glyph_metrics(glyphs, rects, glyph_count):
|
|
entries = []
|
|
min_offset_y, max_extent = None, 0
|
|
for idx in range(glyph_count):
|
|
glyph = glyphs[idx]
|
|
rect = rects[idx]
|
|
# Take the codepoint from the glyph itself: the 7-arg LoadFontData returns a
|
|
# compacted array of only the glyphs it found, so positional pairing with the
|
|
# requested codepoint list would misattribute (and previously overran) entries.
|
|
codepoint = glyph.value
|
|
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": codepoint,
|
|
"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:
|
|
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 _looks_like_lfs_pointer(data: bytes) -> bool:
|
|
head = data[:256]
|
|
return b"git-lfs.github.com/spec/v1" in head and b"oid sha256:" in head
|
|
|
|
|
|
def _can_reuse_prebuilt(font_path: Path) -> bool:
|
|
return (FONT_DIR / f"{font_path.stem}.fnt").exists() and (FONT_DIR / f"{font_path.stem}.png").exists()
|
|
|
|
|
|
def _process_font(font_path: Path, codepoints: tuple[int, ...]):
|
|
font_size = {
|
|
"unifont.otf": 16, # unifont is only 16x8 or 16x16 pixels per glyph
|
|
}.get(font_path.name, 200)
|
|
|
|
data = font_path.read_bytes()
|
|
file_buf = rl.ffi.new("unsigned char[]", data)
|
|
cp_buffer = rl.ffi.new("int[]", codepoints)
|
|
cp_ptr = rl.ffi.cast("int *", cp_buffer)
|
|
args = [rl.ffi.cast("unsigned char *", file_buf), len(data), font_size, cp_ptr, len(codepoints), rl.FontType.FONT_DEFAULT]
|
|
glyph_count_ptr = None
|
|
if _load_font_data_arity() == 7:
|
|
# IQ.OS >= 4.1 wheel: LoadFontData grew an int *glyphCount out-param AND returns
|
|
# only the glyphs it actually loaded (fewer than codepointCount when some are
|
|
# missing). We MUST read that count — using len(codepoints) below would index
|
|
# past the glyph/rect arrays and segfault. The PC pypi wheel (raylib <= 5.5) is
|
|
# the 6-arg version that returns codepointCount glyphs.
|
|
glyph_count_ptr = rl.ffi.new("int *", 0)
|
|
args.append(glyph_count_ptr)
|
|
glyphs = rl.load_font_data(*args)
|
|
if glyphs == rl.ffi.NULL:
|
|
if _can_reuse_prebuilt(font_path):
|
|
reason = "raylib failed to load font data"
|
|
if _looks_like_lfs_pointer(data):
|
|
reason += " (file looks like a Git LFS pointer, not real font data)"
|
|
print(f"WARNING: {reason}; reusing prebuilt atlas for {font_path.name}")
|
|
return
|
|
reason = f"raylib failed to load font data for {font_path.name}"
|
|
if _looks_like_lfs_pointer(data):
|
|
reason += " (file looks like a Git LFS pointer, not real font data)"
|
|
raise RuntimeError(reason)
|
|
|
|
# Actual number of glyphs in the returned array (out-param on 7-arg raylib, else
|
|
# codepointCount). Everything downstream must use this, not len(codepoints).
|
|
glyph_count = glyph_count_ptr[0] if glyph_count_ptr is not None else len(codepoints)
|
|
|
|
rects_ptr = rl.ffi.new("Rectangle **")
|
|
image = rl.gen_image_font_atlas(glyphs, rects_ptr, glyph_count, font_size, GLYPH_PADDING, 0)
|
|
if image.width == 0 or image.height == 0:
|
|
if _can_reuse_prebuilt(font_path):
|
|
print(f"WARNING: raylib returned an empty atlas; reusing prebuilt atlas for {font_path.name}")
|
|
return
|
|
raise RuntimeError(f"raylib returned an empty atlas for {font_path.name}")
|
|
|
|
rects = rects_ptr[0]
|
|
atlas_name = f"{font_path.stem}.png"
|
|
atlas_path = FONT_DIR / atlas_name
|
|
entries, line_height, base = _glyph_metrics(glyphs, rects, glyph_count)
|
|
|
|
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)
|
|
|
|
|
|
def main():
|
|
base_cp, unifont_cp = _char_sets()
|
|
fonts = sorted(FONT_DIR.glob("*.ttf")) + sorted(FONT_DIR.glob("*.otf"))
|
|
for font in fonts:
|
|
if "emoji" in font.name.lower():
|
|
continue
|
|
glyphs = unifont_cp if font.stem.lower().startswith("unifont") else base_cp
|
|
_process_font(font, glyphs)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|