#!/usr/bin/env python3 # gen_ui_font.py — 预渲染 MeowOS UI 位图字库(替代 freetype,零依赖) # # 输入默认字体(本脚本用设备自带 default.otf,因为 ~/Fonts 属主为 10195 不可读; # 如需切换思源黑体/MiSans,改 SRC 路径并把字体拷到可读处,或经 meow.conf font_file= 指定)。 # 输出 ui_font.dat:自定义二进制位图字库(8-bit alpha 灰度,白字用 alpha,blit 时着色)。 # 布局(小端): # +0 uint32 magic "UIF1" # +4 uint16 px (设计字号,用于行距/参考) # +6 uint16 flags (0 = 8-bit alpha 灰度) # +8 uint32 n (glyph 数) # +12 uint16 atlas_w (位图集宽) # +14 uint16 atlas_h # +16 16*n entry: uint32 cp; uint16 x,y,w,h,adv # +16+16*n bitmap: atlas_w*atlas_h 字节(8-bit alpha) # # 字符集:ASCII 0x20-0x7E + GB2312 一级汉字(0xB0-0xD7) + 常用 CJK 标点 + UI 显式字符串。 import struct, sys from PIL import Image, ImageDraw, ImageFont SRC = "fw_ssh/squashfs-root/usr/resource/fonts/default.otf" PX = 24 # 设计像素高(大字,配合 40px 状态栏) OUT = sys.argv[1] if len(sys.argv) > 1 else "/tmp/ftg/ui_font.dat" # ---- 字符集 ---- chars = set() for c in range(0x20, 0x7F): chars.add(chr(c)) # ASCII 可打印 # GB2312 一级汉字: 0xB0A1(176,161) .. 0xD7F9(215,249),每行 94 for row in range(0xB0, 0xD8): for col in range(0xA1, 0xFF): b = bytes([row, col]) try: ch = b.decode('gb2312') except Exception: continue if ch and not ch.startswith(' '): # 去掉 PUA/未定义区 if ord(ch[0]) >= 0x4E00 or (0xFF00 <= ord(ch[0]) <= 0xFFEF): chars.add(ch[0]) # 常用 CJK 标点 / 全角 for ch in ",。、!?:;()《》【】「」『』—…¥%·×÷=+-·  〇": chars.add(ch) # UI 显式中文(电源菜单/状态栏/提示),确保必有 for ch in "关机重启回到取消电源菜单音量蓝牙电量充电返回播放器需网络设置我你的好中英文数分秒年月日应用没有存储卡请插入后开吃到找到放缺失导航打触摸选择无法启动以确认上下": chars.add(ch) chars = sorted(chars) # 宽字符(CJK)用 PX 高,ASCII 用较小一点以保持基线一致;Pillow 自带 advance font = ImageFont.truetype(SRC, PX) # ---- 栅格化并打包进 atlas(行优先)---- # 先用足够大的空白画布测量每个字符 bitmap,再打包 tmp_img_cache = {} maxw = PX * 2 packed = [] # (char, bbox_img, w, h, adv) scale = 1 # 计算每个字符的 bitmap(grayscale alpha) metas = [] for ch in chars: if ch == ' ': # 空格无 bitmap,只记 advance adv = font.getlength(' ') metas.append((ch, None, 0, 0, int(adv))) continue try: bbox = font.getbbox(ch) except Exception: bbox = None if not bbox: adv = int(font.getlength(ch)) metas.append((ch, None, 0, 0, adv)) continue x0, y0, x1, y1 = bbox w = max(1, x1 - x0); h = max(1, y1 - y0) # 渲染到 grayscale (alpha=灰度) im = Image.new('L', (w, h), 0) # 用 getmask 更精确;这里直接画到临时图再裁 mask = font.getmask(ch, mode='L') im = Image.frombytes('L', mask.size, bytes(mask)) # 裁到 bbox 对应的部分 if 0 <= y0 < im.height and 0 <= x0 < im.width: im = im.crop((max(0, x0), max(0, y0), min(im.width, x1), min(im.height, y1))) adv = int(font.getlength(ch)) metas.append((ch, im, im.width, im.height, adv)) # 打包:放入 atlas(简单行打包,一行放满再换行) atlas_w = 512 atlas_rows = [] x = 0; y = 0; maxY = 0 entry_pos = {} # char -> (x,y,w,h,adv) # 先按宽高打包 pos_list = [] curx = 0; cury = 0; rowh = 0 entries = [] for ch, im, w, h, adv in metas: if im is None: entries.append((ch, 0, 0, 0, 0, adv)); continue if curx + w > atlas_w: curx = 0; cury += rowh; rowh = 0 entry_pos[ch] = (curx, cury, w, h) entries.append((ch, curx, cury, w, h, adv)) curx += w + 1 rowh = max(rowh, h) if cury + rowh > 4096: raise RuntimeError("atlas too tall") atlas_h = cury + rowh atlas = bytearray(atlas_w * atlas_h) for ch, im, w, h, adv in metas: if im is None: continue x, y, w, h = entry_pos[ch][0], entry_pos[ch][1], entry_pos[ch][2], entry_pos[ch][3] for yy in range(h): for xx in range(w): v = im.getpixel((xx, yy)) atlas[(y + yy) * atlas_w + (x + xx)] = v # ---- 写文件 ---- with open(OUT, 'wb') as f: f.write(struct.pack('