zcbot/skills/ppt/scripts/render_icon.py

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