Files
STM32F103/tools/font_gen.py

370 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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='CJKGB2312 全部汉字')
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()