import random
import secrets
from pathlib import Path
from typing import Any
from nonebot import require
# 顺序很重要:先 require 声明依赖,再 import(与 war_renderer 一致)
require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import get_new_page # noqa: E402
from .war_renderer import _font_faces, _logo_data_uri, _rel
basic_path = Path(__file__).resolve().parents[1]
template_path = basic_path / "res" / "templates"
img_path = basic_path / "res" / "img"
# 战备类型 -> (标签, 主题色)
TYPE_STYLE: dict[str, dict[str, str]] = {
"blue": {"label": "武装·补给", "color": "#6bb7ea"},
"red": {"label": "轨道·飞鹰", "color": "#fe6d6a"},
"green": {"label": "战术·设备", "color": "#7ecb3f"},
}
def _load_template(name: str) -> str:
"""读取 templates/ 下的模板文件(UTF-8),每次渲染实时读取,便于改模板即时生效。"""
return (template_path / name).read_text(encoding="utf-8")
def _equip_icon(equip: dict[str, Any]) -> str:
"""战备图标:读取 img/helldivers/ 下的 PNG,配合 用相对路径链接。"""
raw = equip.get("path") or ""
if not raw:
return ""
rel = raw.replace("\\", "/")
# 兼容两种路径存法:插件根相对路径 res/img/...(新/播种数据),
# 以及 res/img 目录相对路径 img/...(旧库迁移前遗留),统一找真实文件。
candidates = [rel]
if rel.startswith("res/"):
candidates.append(rel[len("res/"):])
else:
candidates.append("res/" + rel)
candidates.append("res/img/" + rel)
for cand in candidates:
p = basic_path / cand
if p.exists():
return f"
"
return ""
def _item_html(equip: dict[str, Any]) -> str:
name = equip.get("name") or "未知战备"
alias = (random.choice(equip["alias"]) if equip.get("alias") else None) or ""
type_key = equip.get("type") or ""
style = TYPE_STYLE.get(type_key, {"label": "其他", "color": "#cccccc"})
label = style["label"]
color = style["color"]
backpack = "背包位" if equip.get("backpack") else ""
alias_html = f"{alias}" if alias else ""
meta_tail = f" · {backpack}" if backpack else ""
return f"""
{_equip_icon(equip)}
{name}
{label}
{alias_html}{meta_tail}
"""
def _build_html(items: list[dict[str, Any]]) -> str:
rows = "".join(_item_html(e) for e in items)
return (
_load_template("equip_card.html")
.replace("@@BASE@@", basic_path.as_uri() + "/")
.replace("@@FONT_FACES@@", _font_faces())
.replace("@@CSS@@", _load_template("equip_card.css"))
.replace("@@LOGO@@", _logo_data_uri())
.replace("@@ROWS@@", rows)
)
async def _html_to_png(html: str, width: int = 640) -> bytes:
async with get_new_page() as page:
# 初始视口高度给一个小于内容的值,便于按 scrollHeight 收紧,避免截出多余空白
await page.set_viewport_size({"width": width, "height": 300})
# 图片/字体用相对路径 + 解析;写临时 .html 到插件根目录,
# 使页面与 img/、data/ 同目录树,file:// 同源即可加载(无需额外标志)。
tmp = basic_path / f"_equip_render_{secrets.token_hex(6)}.html"
tmp.write_text(html, encoding="utf-8")
try:
await page.goto(tmp.as_uri(), wait_until="networkidle")
# 视口高度收紧到内容实际高度,避免 full_page 截出多余空白
height = await page.evaluate("document.documentElement.scrollHeight")
await page.set_viewport_size({"width": width, "height": height})
return await page.screenshot(full_page=True, type="png")
finally:
try:
tmp.unlink()
except OSError:
pass
async def render_equipment_card(items: list[dict[str, Any]]) -> bytes:
"""把随机/组合出的战备渲染成一张 PNG 图(使用 nonebot_plugin_htmlrender)。"""
html = _build_html(items)
return await _html_to_png(html)