#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ STM32 ST7735S LCD CJK 字模生成工具(Pillow 版) 生成 hz16_data.c + hz16_data.h(含二分查找函数) 用法: # CJK 常用汉字(GB2312 一级,约 3755 字) python font_gen.py --cjk --font simsun --size 32 --threshold 64 # CJK + 标点 python font_gen.py --cjk --punct """ import os import sys import argparse try: from PIL import Image, ImageFont, ImageDraw except ImportError: print("请安装 Pillow: pip install Pillow", file=sys.stderr) sys.exit(1) # ── Otsu 自适应阈值 ── def otsu_threshold(pixels: list) -> int: """Otsu 算法自动计算最佳二值化阈值""" hist = [0] * 256 for v in pixels: hist[v] += 1 total = len(pixels) sum_all = sum(i * hist[i] for i in range(256)) sum_bg = 0 w_bg = 0 w_fg = 0 max_var = 0 best = 128 for t in range(256): w_bg += hist[t] if w_bg == 0: continue w_fg = total - w_bg if w_fg == 0: break sum_bg += t * hist[t] mean_bg = sum_bg / w_bg mean_fg = (sum_all - sum_bg) / w_fg between_var = w_bg * w_fg * (mean_bg - mean_fg) * (mean_bg - mean_fg) if between_var > max_var: max_var = between_var best = t return best # ── 通用渲染 ── def render_char(font, char: str, w: int, h: int, scale: int = 1, threshold: int = None) -> list: """ 在 w*scale × h*scale 画布上渲染字符,缩放到 w×h threshold=None 时自动使用 Otsu 算法 返回逐行字节列表(MSB 优先) """ bw, bh = w * scale, h * scale img = Image.new("L", (bw, bh), 0) draw = ImageDraw.Draw(img) bbox = draw.textbbox((0, 0), char, font=font) tw = bbox[2] - bbox[0] th = bbox[3] - bbox[1] # 防止 bbox[0]/bbox[1] 为负导致偏移错误 left = bbox[0] if bbox[0] > 0 else 0 top = bbox[1] if bbox[1] > 0 else 0 cx = (bw - tw) // 2 - left cy = (bh - th) // 2 - top draw.text((cx, cy), char, font=font, fill=255) small = img.resize((w, h), Image.LANCZOS) pixels = list(small.getdata()) if threshold is None: threshold = otsu_threshold(pixels) bytes_per_row = (w + 7) // 8 result = [] for r in range(h): for br in range(bytes_per_row): byte_val = 0 for c in range(8): col = br * 8 + c if col < w and pixels[r * w + col] >= threshold: byte_val |= (1 << (7 - c)) result.append(byte_val) return result # ── CJK 16×16 ── CJK_W, CJK_H = 16, 16 CJK_BYTES = CJK_H * ((CJK_W + 7) // 8) # 32 def get_gb2312_level1_unicodes(): """GB2312 一级汉字(常用 ~3755 字)的 Unicode 码点""" out = [] for u in range(0x4E00, 0x9FA6): try: b = chr(u).encode("gb2312") if len(b) == 2: qu = b[0] - 0xA1 if 16 <= qu <= 55: out.append(u) except (UnicodeEncodeError, LookupError): pass return out def get_gb2312_all_unicodes(): """GB2312 全部汉字(一级+二级 ~6763 字)""" out = [] for u in range(0x4E00, 0x9FA6): try: b = chr(u).encode("gb2312") if len(b) == 2: out.append(u) except (UnicodeEncodeError, LookupError): pass return out CJK_PUNCTUATION = [ 0x00B7, 0x2014, 0x2018, 0x2019, 0x201C, 0x201D, 0x2026, 0x3000, 0x3001, 0x3002, 0x300A, 0x300B, 0x300C, 0x300D, 0x300E, 0x300F, 0x3010, 0x3011, 0x3014, 0x3015, 0xFF01, 0xFF02, 0xFF07, 0xFF08, 0xFF09, 0xFF0C, 0xFF0E, 0xFF1A, 0xFF1B, 0xFF1F, 0xFF3B, 0xFF3D, 0xFF5B, 0xFF5D, ] CJK_FULLWIDTH_ASCII = list(range(0xFF01, 0xFF5F)) # 全角 ASCII def generate_cjk(base_path: str, font_path: str, size: int, threshold: int, scale: int = 2, mode: str = 'common', punct: bool = False): """生成 CJK 16×16 字库 .c + .h""" font = ImageFont.truetype(font_path, size) if mode == 'common': unicodes = get_gb2312_level1_unicodes() print(f"CJK 范围: GB2312 一级汉字 ({len(unicodes)} 字)") elif mode == 'gb2312': unicodes = get_gb2312_all_unicodes() print(f"CJK 范围: GB2312 全部汉字 ({len(unicodes)} 字)") else: unicodes = [] print("CJK 范围: 无") if punct: extra = set(CJK_PUNCTUATION) | set(CJK_FULLWIDTH_ASCII) unicodes = sorted(set(unicodes) | extra) print(f"已加入 {len(extra)} 个中文标点/全角字符") if not unicodes: print("未指定 CJK 范围,使用默认字符") unicodes = [ord(c) for c in "你好世界一二三"] unicodes = sorted(set(unicodes)) N = len(unicodes) # 渲染 bitmaps = [] for i, u in enumerate(unicodes): ch = chr(u) bm = render_char(font, ch, CJK_W, CJK_H, scale, threshold) bitmaps.append(bm) if (i + 1) % 500 == 0: print(f" CJK 进度: {i+1}/{N}") # ── 写 .c ── c_path = base_path + ".c" h_path = base_path + ".h" name_base = os.path.basename(base_path) guard = name_base.upper().replace(".", "_").replace("-", "_") + "_H" with open(c_path, 'w', encoding='utf-8', newline='\n') as f: f.write(f'/* 16×16 CJK 字库,由 font_gen.py 生成 */\n\n') f.write(f'#include "{name_base}.h"\n\n') f.write(f'static const uint8_t cjk_bitmaps[][{CJK_BYTES}] = {{\n') for u, bm in zip(unicodes, bitmaps): hex_str = ', '.join(f'0x{x:02X}' for x in bm) try: comment = f' /* U+{u:04X} {chr(u)} */' except: comment = f' /* U+{u:04X} */' f.write(f' {{ {hex_str} }},{comment}\n') f.write('};\n\n') f.write(f'static const uint16_t cjk_unicodes[{N}] = {{\n') for i in range(0, N, 16): chunk = unicodes[i:i+16] f.write(' ' + ', '.join(f'0x{u:04X}' for u in chunk) + ',\n') f.write('};\n\n') f.write(f'const uint8_t* cjk_get_bitmap(uint16_t unicode)\n') f.write('{\n') f.write(f' int lo = 0, hi = {N};\n') f.write(' while (lo < hi) {\n') f.write(' int mid = (lo + hi) >> 1;\n') f.write(' if (cjk_unicodes[mid] < unicode) lo = mid + 1;\n') f.write(' else if (cjk_unicodes[mid] > unicode) hi = mid;\n') f.write(' else return cjk_bitmaps[mid];\n') f.write(' }\n') f.write(' return (const uint8_t*)0;\n') f.write('}\n') print(f'已生成: {c_path}') # ── 写 .h ── with open(h_path, 'w', encoding='utf-8', newline='\n') as f: f.write(f'/* 16×16 CJK 字库,由 font_gen.py 生成 */\n\n') f.write(f'#ifndef {guard}\n#define {guard}\n\n') f.write(f'#include \n\n') f.write(f'#define CJK_BYTES_PER_CHAR {CJK_BYTES}\n') f.write(f'#define CJK_WIDTH {CJK_W}\n') f.write(f'#define CJK_HEIGHT {CJK_H}\n') f.write(f'#define CJK_NUM_CHARS {N}\n\n') f.write(f'/* 按 Unicode 码点查找 16×16 点阵,返回 {CJK_BYTES} 字节,未找到返回 NULL */\n') f.write(f'const uint8_t* cjk_get_bitmap(uint16_t unicode);\n\n') f.write(f'#endif /* {guard} */\n') print(f'已生成: {h_path}') def generate_cjk_direct(base_path: str, font_path: str, size: int, threshold: int, scale: int, unicodes: list): """用指定的 Unicode 列表生成 CJK 字库""" font = ImageFont.truetype(font_path, size) N = len(unicodes) print(f"CJK 自定义字符: {N} 字: {''.join(chr(u) for u in unicodes[:50])}{'...' if N > 50 else ''}") bitmaps = [] for i, u in enumerate(unicodes): bm = render_char(font, chr(u), CJK_W, CJK_H, scale, threshold) bitmaps.append(bm) # ── 写 .c ── c_path = base_path + ".c" h_path = base_path + ".h" name_base = os.path.basename(base_path) guard = name_base.upper().replace(".", "_").replace("-", "_") + "_H" with open(c_path, 'w', encoding='utf-8', newline='\n') as f: f.write(f'/* 16×16 CJK 字库,由 font_gen.py 生成(自定义) */\n\n') f.write(f'#include "{name_base}.h"\n\n') f.write(f'static const uint8_t cjk_bitmaps[][{CJK_BYTES}] = {{\n') for u, bm in zip(unicodes, bitmaps): hex_str = ', '.join(f'0x{x:02X}' for x in bm) f.write(f' {{ {hex_str} }}, /* U+{u:04X} {chr(u)} */\n') f.write('};\n\n') f.write(f'static const uint16_t cjk_unicodes[{N}] = {{\n') for i in range(0, N, 16): chunk = unicodes[i:i+16] f.write(' ' + ', '.join(f'0x{u:04X}' for u in chunk) + ',\n') f.write('};\n\n') f.write(f'const uint8_t* cjk_get_bitmap(uint16_t unicode)\n') f.write('{\n') f.write(f' int lo = 0, hi = {N};\n') f.write(' while (lo < hi) {\n') f.write(' int mid = (lo + hi) >> 1;\n') f.write(' if (cjk_unicodes[mid] < unicode) lo = mid + 1;\n') f.write(' else if (cjk_unicodes[mid] > unicode) hi = mid;\n') f.write(' else return cjk_bitmaps[mid];\n') f.write(' }\n') f.write(' return (const uint8_t*)0;\n') f.write('}\n') print(f'已生成: {c_path}') # ── 写 .h ── with open(h_path, 'w', encoding='utf-8', newline='\n') as f: f.write(f'/* 16×16 CJK 字库,由 font_gen.py 生成(自定义) */\n\n') f.write(f'#ifndef {guard}\n#define {guard}\n\n') f.write(f'#include \n\n') f.write(f'#define CJK_BYTES_PER_CHAR {CJK_BYTES}\n') f.write(f'#define CJK_WIDTH {CJK_W}\n') f.write(f'#define CJK_HEIGHT {CJK_H}\n') f.write(f'#define CJK_NUM_CHARS {N}\n\n') f.write(f'const uint8_t* cjk_get_bitmap(uint16_t unicode);\n\n') f.write(f'#endif /* {guard} */\n') print(f'已生成: {h_path}') # ── 主入口 ── def _resolve_font(name_or_path: str) -> str: if os.path.isfile(name_or_path): return name_or_path font_dir = r'C:\Windows\Fonts' key = name_or_path.lower() known = { 'consolas': 'consola.ttf', 'arial': 'arial.ttf', 'tahoma': 'tahoma.ttf', 'simsun': 'simsun.ttc', 'songti': 'simsun.ttc', '宋体': 'simsun.ttc', 'simhei': 'simhei.ttf', '黑体': 'simhei.ttf', 'yahei': 'msyh.ttc', '微软雅黑': 'msyh.ttc', } if key in known: full = os.path.join(font_dir, known[key]) if os.path.isfile(full): return full for f in os.listdir(font_dir): if f.lower().startswith(key) and f.lower().endswith(('.ttf', '.ttc')): return os.path.join(font_dir, f) # fallback fallback = os.path.join(font_dir, 'simsun.ttc') if os.path.isfile(fallback): return fallback return name_or_path def main(): p = argparse.ArgumentParser(description='CJK 字模生成工具') p.add_argument('--font', default='simsun', help='字体名称或路径') p.add_argument('--size', type=int, default=None, help='渲染字号') p.add_argument('--threshold', type=int, default=None, help='二值化阈值(默认 Otsu 自适应)') p.add_argument('--scale', type=int, default=None, help='缩放倍率(越大边缘越平滑)') p.add_argument('--cjk', action='store_true', help='生成 CJK 汉字字模(必须指定)') p.add_argument('--common', action='store_true', help='CJK:仅常用汉字(GB2312 一级)') p.add_argument('--gb2312', action='store_true', help='CJK:GB2312 全部汉字') p.add_argument('--punct', action='store_true', help='CJK:加入中文标点/全角字符') p.add_argument('--string', type=str, default=None, help='CJK:仅生成指定字符串中的字符(节省空间)') p.add_argument('--output', '-o', default=None, help='CJK 输出基名(不含扩展名)') args = p.parse_args() font_path = _resolve_font(args.font) print(f"字体: {font_path}") if not args.cjk: print("请指定 --cjk 来生成 CJK 字库,ASCII 字库已不再由此工具生成") return proj_dir = os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'STM32F103C8T6', 'Drivers', 'BSP', 'LCD')) size = args.size if args.size is not None else 48 scale = args.scale if args.scale is not None else 3 out_base = args.output or os.path.join(proj_dir, 'hz16_data') if args.string is not None: unicodes = sorted(set(ord(c) for c in args.string)) generate_cjk_direct(out_base, font_path, size, args.threshold, scale, unicodes) else: generate_cjk(out_base, font_path, size, args.threshold, scale, mode='common' if args.common else ('gb2312' if args.gb2312 else 'common'), punct=args.punct) if __name__ == '__main__': main()