innoai commited on
Commit
815bc09
·
verified ·
1 Parent(s): be37e5c

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +53 -53
main.py CHANGED
@@ -1,33 +1,35 @@
 
 
1
  """
2
  FastAPI 版 SVG ➜ PNG 转换服务
3
  ──────────────────────────────────────────
4
- 与 Gradio 版本保持同等功能:
5
  1. 自动检测 / 定位 Inkscape
6
- 2. 自动下载并嵌入 <image href="http/https"> 外链图片(Base64)
7
  3. 支持 DPI / 宽高 / 背景色 / 透明 / 导出区域 / 指定元素 ID / 简化 SVG
8
  4. 返回生成的 PNG 图片文件
9
- ──────────────────────────────────────────
10
  运行:
11
  python main.py
12
  接口:
13
- POST /convert ➜ Multipart 表单上传 svg_file + 各参数,返回 image/png
14
- GET /health ➜ 简单健康检查
15
  """
16
-
17
  import os
18
- import subprocess
19
  import platform
20
- import tempfile
21
  import shutil
 
 
22
  import uuid
 
 
 
23
  import base64
24
  import mimetypes
25
  import urllib.request
26
- import re
27
- from pathlib import Path
28
- from typing import Optional
29
 
30
- from fastapi import FastAPI, UploadFile, File, Form, HTTPException
31
  from fastapi.responses import FileResponse, JSONResponse
32
 
33
  ###############################################################################
@@ -48,10 +50,10 @@ def ensure_inkscape_available() -> tuple[bool, str]:
48
  common_paths: list[str] = []
49
  if system == "Windows":
50
  common_paths = [
51
- r"C:\Program Files\Inkscape\bin\inkscape.exe",
52
- r"C:\Program Files (x86)\Inkscape\bin\inkscape.exe",
53
  r"C:\Program Files\Inkscape\inkscape.exe",
54
  r"C:\Program Files (x86)\Inkscape\inkscape.exe",
 
 
55
  ]
56
  elif system == "Linux":
57
  common_paths = [
@@ -64,44 +66,58 @@ def ensure_inkscape_available() -> tuple[bool, str]:
64
  "/Applications/Inkscape.app/Contents/MacOS/inkscape",
65
  "/usr/local/bin/inkscape",
66
  ]
67
-
68
- for path in common_paths:
69
- if os.path.exists(path):
70
- os.environ["PATH"] += os.pathsep + os.path.dirname(path)
71
- return True, f"在 {path} 找到 Inkscape"
72
-
73
  return False, "未找到 Inkscape,请安装或手动加入 PATH"
74
 
75
  ###############################################################################
76
- # 2. 下载并嵌入外链图片
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  ###############################################################################
78
- # 匹配 <image ... href="http(s)://..."> 或 xlink:href
79
  IMG_TAG_PATTERN = re.compile(
80
- r'<image[^>]+(?:href|xlink:href)\s*=\s*["\']([^"\']+)["\']', re.I
 
81
  )
82
 
83
  def download_and_encode(url: str) -> str:
84
  """下载远程图片并转为 data URI(Base64)"""
85
  try:
86
- req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
87
- with urllib.request.urlopen(req, timeout=15) as resp:
 
88
  data = resp.read()
89
- mime, _ = mimetypes.guess_type(url)
90
  mime = mime or "image/png"
91
  b64 = base64.b64encode(data).decode("ascii")
92
  return f"data:{mime};base64,{b64}"
93
  except Exception as e:
94
- # 失败则返回原链接(Inkscape 可能渲染空白 / 红叉)
95
  print(f"[WARN] 下载外链图片失败 {url}: {e}")
96
- return url
97
 
98
  def embed_external_images(svg_path: str) -> str:
99
  """把 SVG 中的远程图片下载并嵌入(Base64),返回新的临时 SVG 路径"""
100
  with open(svg_path, "r", encoding="utf-8") as f:
101
  svg_text = f.read()
102
-
103
  if "http://" not in svg_text and "https://" not in svg_text:
104
- return svg_path # 无外链,直接返回
105
 
106
  def replacer(match: re.Match) -> str:
107
  url = match.group(1)
@@ -125,7 +141,7 @@ def embed_external_images(svg_path: str) -> str:
125
  return new_path
126
 
127
  ###############################################################################
128
- # 3. 核心转换函数
129
  ###############################################################################
130
  def convert_svg_to_png(
131
  src_svg_path: str,
@@ -138,12 +154,10 @@ def convert_svg_to_png(
138
  export_plain_svg: bool = False,
139
  ) -> tuple[Optional[str], str]:
140
  """调用 Inkscape CLI 将 SVG 转为 PNG,返回 (文件路径 or None, 消息)"""
141
-
142
  ok, msg = ensure_inkscape_available()
143
  if not ok:
144
  return None, msg
145
 
146
- # 预处理外链
147
  processed_svg = embed_external_images(src_svg_path)
148
 
149
  tmp_dir = tempfile.mkdtemp()
@@ -164,29 +178,20 @@ def convert_svg_to_png(
164
  cmd += ["--export-height", str(height)]
165
  if background_color != "transparent":
166
  cmd += ["--export-background", background_color]
167
-
168
  if export_area == "drawing":
169
  cmd.append("--export-area-drawing")
170
  elif export_area == "page":
171
  cmd.append("--export-area-page")
172
  elif export_area == "id" and export_id:
173
  cmd += ["--export-id", export_id]
174
-
175
  if export_plain_svg:
176
  cmd.append("--export-plain-svg")
177
 
178
  try:
179
- subprocess.run(
180
- cmd,
181
- stdout=subprocess.PIPE,
182
- stderr=subprocess.PIPE,
183
- text=True,
184
- check=True,
185
- )
186
  if not os.path.exists(output_path):
187
  return None, "未生成 PNG 文件"
188
 
189
- # 把结果移动到项目根目录 output 目录
190
  out_dir = Path(__file__).parent / "output"
191
  out_dir.mkdir(exist_ok=True)
192
  final_path = out_dir / output_name
@@ -199,13 +204,13 @@ def convert_svg_to_png(
199
  return None, f"未知错误:{e}"
200
 
201
  ###############################################################################
202
- # 4. FastAPI 应用
203
  ###############################################################################
204
- app = FastAPI(title="SVG ➜ PNG 转换 API", version="1.0.0")
205
 
206
  @app.get("/health", summary="健康检查")
207
  def health():
208
- return {"status": "ok"}
209
 
210
  @app.post(
211
  "/convert",
@@ -229,9 +234,8 @@ async def convert_endpoint(
229
  ):
230
  """
231
  使用 Inkscape 将上传的 SVG 转换为 PNG 并返回。
232
- 所有参数与 Gradio 版本保持一致。
233
  """
234
- # 保存上传文件到临时目录
235
  temp_dir = tempfile.mkdtemp()
236
  uploaded_path = os.path.join(temp_dir, svg_file.filename)
237
  with open(uploaded_path, "wb") as f:
@@ -247,13 +251,11 @@ async def convert_endpoint(
247
  export_id=export_id if export_area == "id" else None,
248
  export_plain_svg=export_plain_svg,
249
  )
250
-
251
  shutil.rmtree(temp_dir, ignore_errors=True)
252
 
253
  if png_path is None:
254
  raise HTTPException(status_code=400, detail=msg)
255
 
256
- # 返回 PNG 文件流
257
  return FileResponse(
258
  png_path,
259
  media_type="image/png",
@@ -261,12 +263,10 @@ async def convert_endpoint(
261
  )
262
 
263
  ###############################################################################
264
- # 5. 入口
265
  ###############################################################################
266
  if __name__ == "__main__":
267
  import uvicorn
268
 
269
- # 确保 output 目录存在
270
  (Path(__file__).parent / "output").mkdir(exist_ok=True)
271
-
272
  uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=False)
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
  """
4
  FastAPI 版 SVG ➜ PNG 转换服务
5
  ──────────────────────────────────────────
6
+ 功能:
7
  1. 自动检测 / 定位 Inkscape
8
+ 2. 下载并嵌入 <image href="http/https"> 外链图片(Base64,支持中文 URL
9
  3. 支持 DPI / 宽高 / 背景色 / 透明 / 导出区域 / 指定元素 ID / 简化 SVG
10
  4. 返回生成的 PNG 图片文件
 
11
  运行:
12
  python main.py
13
  接口:
14
+ POST /convert ➜ Multipart 上传 svg_file + 参数,返回 image/png
15
+ GET /health ➜ 健康检查
16
  """
 
17
  import os
 
18
  import platform
19
+ import re
20
  import shutil
21
+ import subprocess
22
+ import tempfile
23
  import uuid
24
+ from pathlib import Path
25
+ from typing import Optional
26
+
27
  import base64
28
  import mimetypes
29
  import urllib.request
30
+ import urllib.parse
 
 
31
 
32
+ from fastapi import FastAPI, File, Form, HTTPException, UploadFile
33
  from fastapi.responses import FileResponse, JSONResponse
34
 
35
  ###############################################################################
 
50
  common_paths: list[str] = []
51
  if system == "Windows":
52
  common_paths = [
 
 
53
  r"C:\Program Files\Inkscape\inkscape.exe",
54
  r"C:\Program Files (x86)\Inkscape\inkscape.exe",
55
+ r"C:\Program Files\Inkscape\bin\inkscape.exe",
56
+ r"C:\Program Files (x86)\Inkscape\bin\inkscape.exe",
57
  ]
58
  elif system == "Linux":
59
  common_paths = [
 
66
  "/Applications/Inkscape.app/Contents/MacOS/inkscape",
67
  "/usr/local/bin/inkscape",
68
  ]
69
+ for p in common_paths:
70
+ if os.path.exists(p):
71
+ os.environ["PATH"] += os.pathsep + os.path.dirname(p)
72
+ return True, f" {p} 找到 Inkscape"
 
 
73
  return False, "未找到 Inkscape,请安装或手动加入 PATH"
74
 
75
  ###############################################################################
76
+ # 2. URL 规范化(解决中文 / 空格 / () 等字符)
77
+ ###############################################################################
78
+ def normalize_url(url: str) -> str:
79
+ """
80
+ 将包含非 ASCII 字符的 URL 转为合法百分号编码格式
81
+ """
82
+ parsed = urllib.parse.urlparse(url)
83
+ # 域名 IDNA(如有中文)
84
+ netloc = parsed.netloc.encode("idna").decode("ascii")
85
+ # 路径 / 查询 / 片段 百分号转义
86
+ path = urllib.parse.quote(parsed.path, safe="/")
87
+ params = urllib.parse.quote(parsed.params, safe=":&=")
88
+ query = urllib.parse.quote_plus(parsed.query, safe="=&")
89
+ fragment = urllib.parse.quote(parsed.fragment, safe="")
90
+ return urllib.parse.urlunparse((parsed.scheme, netloc, path, params, query, fragment))
91
+
92
+ ###############################################################################
93
+ # 3. 下载并嵌入外链图片
94
  ###############################################################################
 
95
  IMG_TAG_PATTERN = re.compile(
96
+ r'<image[^>]+(?:href|xlink:href)\s*=\s*["\']([^"\']+)["\']',
97
+ re.I,
98
  )
99
 
100
  def download_and_encode(url: str) -> str:
101
  """下载远程图片并转为 data URI(Base64)"""
102
  try:
103
+ safe_url = normalize_url(url) # 关键改动
104
+ req = urllib.request.Request(safe_url, headers={"User-Agent": "Mozilla/5.0"})
105
+ with urllib.request.urlopen(req, timeout=20) as resp:
106
  data = resp.read()
107
+ mime, _ = mimetypes.guess_type(safe_url)
108
  mime = mime or "image/png"
109
  b64 = base64.b64encode(data).decode("ascii")
110
  return f"data:{mime};base64,{b64}"
111
  except Exception as e:
 
112
  print(f"[WARN] 下载外链图片失败 {url}: {e}")
113
+ return url # 失败则返回原链接
114
 
115
  def embed_external_images(svg_path: str) -> str:
116
  """把 SVG 中的远程图片下载并嵌入(Base64),返回新的临时 SVG 路径"""
117
  with open(svg_path, "r", encoding="utf-8") as f:
118
  svg_text = f.read()
 
119
  if "http://" not in svg_text and "https://" not in svg_text:
120
+ return svg_path # 无外链直接返回
121
 
122
  def replacer(match: re.Match) -> str:
123
  url = match.group(1)
 
141
  return new_path
142
 
143
  ###############################################################################
144
+ # 4. 核心转换函数
145
  ###############################################################################
146
  def convert_svg_to_png(
147
  src_svg_path: str,
 
154
  export_plain_svg: bool = False,
155
  ) -> tuple[Optional[str], str]:
156
  """调用 Inkscape CLI 将 SVG 转为 PNG,返回 (文件路径 or None, 消息)"""
 
157
  ok, msg = ensure_inkscape_available()
158
  if not ok:
159
  return None, msg
160
 
 
161
  processed_svg = embed_external_images(src_svg_path)
162
 
163
  tmp_dir = tempfile.mkdtemp()
 
178
  cmd += ["--export-height", str(height)]
179
  if background_color != "transparent":
180
  cmd += ["--export-background", background_color]
 
181
  if export_area == "drawing":
182
  cmd.append("--export-area-drawing")
183
  elif export_area == "page":
184
  cmd.append("--export-area-page")
185
  elif export_area == "id" and export_id:
186
  cmd += ["--export-id", export_id]
 
187
  if export_plain_svg:
188
  cmd.append("--export-plain-svg")
189
 
190
  try:
191
+ subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
 
 
 
 
 
 
192
  if not os.path.exists(output_path):
193
  return None, "未生成 PNG 文件"
194
 
 
195
  out_dir = Path(__file__).parent / "output"
196
  out_dir.mkdir(exist_ok=True)
197
  final_path = out_dir / output_name
 
204
  return None, f"未知错误:{e}"
205
 
206
  ###############################################################################
207
+ # 5. FastAPI 应用
208
  ###############################################################################
209
+ app = FastAPI(title="SVG ➜ PNG 转换 API", version="1.1.0")
210
 
211
  @app.get("/health", summary="健康检查")
212
  def health():
213
+ return JSONResponse({"status": "ok"})
214
 
215
  @app.post(
216
  "/convert",
 
234
  ):
235
  """
236
  使用 Inkscape 将上传的 SVG 转换为 PNG 并返回。
 
237
  """
238
+ # 保存上传文件
239
  temp_dir = tempfile.mkdtemp()
240
  uploaded_path = os.path.join(temp_dir, svg_file.filename)
241
  with open(uploaded_path, "wb") as f:
 
251
  export_id=export_id if export_area == "id" else None,
252
  export_plain_svg=export_plain_svg,
253
  )
 
254
  shutil.rmtree(temp_dir, ignore_errors=True)
255
 
256
  if png_path is None:
257
  raise HTTPException(status_code=400, detail=msg)
258
 
 
259
  return FileResponse(
260
  png_path,
261
  media_type="image/png",
 
263
  )
264
 
265
  ###############################################################################
266
+ # 6. 入口
267
  ###############################################################################
268
  if __name__ == "__main__":
269
  import uvicorn
270
 
 
271
  (Path(__file__).parent / "output").mkdir(exist_ok=True)
 
272
  uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=False)