2026-09-01 13:13:40 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2026-09-03 15:44:57 +08:00
|
|
|
|
"""SVG -> PNG 转换。
|
|
|
|
|
|
|
|
|
|
|
|
与 docs/HD2/run.py 一致:把上传的单张 SVG 按输出尺寸(默认 550x550)栅格化成
|
|
|
|
|
|
透明背景 PNG,不做批量,也不叠加边框。
|
|
|
|
|
|
|
|
|
|
|
|
优先用 cairosvg(与 run.py 完全一致);若运行环境缺 cairo 原生库,则回退到
|
|
|
|
|
|
Chromium canvas 渲染(透明背景、同尺寸),保证 Web 后台上传始终可用。
|
|
|
|
|
|
"""
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import base64
|
2026-09-03 15:44:57 +08:00
|
|
|
|
import logging
|
2026-09-01 13:13:40 +08:00
|
|
|
|
import re
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-09-03 15:44:57 +08:00
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import cairosvg # type: ignore
|
|
|
|
|
|
_HAS_CAIROSVG = True
|
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
|
cairosvg = None
|
|
|
|
|
|
_HAS_CAIROSVG = False
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
|
|
|
|
|
SCHEMA_SIZE = 550 # 与随包 PNG 图标一致
|
|
|
|
|
|
|
2026-09-03 00:44:38 +08:00
|
|
|
|
basic_path = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
IMG_DIR = basic_path / "res" / "img" / "helldivers"
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_filename(name: str) -> str:
|
|
|
|
|
|
"""把战备名转成安全的 Windows 文件名(保留中文/空格,去掉非法字符)。"""
|
2026-09-03 15:44:57 +08:00
|
|
|
|
s = re.sub(r'[\/:*?"<>|]', "_", name).strip()
|
2026-09-01 13:13:40 +08:00
|
|
|
|
s = re.sub(r"\s+", " ", s)
|
|
|
|
|
|
return s or "strategem"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:44:57 +08:00
|
|
|
|
async def _render_svg_canvas(svg_text: str, size: int) -> bytes:
|
|
|
|
|
|
"""用 Chromium canvas 把 SVG 光栅化为带透明通道的 PNG(兜底方案)。"""
|
2026-09-01 13:13:40 +08:00
|
|
|
|
from playwright.async_api import async_playwright
|
|
|
|
|
|
|
|
|
|
|
|
data_uri = "data:image/svg+xml;base64," + base64.b64encode(svg_text.encode("utf-8")).decode()
|
|
|
|
|
|
script = (
|
|
|
|
|
|
"() => new Promise((resolve) => {"
|
|
|
|
|
|
"const img = new Image();"
|
|
|
|
|
|
"img.onload = () => {"
|
|
|
|
|
|
"const canvas = document.createElement('canvas');"
|
|
|
|
|
|
f"canvas.width = {size}; canvas.height = {size};"
|
|
|
|
|
|
"const ctx = canvas.getContext('2d');"
|
|
|
|
|
|
f"ctx.clearRect(0,0,{size},{size});"
|
|
|
|
|
|
f"const scale = Math.min({size} / img.width, {size} / img.height);"
|
|
|
|
|
|
"const w = img.width * scale, h = img.height * scale;"
|
|
|
|
|
|
f"ctx.drawImage(img, ({size}-w)/2, ({size}-h)/2, w, h);"
|
|
|
|
|
|
"resolve(canvas.toDataURL('image/png'));"
|
|
|
|
|
|
"};"
|
|
|
|
|
|
"img.onerror = () => resolve('');"
|
|
|
|
|
|
f"img.src = '{data_uri}';"
|
|
|
|
|
|
"})"
|
|
|
|
|
|
)
|
|
|
|
|
|
async with async_playwright() as p:
|
|
|
|
|
|
browser = await p.chromium.launch()
|
|
|
|
|
|
page = await browser.new_page()
|
|
|
|
|
|
await page.set_content("<!-- svg to png -->")
|
|
|
|
|
|
data_url = await page.evaluate(script)
|
|
|
|
|
|
await browser.close()
|
2026-09-03 15:44:57 +08:00
|
|
|
|
if not data_url:
|
|
|
|
|
|
raise ValueError("SVG 转换失败")
|
2026-09-01 13:13:40 +08:00
|
|
|
|
return base64.b64decode(data_url.split(",", 1)[1])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-03 15:44:57 +08:00
|
|
|
|
async def svg_to_png(svg_text: str, size: int = SCHEMA_SIZE) -> bytes:
|
|
|
|
|
|
"""SVG -> PNG(透明背景,默认 550x550)。
|
|
|
|
|
|
|
|
|
|
|
|
参考 docs/HD2/run.py:cairosvg.svg2png 按 output_width/output_height 栅格化到目标尺寸。
|
|
|
|
|
|
"""
|
2026-09-01 13:13:40 +08:00
|
|
|
|
if not svg_text.strip():
|
|
|
|
|
|
raise ValueError("SVG 内容为空")
|
2026-09-03 15:44:57 +08:00
|
|
|
|
if _HAS_CAIROSVG:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return cairosvg.svg2png(
|
|
|
|
|
|
bytestring=svg_text.encode("utf-8"),
|
|
|
|
|
|
output_width=size,
|
|
|
|
|
|
output_height=size,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
logger.warning(f"cairosvg 转换失败,改用 canvas 兜底: {type(e).__name__}: {e}")
|
|
|
|
|
|
return await _render_svg_canvas(svg_text, size)
|
2026-09-01 13:13:40 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_icon_png(png_bytes: bytes, name: str) -> str:
|
|
|
|
|
|
"""写入 img/helldivers/<sanitized name>.png,返回相对路径。"""
|
|
|
|
|
|
IMG_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
filename = sanitize_filename(name) + ".png"
|
|
|
|
|
|
dest = IMG_DIR / filename
|
|
|
|
|
|
dest.write_bytes(png_bytes)
|
2026-09-03 15:44:57 +08:00
|
|
|
|
return f"res/img/helldivers/{filename}"
|