Files
STM32F103/tools/font_gen.py
2026-07-14 17:51:39 +08:00

395 lines
14 KiB
Python
Raw 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 字模生成工具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 <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='字模生成工具')
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='CJKGB2312 全部汉字')
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()