370 lines
13 KiB
Python
370 lines
13 KiB
Python
#!/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 <stdint.h>\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 <stdint.h>\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()
|