178 lines
5.6 KiB
Python
178 lines
5.6 KiB
Python
"""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 "<svg" not in svg:
|
|
print(
|
|
f"[error] 返回不是 SVG: 图标 '{args.set}:{args.name}' 不存在",
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
out: Path = args.out
|
|
want_svg = args.svg_only or out.suffix.lower() == ".svg"
|
|
|
|
if want_svg:
|
|
if out.suffix.lower() != ".svg":
|
|
out = out.with_suffix(".svg")
|
|
out.write_text(svg, encoding="utf-8")
|
|
print(f"[ok] SVG → {out}")
|
|
return 0
|
|
|
|
if svg_to_png(svg, out, args.size):
|
|
print(f"[ok] PNG → {out} ({args.set}:{args.name} #{args.color})")
|
|
return 0
|
|
|
|
# PNG 转换失败,保存 SVG 兜底
|
|
svg_alt = out.with_suffix(".svg")
|
|
svg_alt.write_text(svg, encoding="utf-8")
|
|
print(
|
|
f"[warn] PNG 转换不可用 (装 `pip install cairosvg` 或 `pip install svglib`)\n"
|
|
f" 已保存 SVG → {svg_alt}\n"
|
|
f" PowerPoint 2016+ 直接 add_picture(svg) 也可以",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|