"""fetch_icon.py: 从 Iconify CDN 拉个性化图标,按主题色染色,缓存本地。 Iconify 聚合了 150+ 免费开源图标集,无需账号、无 API key: tabler -- 现代描边 (Apache 2.0) ⭐ 推荐 lucide -- 开源经典 (ISC) heroicons -- Tailwind (MIT) material-symbols -- Google (Apache 2.0) carbon -- IBM (Apache 2.0) fluent -- Microsoft (MIT) mdi -- Material Design (Apache 2.0) 每个集都有数千图标,在 https://icon-sets.iconify.design/ 浏览找名字。 用法: # 推荐: 染主色,导出 PNG (需 cairosvg 或 svglib) python fetch_icon.py rocket --set tabler --color C00000 --size 128 \\ -o slides/rocket.png # 只要 SVG (PowerPoint 2016+ 支持嵌入 SVG) python fetch_icon.py target --set lucide --color FFC107 \\ -o slides/target.svg # 默认值: set=tabler, color=C00000(主红), size=128 python fetch_icon.py chart-bar -o slides/chart_bar.png 环境: PNG 转换依赖任一: `pip install cairosvg` (推荐) 或 `pip install svglib` 若都没有,会保存 .svg 到目标路径(扩展名自动改). 退出码: 0 = 成功 PNG/SVG 1 = SVG 有了但 PNG 转换失败 (已保存 SVG) 2 = 网络/图标名错误 (没拉到) """ from __future__ import annotations import argparse import io import sys import urllib.parse import urllib.request from pathlib import Path ICONIFY_API = "https://api.iconify.design/{set}/{name}.svg" def fetch_svg(name: str, icon_set: str, color: str, size: int) -> str: """从 Iconify 拉 SVG,带主题色和大小参数。""" params: dict[str, str] = {} if color: params["color"] = "#" + color.lstrip("#") if size: params["height"] = str(size) params["width"] = str(size) url = ICONIFY_API.format(set=icon_set, name=name) if params: url += "?" + urllib.parse.urlencode(params) req = urllib.request.Request( url, headers={"User-Agent": "ppt-skill-fetch_icon/1.0"} ) with urllib.request.urlopen(req, timeout=15) as resp: body = resp.read().decode("utf-8") return body def svg_to_png(svg_text: str, out_path: Path, size: int) -> bool: """SVG → PNG,降级链:cairosvg → svglib+reportlab → 失败。""" # 路径 1: cairosvg (推荐,质量最好) try: import cairosvg # type: ignore cairosvg.svg2png( bytestring=svg_text.encode("utf-8"), write_to=str(out_path), output_width=size, output_height=size, ) return True except ImportError: pass except Exception as e: print(f"[warn] cairosvg 渲染失败: {e}", file=sys.stderr) # 路径 2: svglib + reportlab try: from svglib.svglib import svg2rlg # type: ignore from reportlab.graphics import renderPM # type: ignore drawing = svg2rlg(io.StringIO(svg_text)) if drawing is None: return False renderPM.drawToFile(drawing, str(out_path), fmt="PNG") return True except ImportError: pass except Exception as e: print(f"[warn] svglib 渲染失败: {e}", file=sys.stderr) return False def main() -> int: ap = argparse.ArgumentParser( description="从 Iconify CDN 拉个性化 SVG/PNG 图标" ) ap.add_argument("name", help="图标名,见 https://icon-sets.iconify.design/") ap.add_argument( "--set", default="tabler", help="图标集 (默认 tabler;可选 lucide/heroicons/material-symbols/carbon/fluent/mdi)", ) ap.add_argument( "--color", default="C00000", help="主题色 hex,无 # (默认 C00000 商务红主色)", ) ap.add_argument( "--size", type=int, default=128, help="像素 (默认 128,适合 0.5-1.0 in PPT 图标)", ) ap.add_argument( "-o", "--out", required=True, type=Path, help="输出路径 (.png 或 .svg)", ) ap.add_argument( "--svg-only", action="store_true", help="只输出 SVG,跳过 PNG 转换", ) args = ap.parse_args() args.out.parent.mkdir(parents=True, exist_ok=True) try: svg = fetch_svg(args.name, args.set, args.color, args.size) except urllib.error.HTTPError as e: print( f"[error] Iconify 返回 {e.code}: 图标 '{args.set}:{args.name}' " f"可能不存在,在 https://icon-sets.iconify.design/{args.set}/ 搜", file=sys.stderr, ) return 2 except Exception as e: print(f"[error] 拉取失败: {e}", file=sys.stderr) return 2 if "