#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ gen_font.py — MeowOS 预渲染字库生成器(主机侧)· v2:像素进 PNG、索引进 idx。 产出(单份存储,设备与主机共用同一套字节): /font.png 灰度 PNG(bit depth 4|8),含全部字号的面,每面一个子矩形 /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 hashlib import json import os import struct import unicodedata 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) # -------------------------------------------------------------------------- # 字符集(字体档位:common / gbk / full) # 口径见 docs/MEOWOS_SPEC_REF.md §5.2「字体档位/大字库」与 §5.7.8: # common ≈ 4k 码位(ASCII + GB2312 常用 + 标点 + --extra) = 现状; # gbk ≈ 20k+(再并入 CJK 统一表意 U+4E00-9FFF + CJK 符号/全角,含常用简繁/生僻); # full ≈ 全 Unicode 可渲染区间(gbk + CJK 扩展 A/B-F + 拉丁/希腊/西里尔 + 通用标点,可选)。 # 每档在产物 font.json 写入 charset + glyph_count(见 write_font_json)。 # -------------------------------------------------------------------------- CHARSET_TIERS = ("common", "gbk", "full") CHARSET_ALIAS = {"0": "common", "common": "common", "1": "gbk", "gbk": "gbk", "2": "full", "full": "full"} def build_charset(tier="common", extra=""): if tier not in CHARSET_TIERS: tier = "common" cps = [] def add(cp): if 0 <= cp <= 0x10FFFF and cp not in cps: cps.append(cp) # ASCII 可打印字符 for c in range(0x20, 0x7F): add(c) # GB2312 一/二级常用简体(B0-D7 + D8 常用二级首段) for hi in range(0xB0, 0xD8): for lo in range(0xA1, 0xFF): try: add(ord(bytes([hi, lo]).decode("gb2312"))) except UnicodeDecodeError: pass # GB2312 符号/全角(A1-A4) for hi in range(0xA1, 0xA5): for lo in range(0xA1, 0xFF): try: add(ord(bytes([hi, lo]).decode("gb2312"))) except UnicodeDecodeError: pass if tier in ("gbk", "full"): # CJK 统一表意(含常用简繁/生僻):U+4E00-9FFF ≈ 2万+ for cp in range(0x4E00, 0xA000): add(cp) # CJK 符号与标点 + 全角形式(补齐 common 未含的) for cp in range(0x3000, 0x3040): add(cp) for cp in range(0xFF00, 0xFFF0): add(cp) if tier == "full": # 拉丁补充 / 希腊 / 西里尔 等常用非 CJK(Latin-1 起已覆盖 AI 常见重音) for cp in range(0xA0, 0x0530): add(cp) # CJK 扩展 A + 兼容表意 for cp in range(0x3400, 0x4DC0): add(cp) for cp in range(0xF900, 0xFB00): add(cp) # 通用标点 / 货币等(— … “ ” 等已在 gbk;此处补全) for cp in range(0x2000, 0x2070): add(cp) for cp in range(0x20A0, 0x20D0): add(cp) # CJK 扩展 B-F(补充平面) for cp in range(0x20000, 0x2EBF0): add(cp) for cp in range(0x30000, 0x31350): add(cp) for ch in extra: add(ord(ch)) return sorted(cps) def charset_glyph_count(cps): """glyph_count = 字符集码位数(信息用,落 font.json)。""" return len(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("