IMX6U-Game/tools/gen_font_atlas.py

143 lines
5.0 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.

"""
用 PIL 渲染 ASCII 像素风位图字体图集。
用法:
python tools/gen_font_atlas.py [output_dir]
默认输出目录: assets/font/
生成: font_atlas.png, font_atlas.h
字符范围: ASCII 32~126
字体: 优先使用 assets/font/ndrtyyl-prqyys-undertale-hebrew-uppercase.ttf
不存在时回退到系统等宽字体Courier New / Consolas / monospace
像素格式: (R << 24) | (G << 16) | (B << 8) | A
生成端会把 alpha 阈值化为 0/255匹配当前 DrawContext::draw_text 的像素风绘制路径。
"""
import os
import sys
from PIL import Image, ImageDraw, ImageFont
FIRST_CHAR = 32
LAST_CHAR = 126
NUM_CHARS = LAST_CHAR - FIRST_CHAR + 1 # 95
COLUMNS = 16
ROWS = (NUM_CHARS + COLUMNS - 1) // COLUMNS # 6
def find_mono_font(size: int) -> ImageFont.FreeTypeFont:
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
candidates = [
os.path.join(repo_root, "assets/font", "ndrtyyl-prqyys-undertale-hebrew-uppercase.ttf"),
"C:/Windows/Fonts/consola.ttf", # Consolas (Windows)
"C:/Windows/Fonts/cour.ttf", # Courier New (Windows)
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", # Linux
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
]
for path in candidates:
if os.path.exists(path):
return ImageFont.truetype(path, size)
return ImageFont.load_default()
def generate(output_dir: str):
font = find_mono_font(14)
# 测量完整 ASCII 字符集的尺寸。
# 不能只用 "M":它没有下伸部,按它算高度会裁掉 g/j/p/q/y/_ 等字符。
tmp_img = Image.new("RGBA", (1, 1))
tmp_draw = ImageDraw.Draw(tmp_img)
chars = [chr(FIRST_CHAR + i) for i in range(NUM_CHARS)]
bboxes = [tmp_draw.textbbox((0, 0), ch, font=font) for ch in chars]
pad = 1
min_left = min(b[0] for b in bboxes)
min_top = min(b[1] for b in bboxes)
max_right = max(b[2] for b in bboxes)
max_bottom = max(b[3] for b in bboxes)
# 等宽字体仍使用统一 cell。宽度用字体 advance避免窄字符影响字距
# 高度用全字符 bbox保证下伸字符不被裁切。
max_advance = max(tmp_draw.textlength(ch, font=font) for ch in chars)
char_w = int(max_advance + 0.9999) + pad * 2
char_h = (max_bottom - min_top) + pad * 2
atlas_w = COLUMNS * char_w
atlas_h = ROWS * char_h
img = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
for i in range(NUM_CHARS):
ch = chr(FIRST_CHAR + i)
col = i % COLUMNS
row = i // COLUMNS
x = col * char_w + pad - min_left
y = row * char_h + pad - min_top
draw.text((x, y), ch, fill=(255, 255, 255, 255), font=font)
# 像素风:把 TrueType 抗锯齿 alpha 二值化。
# 渲染端目前只判断 alpha 是否非 0如果保留半透明边缘会被画成实心像素导致发胖/毛边。
threshold = 128
for py in range(atlas_h):
for px in range(atlas_w):
_, _, _, a = img.getpixel((px, py))
img.putpixel((px, py), (255, 255, 255, 255) if a >= threshold else (0, 0, 0, 0))
png_path = os.path.join(output_dir, "font_atlas.png")
img.save(png_path)
# 生成 C 头文件:将每行像素打包成 bit 行(每像素 1 bit白色=1透明=0
# 这样每个 glyph 的一行只需要 char_w/8 个字节
# 但为了简单,这里直接存储为 uint32_t RGBA 像素数组
# 使用时白色像素用指定颜色替换,透明像素跳过
pixels = []
for y in range(atlas_h):
for x in range(atlas_w):
r, g, b, a = img.getpixel((x, y))
pixels.append((r << 24) | (g << 16) | (b << 8) | a)
h_path = os.path.join(output_dir, "font_atlas.h")
with open(h_path, "w", encoding="utf-8") as f:
f.write("// Auto-generated by tools/gen_font_atlas.py\n")
f.write("#pragma once\n#include <cstdint>\n\n")
f.write(f"static const int32_t font_atlas_width = {atlas_w};\n")
f.write(f"static const int32_t font_atlas_height = {atlas_h};\n")
f.write(f"static const int32_t font_char_w = {char_w};\n")
f.write(f"static const int32_t font_char_h = {char_h};\n")
f.write(f"static const int32_t font_first_char = {FIRST_CHAR};\n")
f.write(f"static const int32_t font_columns = {COLUMNS};\n\n")
f.write(f"static const uint32_t font_atlas_pixels[] = {{\n")
for i in range(0, len(pixels), 16):
chunk = pixels[i:i + 16]
line = ", ".join(f"0x{p:08X}" for p in chunk)
f.write(f" {line},\n")
f.write("};\n")
print(f"Atlas: {atlas_w}x{atlas_h}, char: {char_w}x{char_h}")
print(f"Generated: {png_path}, {h_path}")
def main():
if len(sys.argv) >= 2:
out_dir = sys.argv[1]
else:
out_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"assets", "font"
)
os.makedirs(out_dir, exist_ok=True)
generate(out_dir)
if __name__ == "__main__":
main()