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()
|