"""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()