#!/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 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("