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)