File size: 4,621 Bytes
6e876c1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""把榜单画成品牌化 PNG,供「一键导出发微信群」。仅依赖 Pillow。

绘图与数据解耦:调用方传入已排好序的 boards(每个榜单 = 标题 + 行列表),
本模块只负责排版渲染,不碰数据库。
"""
from __future__ import annotations

import io
import os
from typing import Optional

from PIL import Image, ImageDraw, ImageFont

from .config import WWW_DIR

# 品牌色(取自队徽:藏青 / 金 / 红)
NAVY_DEEP = (10, 23, 48)
NAVY = (17, 37, 74)
PANEL = (24, 48, 92)
GOLD = (212, 175, 55)
GOLD2 = (235, 203, 107)
CREAM = (242, 236, 221)
BLUE = (61, 165, 217)
MUTED = (157, 176, 208)
LINE = (40, 70, 120)

# 候选 CJK 字体(Windows / Linux)
_FONT_DIRS = [r"C:\Windows\Fonts", "/usr/share/fonts", os.path.expanduser("~/.fonts")]
_FONT_FILES = [
    "msyhbd.ttc", "msyh.ttc", "msyh.ttf", "simhei.ttf", "simsun.ttc",
    "NotoSansCJK-Regular.ttc", "NotoSansSC-Regular.otf", "DengXian.ttf",
]


def _font_path() -> Optional[str]:
    for d in _FONT_DIRS:
        for name in _FONT_FILES:
            p = os.path.join(d, name)
            if os.path.exists(p):
                return p
    return None


_FONT_PATH = _font_path()


def _font(size: int) -> ImageFont.FreeTypeFont:
    if _FONT_PATH:
        try:
            return ImageFont.truetype(_FONT_PATH, size)
        except OSError:
            pass
    return ImageFont.load_default()


def _crest(size: int) -> Optional[Image.Image]:
    """加载队徽并裁成圆形;失败则返回 None(不阻断导出)。"""
    path = WWW_DIR / "crest.jpg"
    if not path.exists():
        return None
    try:
        im = Image.open(path).convert("RGBA").resize((size, size))
        mask = Image.new("L", (size, size), 0)
        ImageDraw.Draw(mask).ellipse((0, 0, size, size), fill=255)
        im.putalpha(mask)
        return im
    except Exception:
        return None


def render_leaderboard_png(
    team_name: str,
    season_label: str,
    date_str: str,
    boards: list[tuple[str, list[tuple[str, str, str]]]],
) -> bytes:
    """渲染榜单 PNG。

    boards: [(标题, [(名次, 名字, 数值), ...]), ...],建议 4 个榜单(2×2 网格)。
    返回 PNG 字节流。
    """
    W = 920
    margin = 40
    header_h = 132
    col_gap = 28
    row_title_h = 46
    line_h = 34
    max_rows = 8

    col_w = (W - margin * 2 - col_gap) // 2
    rows_per_board = min(
        max_rows, max((len(rows) for _, rows in boards), default=0)
    )
    board_h = row_title_h + line_h * max(1, rows_per_board) + 16
    n_board_rows = (len(boards) + 1) // 2
    H = header_h + n_board_rows * (board_h + col_gap) + margin

    img = Image.new("RGB", (W, H), NAVY_DEEP)
    d = ImageDraw.Draw(img)

    f_team = _font(40)
    f_sub = _font(18)
    f_btitle = _font(22)
    f_row = _font(20)
    f_rank = _font(20)

    # ---- 顶部渐变带 + 队名 ----
    d.rectangle((0, 0, W, header_h), fill=NAVY)
    d.line((0, header_h, W, header_h), fill=GOLD, width=2)
    x_text = margin
    crest = _crest(84)
    if crest is not None:
        img.paste(crest, (margin, 24), crest)
        x_text = margin + 84 + 20
    d.text((x_text, 30), team_name, font=f_team, fill=GOLD2)
    d.text((x_text + 2, 84), f"{season_label} · 榜单 · {date_str}", font=f_sub, fill=MUTED)

    # ---- 2×2 榜单 ----
    medals = {1: "①", 2: "②", 3: "③"}
    for idx, (title, rows) in enumerate(boards):
        r, c = divmod(idx, 2)
        x = margin + c * (col_w + col_gap)
        y = header_h + col_gap + r * (board_h + col_gap)
        # 卡片底
        d.rounded_rectangle((x, y, x + col_w, y + board_h), radius=16, fill=NAVY)
        d.rounded_rectangle(
            (x, y, x + col_w, y + board_h), radius=16, outline=LINE, width=1
        )
        # 标题
        d.text((x + 18, y + 12), title, font=f_btitle, fill=GOLD2)
        d.line((x + 18, y + 44, x + col_w - 18, y + 44), fill=LINE, width=1)
        # 行
        ry = y + row_title_h + 6
        if not rows:
            d.text((x + 18, ry), "暂无数据", font=f_row, fill=MUTED)
        for rank, name, value in rows[:max_rows]:
            rk = medals.get(int(rank) if rank.isdigit() else 0, rank)
            rk_color = GOLD if rank in ("1", "2", "3") else MUTED
            d.text((x + 18, ry), rk, font=f_rank, fill=rk_color)
            d.text((x + 56, ry), name, font=f_row, fill=CREAM)
            vw = d.textlength(value, font=f_row)
            d.text((x + col_w - 18 - vw, ry), value, font=f_row, fill=GOLD2)
            ry += line_h

    buf = io.BytesIO()
    img.save(buf, format="PNG")
    return buf.getvalue()