MeowOS for HiBy R3 Pro II — 公共发布版本(历史已重置)
• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC) • 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库) • 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo 增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
This commit is contained in:
commit
245bee2946
136 files changed
+32730
No files matched your search
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
gen_font.py — MeowOS 预渲染字库生成器(主机侧)· v2:像素进 PNG、索引进 idx。
|
||||
|
||||
产出(单份存储,设备与主机共用同一套字节):
|
||||
<out>/font.png 灰度 PNG(bit depth 4|8),含全部字号的面,每面一个子矩形
|
||||
<out>/font.idx 二进制索引:头部 + 每面元数据 + 每字符 16B 记录
|
||||
|
||||
设计要点:
|
||||
* 像素只存一次(font.png);idx 只做索引。设备端用 zlib inflate + 自写 PNG 反滤波解码,
|
||||
无需 libpng(目标根文件系统无 libpng,仅有 zlib)。
|
||||
* 位深默认 4(16 级 alpha,仍抗锯齿)以把多张高密度汉字面压到 ≤2MB 预算内;8-bit 会超。
|
||||
--bitdepth 8 可出 8-bit(建议单字号)。
|
||||
* 绘制模型:drawtext(fb,x,y,str,color,size),(x,y)=行首「升线顶」锚点;baseline=y+ascent;
|
||||
每字形 blit 于 (x+pen+dx, y+dy),pen 累加 advance。
|
||||
|
||||
idx 二进制布局(little-endian, version=2):
|
||||
Header(16B):
|
||||
+0 u8[4] magic='MFNI'
|
||||
+4 u16 version=2
|
||||
+6 u8 bit_depth (4|8)
|
||||
+7 u8 face_count
|
||||
+8 u32 png_w
|
||||
+12 u32 png_h
|
||||
FaceHeader ×face_count (20B each):
|
||||
+0 u16 px_size
|
||||
+2 u16 plane_x
|
||||
+4 u16 plane_y (PNG 像素内面左上角)
|
||||
+6 u16 plane_w
|
||||
+8 u16 plane_h
|
||||
+10 i16 ascent
|
||||
+12 i16 descent (负值)
|
||||
+14 u16 glyph_count
|
||||
+16 u32 glyph_offset (文件内本面 glyph 表偏移)
|
||||
Glyph 表(每面按 codepoint 升序,16B each):
|
||||
+0 u32 codepoint
|
||||
+4 u16 x (PNG 像素左上角,已含 offset)
|
||||
+6 u16 y
|
||||
+8 u16 w
|
||||
+10 u16 h
|
||||
+12 u16 advance
|
||||
+14 i8 dx
|
||||
+15 u8 dy
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
MAGIC = b"MFNI"
|
||||
VER = 2
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# PNG 编码(灰度, bit depth 4|8, 全部扫描行 filter=0;规范解码器可读)
|
||||
# --------------------------------------------------------------------------
|
||||
def _png_chunk(ctype, data):
|
||||
c = ctype + data
|
||||
return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def gray_png_encode(width, height, depth, packed):
|
||||
rowbytes = (width * depth + 7) // 8
|
||||
ihdr = struct.pack(">IIBBBBB", width, height, depth, 0, 0, 0, 0) # gray, compression0 filter0 interlace0
|
||||
raw = bytearray()
|
||||
for y in range(height):
|
||||
raw.append(0) # filter type 0 (None)
|
||||
raw += packed[y * rowbytes:(y + 1) * rowbytes]
|
||||
idat = zlib.compress(bytes(raw), 9)
|
||||
out = b"\x89PNG\r\n\x1a\n"
|
||||
out += _png_chunk(b"IHDR", ihdr)
|
||||
out += _png_chunk(b"IDAT", idat)
|
||||
out += _png_chunk(b"IEND", b"")
|
||||
return out
|
||||
|
||||
|
||||
def pack_plane_depth(plane_l8, width, height, depth):
|
||||
"""把 L8 行主序 plane 转为指定 bit depth 的打包字节。"""
|
||||
if depth == 8:
|
||||
return bytes(plane_l8)
|
||||
assert depth == 4
|
||||
rowbytes = (width + 1) // 2
|
||||
out = bytearray()
|
||||
for y in range(height):
|
||||
base = y * width
|
||||
row = bytearray(rowbytes)
|
||||
for x in range(0, width, 2):
|
||||
a = plane_l8[base + x]
|
||||
b = plane_l8[base + x + 1] if x + 1 < width else 0
|
||||
row[x // 2] = ((a >> 4) << 4) | (b >> 4)
|
||||
out += row
|
||||
return bytes(out)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 字符集
|
||||
# --------------------------------------------------------------------------
|
||||
def build_charset(extra=""):
|
||||
cps = []
|
||||
def add(cp):
|
||||
if cp not in cps:
|
||||
cps.append(cp)
|
||||
for c in range(0x20, 0x7F):
|
||||
add(c)
|
||||
for hi in range(0xB0, 0xD8):
|
||||
for lo in range(0xA1, 0xFF):
|
||||
try:
|
||||
add(ord(bytes([hi, lo]).decode("gb2312")))
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
for hi in range(0xA1, 0xA4):
|
||||
for lo in range(0xA1, 0xFF):
|
||||
try:
|
||||
add(ord(bytes([hi, lo]).decode("gb2312")))
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
for ch in extra:
|
||||
add(ord(ch))
|
||||
return sorted(cps)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 逐字形渲染
|
||||
# --------------------------------------------------------------------------
|
||||
class Glyph:
|
||||
__slots__ = ("cp", "w", "h", "dx", "dy", "adv", "pixels")
|
||||
|
||||
def __init__(self, cp, w, h, dx, dy, adv, pixels):
|
||||
self.cp, self.w, self.h, self.dx = cp, w, h, dx
|
||||
self.dy, self.adv, self.pixels = dy, adv, pixels
|
||||
|
||||
|
||||
def render_one(font, ch, pad):
|
||||
asc, desc = font.getmetrics()
|
||||
# Pillow: ascent & descent 均为正; 字体行高 = asc + |desc|(desc 或为负,用 abs 归一)
|
||||
line_h = asc + abs(desc)
|
||||
Wc = int(font.getlength(ch)) + 2 * pad + 16
|
||||
Hc = line_h + 2 * pad
|
||||
img = Image.new("L", (Wc, Hc), 0)
|
||||
ImageDraw.Draw(img).text((pad, pad), ch, font=font, fill=255)
|
||||
bb = img.getbbox()
|
||||
if bb is None:
|
||||
return Glyph(ord(ch), 0, 0, 0, 0, int(round(font.getlength(ch))), None)
|
||||
l, t, r, b = bb
|
||||
return Glyph(ord(ch), r - l, b - t, l - pad, t - pad,
|
||||
int(round(font.getlength(ch))), img.crop((l, t, r, b)).tobytes())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 打包(decreasing-height 书架式;枚举多个最大宽求最小面积)
|
||||
# --------------------------------------------------------------------------
|
||||
def pack_best(glyphs, gap=1):
|
||||
best = None
|
||||
for max_w in (256, 384, 512, 640, 768, 1024, 1280, 1536, 2048, 3072):
|
||||
items = sorted(enumerate(glyphs), key=lambda t: -t[1].h)
|
||||
placements = {}
|
||||
x = y = 0
|
||||
shelf_h = 0
|
||||
cur_w = 0
|
||||
for idx, g in items:
|
||||
if g.w == 0 or g.h == 0:
|
||||
continue
|
||||
if cur_w + g.w > max_w:
|
||||
y += shelf_h + gap
|
||||
x = 0
|
||||
cur_w = 0
|
||||
shelf_h = 0
|
||||
placements[idx] = (x, y)
|
||||
x += g.w + gap
|
||||
cur_w += g.w + gap
|
||||
if g.h > shelf_h:
|
||||
shelf_h = g.h
|
||||
if not placements:
|
||||
continue
|
||||
aw = max(placements[i][0] + g.w for i, g in enumerate(glyphs) if i in placements)
|
||||
ah = y + shelf_h
|
||||
area = aw * ah
|
||||
if best is None or area < best[0]:
|
||||
best = (area, placements, aw, ah, max_w)
|
||||
if best is None:
|
||||
return {}, 0, 0, 0
|
||||
return best[1], best[2], best[3], best[4]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# idx 写入
|
||||
# --------------------------------------------------------------------------
|
||||
def write_idx(path, bit_depth, faces, glyph_tabs):
|
||||
n = len(faces)
|
||||
png_w = max(f["plane_x"] + f["plane_w"] for f in faces)
|
||||
png_h = max(f["plane_y"] + f["plane_h"] for f in faces)
|
||||
header = bytearray(struct.pack("<4sHBBII", MAGIC, VER, bit_depth, n, png_w, png_h))
|
||||
assert len(header) == 16
|
||||
|
||||
face_off = 16
|
||||
glyph_off = face_off + 20 * n
|
||||
body = bytearray()
|
||||
for i, f in enumerate(faces):
|
||||
body += struct.pack("<HHHHHhhHI", f["px"], f["plane_x"], f["plane_y"],
|
||||
f["plane_w"], f["plane_h"], f["ascent"], f["descent"],
|
||||
len(glyph_tabs[i]), 0)
|
||||
|
||||
cur = glyph_off
|
||||
tables = []
|
||||
offs = []
|
||||
for tab in glyph_tabs:
|
||||
offs.append(cur)
|
||||
tab_sorted = sorted(tab, key=lambda g: g[0])
|
||||
buf = bytearray()
|
||||
for cp, x, y, w, h, adv, dx, dy in tab_sorted:
|
||||
buf += struct.pack("<IHHHHHbB", cp, x, y, w, h, adv, dx, dy)
|
||||
tables.append(buf)
|
||||
cur += len(buf)
|
||||
|
||||
for i, off in enumerate(offs):
|
||||
struct.pack_into("<I", body, i * 20 + 16, off)
|
||||
for buf in tables:
|
||||
body += buf
|
||||
|
||||
with open(path, "wb") as fp:
|
||||
fp.write(bytes(header))
|
||||
fp.write(bytes(body))
|
||||
return len(header) + len(body)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 主流程
|
||||
# --------------------------------------------------------------------------
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="MeowOS 预渲染字库生成器 v2")
|
||||
ap.add_argument("--font", required=True)
|
||||
ap.add_argument("--sizes", default="28,20")
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--name", default="font")
|
||||
ap.add_argument("--bitdepth", type=int, default=4, choices=(4, 8))
|
||||
ap.add_argument("--extra", default="")
|
||||
args = ap.parse_args()
|
||||
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
sizes = [int(s) for s in args.sizes.split(",") if s.strip()]
|
||||
cps = build_charset(args.extra)
|
||||
print(f"字符集: {len(cps)} 码位")
|
||||
|
||||
all_planes = [] # (aw, ah, plane_l8 bytes, tab)
|
||||
max_w = 0
|
||||
for size in sizes:
|
||||
print(f"\n=== 字号 {size}px ===")
|
||||
font = ImageFont.truetype(args.font, size)
|
||||
asc, desc = font.getmetrics()
|
||||
print(f" metrics ascent={asc} descent={desc}")
|
||||
|
||||
glyphs = []
|
||||
miss = []
|
||||
for cp in cps:
|
||||
ch = chr(cp)
|
||||
g = render_one(font, ch, pad=8)
|
||||
if g.w == 0 and g.h == 0 and cp != 0x20:
|
||||
miss.append(ch)
|
||||
glyphs.append(g)
|
||||
if miss:
|
||||
print(f" [warn] {len(miss)} 码位无字形(保留advance): {''.join(miss[:20])}")
|
||||
|
||||
placements, aw, ah, chosen = pack_best(glyphs)
|
||||
print(f" packing atlas {aw}x{ah} (max_w={chosen}) glyphs={len(glyphs)}")
|
||||
|
||||
plane_l8 = bytearray(aw * ah)
|
||||
tab = []
|
||||
for i, g in enumerate(glyphs):
|
||||
if i in placements:
|
||||
px, py = placements[i]
|
||||
for row in range(g.h):
|
||||
s = row * g.w
|
||||
d = (py + row) * aw + px
|
||||
plane_l8[d:d + g.w] = g.pixels[s:s + g.w]
|
||||
tab.append((g.cp, px, py, g.w, g.h, g.adv, g.dx, g.dy))
|
||||
else:
|
||||
tab.append((g.cp, 0, 0, 0, 0, g.adv, 0, 0))
|
||||
all_planes.append((aw, ah, bytes(plane_l8), tab))
|
||||
max_w = max(max_w, aw)
|
||||
|
||||
# ---- 编排到公共画布(纵向堆叠,公共宽 max_w 偶数化) ----
|
||||
if max_w % 2:
|
||||
max_w += 1
|
||||
total_h = sum(p[1] for p in all_planes)
|
||||
canvas_l8 = bytearray(max_w * total_h)
|
||||
face_meta = []
|
||||
fy = 0
|
||||
for i, (aw, ah, plane, tab) in enumerate(all_planes):
|
||||
for y in range(ah):
|
||||
src = y * aw
|
||||
dst = (fy + y) * max_w
|
||||
canvas_l8[dst:dst + aw] = plane[src:src + aw]
|
||||
face_meta.append({"px": sizes[i], "plane_x": 0, "plane_y": fy,
|
||||
"plane_w": aw, "plane_h": ah,
|
||||
"ascent": int(ImageFont.truetype(args.font, sizes[i]).getmetrics()[0]),
|
||||
"descent": int(abs(ImageFont.truetype(args.font, sizes[i]).getmetrics()[1]))})
|
||||
fy += ah
|
||||
|
||||
# ---- 打包 + PNG ----
|
||||
packed = pack_plane_depth(canvas_l8, max_w, total_h, args.bitdepth)
|
||||
png_bytes = gray_png_encode(max_w, total_h, args.bitdepth, packed)
|
||||
png_path = os.path.join(args.out, f"{args.name}.png")
|
||||
with open(png_path, "wb") as fp:
|
||||
fp.write(png_bytes)
|
||||
|
||||
# ---- idx ----
|
||||
idx_path = os.path.join(args.out, f"{args.name}.idx")
|
||||
idx_size = write_idx(idx_path, args.bitdepth, face_meta, [p[3] for p in all_planes])
|
||||
|
||||
png_size = os.path.getsize(png_path)
|
||||
total = png_size + idx_size
|
||||
print(f"\nfont.png {png_size} B")
|
||||
print(f"font.idx {idx_size} B")
|
||||
print(f"合计 {total} B = {total/1024:.1f} KiB = {total/1048576:.2f} MiB (预算 2MiB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user