#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ STM32 ST7735S LCD 字模生成工具(Pillow 版) - ASCII 8×16:生成 lcd_data.c - CJK 16×16:生成 hz16_data.c + hz16_data.h(含二分查找函数) 用法: # ASCII python font_gen.py --font simsun --size 56 --threshold 64 # 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 import struct try: from PIL import Image, ImageFont, ImageDraw except ImportError: print("请安装 Pillow: pip install Pillow", file=sys.stderr) sys.exit(1) # ── 通用渲染 ── def render_char(font, char: str, w: int, h: int, scale: int = 1, threshold: int = 64) -> list: """ 在 w*scale × h*scale 画布上渲染字符,缩放到 w×h 返回逐行字节列表(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] cx = (bw - tw) // 2 - bbox[0] cy = (bh - th) // 2 - bbox[1] draw.text((cx, cy), char, font=font, fill=255) small = img.resize((w, h), Image.LANCZOS) pixels = list(small.getdata()) 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 # ── ASCII 8×16 ── def generate_ascii(output_path: str, font_path: str, size: int, threshold: int, scale: int = 4): font = ImageFont.truetype(font_path, size) chars = list(range(32, 127)) N = len(chars) lines = [ '#include "lcd_data.h"', '/*ASCII字模数据*********************/', '', '/*宽8像素,高16像素*/', f'/* 字体: {os.path.basename(font_path)}, 字号: {size}, 阈值: {threshold} */', 'const uint8_t LCD_F8x16[][16] =', '{', ] for idx, ch in enumerate(chars): if ch == ord('_'): bitmap = [0x00] * 15 + [0x7F] else: bitmap = render_char(font, chr(ch), 8, 16, scale, threshold) show = chr(ch) if 32 <= ch <= 126 else '?' comma = ',' if idx < N - 1 else '' line1 = ', '.join(f'0x{b:02X}' for b in bitmap[:8]) line2 = ', '.join(f'0x{b:02X}' for b in bitmap[8:]) lines.append(f' {line1},') lines.append(f' {line2}{comma} // {show} {idx}') if (idx + 1) % 10 == 0: print(f" ASCII 进度: {idx+1}/{N}") lines.append('};') lines.append('') lines.append('/*宽6像素,高8像素(未使用)*/') lines.append('const uint8_t LCD_F6x8[][6] =') lines.append('{') for idx, ch in enumerate(chars): show = chr(ch) if 32 <= ch <= 126 else '?' comma = ',' if idx < N - 1 else '' lines.append(f' 0x00,0x00,0x00,0x00,0x00,0x00{comma} // {show} {idx}') lines.append('};') lines.append('') content = '\n'.join(lines) with open(output_path, 'w', encoding='utf-8', newline='\n') as f: f.write(content) print(f"已生成 ASCII 字模: {output_path}") # ── 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='字模生成工具') p.add_argument('--font', default='simsun', help='字体名称或路径') p.add_argument('--size', type=int, default=64, help='渲染字号') p.add_argument('--threshold', type=int, default=50, help='二值化阈值(默认50)') p.add_argument('--scale', type=int, default=4, help='缩放倍率') g = p.add_argument_group('ASCII 8×16') g.add_argument('--ascii', action='store_true', help='生成 ASCII 字模(默认)') g = p.add_argument_group('CJK 16×16') g.add_argument('--cjk', action='store_true', help='生成 CJK 汉字字模') g.add_argument('--common', action='store_true', help='CJK:仅常用汉字(GB2312 一级)') g.add_argument('--gb2312', action='store_true', help='CJK:GB2312 全部汉字') g.add_argument('--punct', action='store_true', help='CJK:加入中文标点/全角字符') g.add_argument('--string', type=str, default=None, help='CJK:仅生成指定字符串中的字符(节省空间)') g.add_argument('--output', '-o', default=None, help='CJK 输出基名(不含扩展名)') args = p.parse_args() font_path = _resolve_font(args.font) print(f"字体: {font_path}") proj_dir = os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'STM32F103C8T6', 'Drivers', 'BSP', 'LCD')) if args.cjk: size = args.size or 48 scale = args.scale or 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) else: # 默认 ASCII size = args.size or 56 scale = args.scale or 4 out = os.path.join(proj_dir, 'lcd_data.c') generate_ascii(out, font_path, size, args.threshold, scale) if __name__ == '__main__': main()