File size: 5,259 Bytes
880cb16
 
 
f2690e7
880cb16
 
 
f2690e7
880cb16
 
f2690e7
880cb16
f2690e7
f97f2ac
f2690e7
 
 
 
f97f2ac
f2690e7
 
880cb16
f2690e7
 
880cb16
f2690e7
 
880cb16
f2690e7
880cb16
f2690e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f97f2ac
f2690e7
 
 
 
 
 
 
 
 
f97f2ac
f2690e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
880cb16
f97f2ac
880cb16
f2690e7
 
880cb16
f2690e7
f97f2ac
f2690e7
 
 
 
 
 
 
880cb16
f2690e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
880cb16
f2690e7
 
 
 
 
 
 
 
 
 
880cb16
 
f2690e7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/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