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:
MeowOS Dev committed 2026-08-30 01:12:34 +08:00
commit 245bee2946
136 files changed
+32730

No files matched your search

+321
View File
@@ -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()