#!/usr/bin/env python # -*- coding: utf-8 -*- """ SVG → PNG 批量转换(rsvg-convert ▸ Inkscape ▸ CairoSVG 三层保险版) 作者: ChatGPT 示例 """ import shutil, subprocess, tempfile, zipfile, gradio as gr from pathlib import Path from typing import List, Tuple import cairosvg # 兜底 DEFAULT_DPI = 300 # 默认分辨率 # ───────────────────────── 工具检测 ───────────────────────── def which(cmd: str) -> str | None: """返回可执行文件完整路径,找不到则 None""" return shutil.which(cmd) RSVG = which("rsvg-convert") INKSCAPE = which("inkscape") # ──────────────────────── 渲染核心函数 ─────────────────────── def svg_to_png(svg: Path, png: Path, dpi: int = DEFAULT_DPI): """ 依次尝试 rsvg-convert → Inkscape → CairoSVG。 任一成功即返回;全部失败则抛 RuntimeError。 """ errs = [] # 1) rsvg-convert(最快最稳,支持大部分特性) if RSVG: cmd = [RSVG, "-d", str(dpi), "-p", str(dpi), "-f", "png", "-o", str(png), str(svg)] try: subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if png.stat().st_size > 2048: # 简单判空 return except subprocess.CalledProcessError as e: errs.append(f"[rsvg-convert] {e.stderr.decode(errors='ignore')}") # 2) Inkscape CLI(加 export-area-drawing 防止空白) if INKSCAPE: cmd_new = [INKSCAPE, str(svg), "--export-type=png", f"--export-filename={png}", f"--export-dpi={dpi}", "--export-area-drawing"] # 关键参数 try: subprocess.run(cmd_new, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if png.stat().st_size > 2048: return except subprocess.CalledProcessError as e_new: # 极老版本 Inkscape (<1.0) 回退旧参数 cmd_old = [INKSCAPE, str(svg), f"--export-png={png}", f"-d={dpi}", "--export-area-drawing"] try: subprocess.run(cmd_old, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if png.stat().st_size > 2048: return except subprocess.CalledProcessError as e_old: errs.append(f"[Inkscape] {e_new.stderr.decode(errors='ignore')}\n" f"[Inkscape-old] {e_old.stderr.decode(errors='ignore')}") # 3) CairoSVG 兜底 try: cairosvg.svg2png(url=str(svg), write_to=str(png), dpi=dpi) if png.stat().st_size > 2048: return except Exception as cs_err: errs.append(f"[CairoSVG] {cs_err}") raise RuntimeError("三种渲染器均失败:\n" + "\n".join(errs)) # ───────────────────────── Gradio 回调 ─────────────────────── def batch_convert(files: List[gr.File], dpi: int = DEFAULT_DPI ) -> Tuple[List[Tuple[str, str]], str]: if not files: raise gr.Error("请先上传至少一个 SVG 文件!") tmp = Path(tempfile.mkdtemp(prefix="svg2png_")) out_dir = tmp / "png"; out_dir.mkdir() gallery, errs = [], [] for f in files: svg = Path(f.name) out = out_dir / f"{svg.stem}.png" try: svg_to_png(svg, out, dpi) gallery.append((out.name, str(out))) except Exception as e: errs.append(f"{svg.name} 转换失败:{e}") # 打包 zip zip_path = tmp / "result.zip" with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for p in out_dir.iterdir(): zf.write(p, p.name) if errs: # 收敛为单条 Warning,避免打断流程 gr.Warning("\n".join(errs)) return gallery, str(zip_path) # ────────────────────────── Gradio UI ──────────────────────── with gr.Blocks(title="SVG → PNG 在线转换", theme="soft") as demo: gr.Markdown(""" # 🖼️ SVG → PNG 转换器 - **rsvg-convert ▸ Inkscape ▸ CairoSVG** 三层保险 - 支持批量上传,DPI 可调,默认导出实际绘图区域 """) with gr.Row(): uploader = gr.File(file_count="multiple", file_types=[".svg"], label="上传 SVG 文件(可多选)") dpi_slider = gr.Slider(72, 600, value=DEFAULT_DPI, step=1, label="输出 DPI", info="分辨率越高越清晰") btn = gr.Button("🚀 开始转换") gallery = gr.Gallery(label="PNG 预览") zip_out = gr.File(label="下载全部 PNG (ZIP)") btn.click(batch_convert, [uploader, dpi_slider], [gallery, zip_out], api_name="convert", queue=True) if __name__ == "__main__": demo.launch(share=False) # 本地调试可改 True