130 lines
4.1 KiB
Python
130 lines
4.1 KiB
Python
"""render_icon.py: unicode 字形 → 透明背景 PNG。
|
|
|
|
MSO_SHAPE 覆盖不到的图标 (齿轮、放大镜、文件夹等),用字形渲染兜底。
|
|
首选 MSO_SHAPE,见 references/icons.md。
|
|
|
|
用法:
|
|
python render_icon.py "✓" --color "#38B2AC" --size 96 -o check.png
|
|
python render_icon.py "★" --color "#FFC000" --size 128 -o star.png
|
|
python render_icon.py "→" --color "#1F4E79" --size 64 -o arrow.png
|
|
|
|
退出码:
|
|
0 = 成功
|
|
1 = Pillow 缺失
|
|
2 = 字体找不到
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def find_font(preferred: list[str]) -> str | None:
|
|
"""按顺序找系统字体。返回字体路径或 None。"""
|
|
candidates = []
|
|
# Windows
|
|
candidates += [
|
|
rf"C:\Windows\Fonts\{name}" for name in [
|
|
"seguisym.ttf", # Segoe UI Symbol
|
|
"seguiemj.ttf", # Segoe UI Emoji (彩色,慎用)
|
|
"msyh.ttc", "msyh.ttf", # 微软雅黑
|
|
"simsun.ttc", # 宋体
|
|
"arial.ttf",
|
|
]
|
|
]
|
|
# macOS
|
|
candidates += [
|
|
"/System/Library/Fonts/Apple Symbols.ttf",
|
|
"/System/Library/Fonts/PingFang.ttc",
|
|
"/Library/Fonts/Arial Unicode.ttf",
|
|
]
|
|
# Linux
|
|
candidates += [
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
|
]
|
|
|
|
if preferred:
|
|
candidates = preferred + candidates
|
|
|
|
for c in candidates:
|
|
if Path(c).exists():
|
|
return c
|
|
return None
|
|
|
|
|
|
def hex_to_rgba(hex_str: str) -> tuple[int, int, int, int]:
|
|
h = hex_str.lstrip("#")
|
|
if len(h) == 6:
|
|
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), 255
|
|
if len(h) == 8:
|
|
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), int(h[6:8], 16)
|
|
raise ValueError(f"bad hex color: {hex_str}")
|
|
|
|
|
|
def render(glyph: str, color: str, size_px: int, output: Path,
|
|
font_path: str | None, padding: int) -> None:
|
|
try:
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
except ImportError:
|
|
print("[fatal] pip install Pillow", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
font_path = font_path or find_font([])
|
|
if not font_path:
|
|
print("[fatal] no symbol font found; pass --font /path/to/font.ttf",
|
|
file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
rgba = hex_to_rgba(color)
|
|
|
|
# 字体载入,用 size_px 的 0.85 做实际字号让字形不顶格
|
|
font_size = int(size_px * 0.85)
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
|
|
# 测量字形真实包围盒
|
|
tmp = Image.new("RGBA", (size_px * 2, size_px * 2), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(tmp)
|
|
bbox = draw.textbbox((0, 0), glyph, font=font)
|
|
tw = bbox[2] - bbox[0]
|
|
th = bbox[3] - bbox[1]
|
|
|
|
# 输出画布:正方形,边长 = size_px,加 padding
|
|
canvas_size = size_px + 2 * padding
|
|
img = Image.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# 居中绘制 (考虑 bbox 偏移)
|
|
x = (canvas_size - tw) // 2 - bbox[0]
|
|
y = (canvas_size - th) // 2 - bbox[1]
|
|
draw.text((x, y), glyph, font=font, fill=rgba)
|
|
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
img.save(output, "PNG")
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("glyph", help="unicode 字符,如 ✓ ★ →")
|
|
ap.add_argument("--color", default="#1F4E79", help="hex,默认 #1F4E79")
|
|
ap.add_argument("--size", type=int, default=96,
|
|
help="像素边长 (字形主体),默认 96")
|
|
ap.add_argument("--padding", type=int, default=8,
|
|
help="周围透明边距像素,默认 8")
|
|
ap.add_argument("--font", default=None,
|
|
help="自定义字体路径 (.ttf/.ttc/.otf)")
|
|
ap.add_argument("-o", "--output", type=Path, required=True,
|
|
help="输出 PNG 路径")
|
|
args = ap.parse_args()
|
|
|
|
render(args.glyph, args.color, args.size, args.output,
|
|
args.font, args.padding)
|
|
size_kb = args.output.stat().st_size / 1024
|
|
print(f"[ok] {args.output} ({size_kb:.1f} KB)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|