项目改造

This commit is contained in:
2026-09-03 15:44:57 +08:00
parent 24d7c64e17
commit 2849a6250c
60 changed files with 166 additions and 13754 deletions
@@ -1,21 +1,30 @@
# -*- coding: utf-8 -*-
"""SVG -> PNG 转换 + 可选 HD2 风格边框,用于战备图标上传。"""
"""SVG -> PNG 转换。
与 docs/HD2/run.py 一致:把上传的单张 SVG 按输出尺寸(默认 550x550)栅格化成
透明背景 PNG,不做批量,也不叠加边框。
优先用 cairosvg(与 run.py 完全一致);若运行环境缺 cairo 原生库,则回退到
Chromium canvas 渲染(透明背景、同尺寸),保证 Web 后台上传始终可用。
"""
from __future__ import annotations
import base64
import io
import logging
import re
from pathlib import Path
from PIL import Image, ImageDraw
logger = logging.getLogger(__name__)
try:
import cairosvg # type: ignore
_HAS_CAIROSVG = True
except Exception: # noqa: BLE001
cairosvg = None
_HAS_CAIROSVG = False
SCHEMA_SIZE = 550 # 与随包 PNG 图标一致
TYPE_COLORS = {
"blue": "#6bb7ea",
"red": "#fe6d6a",
"green": "#7ecb3f",
}
basic_path = Path(__file__).resolve().parents[1]
IMG_DIR = basic_path / "res" / "img" / "helldivers"
@@ -23,49 +32,13 @@ IMG_DIR = basic_path / "res" / "img" / "helldivers"
def sanitize_filename(name: str) -> str:
"""把战备名转成安全的 Windows 文件名(保留中文/空格,去掉非法字符)。"""
s = re.sub(r'[\\/:*?"<>|]', "_", name).strip()
s = re.sub(r'[\/:*?"<>|]', "_", name).strip()
s = re.sub(r"\s+", " ", s)
return s or "strategem"
def _hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
h = hex_color.lstrip("#")
if len(h) == 3:
h = "".join(c * 2 for c in h)
return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
def _apply_frame(src_png: bytes, size: int, frame_color: str) -> bytes:
"""把透明的 SVG 画面叠到 HD2 风格画布上:彩色边框 + 深色内底 + 居中图形。"""
rgb = _hex_to_rgb(frame_color)
img = Image.open(io.BytesIO(src_png)).convert("RGBA")
canvas = Image.new("RGBA", (size, size), rgb + (255,))
draw = ImageDraw.Draw(canvas)
border = max(10, size // 20)
# 外圈用类型色,内部深色
draw.rectangle([0, 0, size - 1, size - 1], fill=rgb + (255,))
draw.rectangle([border, border, size - 1 - border, size - 1 - border], fill=(12, 12, 12, 255))
# 去掉透明边距再居中
alpha = img.getchannel("A")
bbox = alpha.getbbox()
glyph = img.crop(bbox) if bbox else img
inner = size - 2 * border
avail = int(inner * 0.82)
glyph.thumbnail((avail, avail), Image.LANCZOS)
ox = (size - glyph.width) // 2
oy = (size - glyph.height) // 2
canvas.alpha_composite(glyph, (ox, oy))
out = io.BytesIO()
canvas.save(out, format="PNG")
return out.getvalue()
async def _render_svg(svg_text: str, size: int) -> bytes:
"""用 Chromium 的 canvas 把 SVG 光栅化为带透明通道的 PNG。
用 toDataURL 导出而非截图,避免 omit_background 在部分环境把透明背景
拍成白色,从而保住透明 alpha。
"""
async def _render_svg_canvas(svg_text: str, size: int) -> bytes:
"""用 Chromium canvas 把 SVG 光栅化为带透明通道的 PNG(兜底方案)。"""
from playwright.async_api import async_playwright
data_uri = "data:image/svg+xml;base64," + base64.b64encode(svg_text.encode("utf-8")).decode()
@@ -92,17 +65,28 @@ async def _render_svg(svg_text: str, size: int) -> bytes:
await page.set_content("<!-- svg to png -->")
data_url = await page.evaluate(script)
await browser.close()
if not data_url:
raise ValueError("SVG 转换失败")
return base64.b64decode(data_url.split(",", 1)[1])
async def svg_to_png(svg_text: str, framed: bool = False, frame_color: str | None = None, size: int = SCHEMA_SIZE) -> bytes:
"""SVG -> PNG。framed=True 时叠加 HD2 风格边框到类型色。"""
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 栅格化到目标尺寸。
"""
if not svg_text.strip():
raise ValueError("SVG 内容为空")
png = await _render_svg(svg_text, size)
if framed:
png = _apply_frame(png, size, frame_color or TYPE_COLORS["blue"])
return png
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)
def save_icon_png(png_bytes: bytes, name: str) -> str:
@@ -111,4 +95,4 @@ def save_icon_png(png_bytes: bytes, name: str) -> str:
filename = sanitize_filename(name) + ".png"
dest = IMG_DIR / filename
dest.write_bytes(png_bytes)
return f"res/img/helldivers/{filename}"
return f"res/img/helldivers/{filename}"