IMX6U-Game/tools/gen_font_atlas.py

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

"""
用 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
输出格式: 1-bit row-major bit-packed maskMSB-first
生成端会把 alpha 阈值化为 0/255再将整张 atlas 逐行打包成遮罩位图。
"""
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)
mask_bits = []
packed = 0
bit_count = 0
for y in range(atlas_h):
for x in range(atlas_w):
_, _, _, a = img.getpixel((x, y))
packed = (packed << 1) | (1 if a != 0 else 0)
bit_count += 1
if bit_count == 8:
mask_bits.append(packed)
packed = 0
bit_count = 0
if bit_count != 0:
mask_bits.append(packed << (8 - bit_count))
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 uint8_t font_atlas_mask[] = {{\n")
for i in range(0, len(mask_bits), 16):
chunk = mask_bits[i:i + 16]
line = ", ".join(f"0x{value:02X}" for value 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()