菜单支持,所有功能都测试成功

This commit is contained in:
2026-07-15 15:39:49 +08:00
parent d4112399c2
commit 92facadeb0
16 changed files with 908 additions and 447 deletions

View File

@@ -1,14 +1,10 @@
#!/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含二分查找函数
STM32 ST7735S LCD CJK 字模生成工具Pillow 版)
生成 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
@@ -19,7 +15,6 @@ STM32 ST7735S LCD 字模生成工具Pillow 版)
import os
import sys
import argparse
import struct
try:
from PIL import Image, ImageFont, ImageDraw
@@ -27,12 +22,44 @@ 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 = 64) -> list:
threshold: int = None) -> list:
"""
在 w*scale × h*scale 画布上渲染字符,缩放到 w×h
threshold=None 时自动使用 Otsu 算法
返回逐行字节列表MSB 优先)
"""
bw, bh = w * scale, h * scale
@@ -42,13 +69,20 @@ def render_char(font, char: str, w: int, h: int, scale: int = 1,
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]
# 防止 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):
@@ -62,57 +96,6 @@ def render_char(font, char: str, w: int, h: int, scale: int = 1,
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
@@ -344,50 +327,42 @@ def _resolve_font(name_or_path: str) -> str:
def main():
p = argparse.ArgumentParser(description='字模生成工具')
p = argparse.ArgumentParser(description='CJK 字模生成工具')
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 输出基名(不含扩展名)')
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'))
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')
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 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:
# 默认 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)
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__':