feat(steam-info): 资料页状态/最近游玩/ban 描述增强,时长图渲染与定时任务迁移
- steam.py / draw.py / handlers 扩充:个人资料页状态构建、最近游玩、 游戏头像与中文游戏名查询、ban 描述 - 定时播报迁入 hexi.core.scheduler - 新增 tests/test_steam_profile_parse.py(资料 HTML/XML 解析) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
0ac1958932
commit
6af60a7d55
6 files changed
+1298
-125
No files matched your search
@@ -2,12 +2,12 @@ import numpy as np
|
||||
from functools import lru_cache
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Tuple
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
from colorsys import rgb_to_hsv, hsv_to_rgb
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
|
||||
|
||||
from .utils import hex_to_rgb
|
||||
from .models import DrawPlayerStatusData, Achievements
|
||||
from .models import DrawPlayerStatusData, Achievements, ProfileCardExtra
|
||||
|
||||
|
||||
WIDTH = 400
|
||||
@@ -824,6 +824,7 @@ def draw_game_info(
|
||||
completed_achievement_number: int,
|
||||
total_achievement_number: int,
|
||||
achievement_color: Tuple[int, int, int],
|
||||
two_week_time: Optional[str] = None,
|
||||
) -> Image.Image:
|
||||
bg = Image.new("RGBA", (880, 110 + 64 + 10), (0, 0, 0, 110))
|
||||
header = header.resize((229, 86), Image.BICUBIC)
|
||||
@@ -831,11 +832,27 @@ def draw_game_info(
|
||||
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
# 近两周时长(可选):右上角绿色小字,游戏名超宽时先截断避让
|
||||
two_week_width = 0
|
||||
if two_week_time:
|
||||
font_two_week = get_font(font_light_path, 20)
|
||||
two_week_width = int(draw.textlength(two_week_time, font=font_two_week))
|
||||
draw.text(
|
||||
(bg.width - two_week_width - 10, 22),
|
||||
two_week_time,
|
||||
font=font_two_week,
|
||||
fill=hex_to_rgb("91c257"),
|
||||
)
|
||||
|
||||
# 画游戏名
|
||||
name_font = get_font(font_regular_path, 26)
|
||||
max_name_width = bg.width - 260 - 20 - two_week_width - (30 if two_week_width else 0)
|
||||
if draw.textlength(game_name, font=name_font) > max_name_width:
|
||||
game_name = _truncate_with_ellipsis(draw, game_name, name_font, max_name_width)
|
||||
draw.text(
|
||||
(260, 10),
|
||||
game_name,
|
||||
font=get_font(font_regular_path, 26),
|
||||
font=name_font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
|
||||
@@ -880,7 +897,7 @@ def draw_game_info(
|
||||
(int(x), 20),
|
||||
f"{completed_achievement_number} / {total_achievement_number}",
|
||||
font=font,
|
||||
fill=(130, 130, 130),
|
||||
fill=(185, 185, 185),
|
||||
)
|
||||
x += (
|
||||
font.getlength(f"{completed_achievement_number} / {total_achievement_number}")
|
||||
@@ -913,168 +930,413 @@ def draw_game_info(
|
||||
return bg
|
||||
|
||||
|
||||
# 信息卡尺寸/排版常量(调整版式优先动这里)
|
||||
CARD_WIDTH = 1200 # 卡片条宽度,背景按此居中裁切
|
||||
CARD_MARGIN = 20 # 左右安全边距
|
||||
LEFT_W = 880 # 左栏(游戏动态)宽度
|
||||
RIGHT_X = CARD_MARGIN + LEFT_W + CARD_MARGIN # 920:右栏起点
|
||||
RIGHT_W = CARD_WIDTH - RIGHT_X - CARD_MARGIN # 260:右栏宽度
|
||||
AVATAR_SIZE = 160
|
||||
AVATAR_X = 30 # 头像框约 1.36×头像,需留出外溢空间
|
||||
HEADER_TEXT_X = AVATAR_X + AVATAR_SIZE + 30 # 220:头部文字统一左对齐点
|
||||
GAME_GAP = 16 # 游戏卡片间距
|
||||
COUNT_ROW_MAX_STEP = 44 # 右栏计数条目间距上限(面板矮时自动收紧)
|
||||
AVATAR_FRAME_SCALE = 1.36 # 头像框相对头像的缩放(对齐 Steam 内孔比例)
|
||||
|
||||
# 状态行配色,key 为 ProfileCardExtra.status_kind
|
||||
status_row_colors = {
|
||||
"in-game": hex_to_rgb("91c257"),
|
||||
"online": hex_to_rgb("6dcef5"),
|
||||
"away": hex_to_rgb("e0a948"),
|
||||
"offline": hex_to_rgb("969697"),
|
||||
"private": hex_to_rgb("969697"),
|
||||
}
|
||||
|
||||
# 风控标记配色,key 为 ProfileCardExtra.ban_kind
|
||||
ban_tag_colors = {
|
||||
"vac": hex_to_rgb("c4453c"),
|
||||
"trade": hex_to_rgb("c98a2e"),
|
||||
"limited": hex_to_rgb("67665c"),
|
||||
}
|
||||
|
||||
# 右栏状态区标题,key 为 ProfileCardExtra.status_kind
|
||||
status_titles = {
|
||||
"in-game": "当前正在游戏",
|
||||
"online": "当前在线",
|
||||
"away": "当前在线",
|
||||
"offline": "当前离线",
|
||||
"private": "当前状态",
|
||||
}
|
||||
|
||||
|
||||
def _draw_kv_parts(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
x: int,
|
||||
y: int,
|
||||
parts: List[Tuple[str, tuple]],
|
||||
font,
|
||||
) -> int:
|
||||
"""按 (文本, 颜色) 依次绘制,返回右边缘 x(间距由文本自带的空格控制)"""
|
||||
for text, color in parts:
|
||||
draw.text((x, y), text, font=font, fill=color)
|
||||
x += int(draw.textlength(text, font=font))
|
||||
return x
|
||||
|
||||
|
||||
def _draw_status_row(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
x: int,
|
||||
y: int,
|
||||
text: str,
|
||||
kind: str = "online",
|
||||
second_line: str = "",
|
||||
) -> None:
|
||||
"""状态行:彩色圆点 + 文案(游戏中/在线/离线/资料私密),过长可折第二行"""
|
||||
color = status_row_colors.get(kind, status_row_colors["online"])
|
||||
font = get_font(font_regular_path, 17)
|
||||
draw.ellipse((x, y + 4, x + 11, y + 15), fill=color)
|
||||
draw.text((x + 19, y), text, font=font, fill=color)
|
||||
if second_line:
|
||||
draw.text((x + 19, y + 22), second_line, font=font, fill=color)
|
||||
|
||||
|
||||
def _split_two_lines(
|
||||
draw: ImageDraw.ImageDraw, text: str, font, max_width: int
|
||||
) -> List[str]:
|
||||
"""超宽文本折成最多两行:优先在最后一个放得下的分隔符处断,否则省略号截断"""
|
||||
if draw.textlength(text, font=font) <= max_width:
|
||||
return [text]
|
||||
break_at = None
|
||||
for idx, char in enumerate(text):
|
||||
if char in "::" or char == " ":
|
||||
if draw.textlength(text[: idx + 1], font=font) <= max_width:
|
||||
break_at = idx + 1
|
||||
if break_at is None:
|
||||
return [_truncate_with_ellipsis(draw, text, font, max_width)]
|
||||
second = text[break_at:].strip()
|
||||
if draw.textlength(second, font=font) > max_width:
|
||||
second = _truncate_with_ellipsis(draw, second, font, max_width)
|
||||
return [text[:break_at].strip(), second]
|
||||
|
||||
|
||||
def _draw_level_badge(
|
||||
draw: ImageDraw.ImageDraw, x: int, center_y: int, level: int
|
||||
) -> int:
|
||||
"""Steam 风格等级圆牌,返回右边缘 x"""
|
||||
diameter = 34
|
||||
top = center_y - diameter // 2
|
||||
draw.ellipse(
|
||||
(x, top, x + diameter, top + diameter),
|
||||
fill=hex_to_rgb("1b2838"),
|
||||
outline=hex_to_rgb("66c0f4"),
|
||||
width=2,
|
||||
)
|
||||
font = get_font(font_bold_path, 18)
|
||||
text = str(level)
|
||||
text_width = draw.textlength(text, font=font)
|
||||
draw.text(
|
||||
(x + (diameter - text_width) / 2, top + (diameter - font.size) / 2 - 3),
|
||||
text,
|
||||
font=font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
return x + diameter
|
||||
|
||||
|
||||
def _draw_ban_tag(
|
||||
draw: ImageDraw.ImageDraw, x: int, y: int, text: str, kind: str
|
||||
) -> int:
|
||||
"""风控标记圆角标签,返回右边缘 x"""
|
||||
font = get_font(font_regular_path, 13)
|
||||
padding_x, padding_y = 9, 4
|
||||
width = draw.textlength(text, font=font) + padding_x * 2
|
||||
height = font.size + padding_y * 2
|
||||
draw.rounded_rectangle(
|
||||
(x, y, x + width, y + height),
|
||||
radius=height // 2,
|
||||
fill=ban_tag_colors.get(kind, ban_tag_colors["limited"]),
|
||||
)
|
||||
draw.text(
|
||||
(x + padding_x, y + padding_y - 1),
|
||||
text,
|
||||
font=font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
return int(x + width)
|
||||
|
||||
|
||||
def draw_player_status(
|
||||
player_bg: Image.Image,
|
||||
player_avatar: Image.Image,
|
||||
player_name: str,
|
||||
player_id: str,
|
||||
player_description: str,
|
||||
player_last_two_weeks_time: str, # e.g. 10.2 小时
|
||||
player_last_two_weeks_time: str, # e.g. 10.2 小时(过去 2 周)
|
||||
player_games: List[DrawPlayerStatusData],
|
||||
extra: Optional[ProfileCardExtra] = None,
|
||||
avatar_frame: Optional[bytes] = None,
|
||||
):
|
||||
"""信息卡:通栏头部 + 左栏「游戏动态」+ 右栏「当前在线/计数」(参考 Steam 官方资料页)"""
|
||||
if isinstance(player_bg, bytes):
|
||||
player_bg = Image.open(BytesIO(player_bg))
|
||||
if isinstance(player_avatar, bytes):
|
||||
player_avatar = Image.open(BytesIO(player_avatar))
|
||||
extra = extra or {}
|
||||
|
||||
bg = recolor_image(
|
||||
player_bg.crop(
|
||||
(
|
||||
(player_bg.width - 960) // 2,
|
||||
(player_bg.width - CARD_WIDTH) // 2,
|
||||
0,
|
||||
(player_bg.width + 960) // 2,
|
||||
(player_bg.width + CARD_WIDTH) // 2,
|
||||
player_bg.height,
|
||||
)
|
||||
),
|
||||
10,
|
||||
10,
|
||||
)
|
||||
# 调暗背景
|
||||
enhancer = ImageEnhance.Brightness(bg)
|
||||
bg = enhancer.enhance(0.7)
|
||||
# bg.size = (960, 1020)
|
||||
player_avatar = player_avatar.resize((200, 200))
|
||||
bg.paste(player_avatar, (40, 40))
|
||||
|
||||
bg = ImageEnhance.Brightness(bg).enhance(0.7)
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
# 画头像外框
|
||||
draw.rectangle((40, 40, 240, 240), outline=(83, 164, 196), width=3)
|
||||
# ---------- 头部:头像(+头像框)/ 昵称+等级+风控 / 简介 / 好友代码·注册时间 ----------
|
||||
avatar = player_avatar.convert("RGB").resize(
|
||||
(AVATAR_SIZE, AVATAR_SIZE), Image.BICUBIC
|
||||
)
|
||||
bg.paste(avatar_with_border(avatar), (AVATAR_X - 2, AVATAR_X - 2))
|
||||
if avatar_frame:
|
||||
try:
|
||||
frame = Image.open(BytesIO(avatar_frame)).convert("RGBA")
|
||||
frame_size = int(AVATAR_SIZE * AVATAR_FRAME_SCALE)
|
||||
frame = frame.resize((frame_size, frame_size), Image.LANCZOS)
|
||||
frame_pos = AVATAR_X + (AVATAR_SIZE - frame_size) // 2
|
||||
bg.paste(frame, (frame_pos, frame_pos), frame)
|
||||
except Exception:
|
||||
pass # 头像框只是装饰,画不出来不影响整卡
|
||||
|
||||
# 画昵称
|
||||
# 好友代码/注册时间的标签色:比正文灰稍亮一档(#969696 在深色底上偏暗)
|
||||
text_gray = hex_to_rgb("b4b4b4")
|
||||
name_font = get_font(font_light_path, 38)
|
||||
reserved = 34 + 10 if extra.get("level") is not None else 0
|
||||
if extra.get("ban_text"):
|
||||
ban_font = get_font(font_regular_path, 13)
|
||||
reserved += int(draw.textlength(extra["ban_text"], font=ban_font)) + 18 + 10
|
||||
max_name_width = CARD_WIDTH - CARD_MARGIN - HEADER_TEXT_X - reserved
|
||||
if draw.textlength(player_name, font=name_font) > max_name_width:
|
||||
player_name = _truncate_with_ellipsis(
|
||||
draw, player_name, name_font, max_name_width
|
||||
)
|
||||
name_y = AVATAR_X + 6
|
||||
draw.text(
|
||||
(280, 48),
|
||||
player_name,
|
||||
font=get_font(font_light_path, 40),
|
||||
fill=(255, 255, 255),
|
||||
(HEADER_TEXT_X, name_y), player_name, font=name_font, fill=(255, 255, 255)
|
||||
)
|
||||
|
||||
# 画ID
|
||||
draw.text(
|
||||
(280, 100),
|
||||
f"好友代码: {player_id}",
|
||||
font=get_font(font_regular_path, 19),
|
||||
fill=(191, 191, 191),
|
||||
)
|
||||
# 等级圆牌 / 风控标记紧随昵称
|
||||
cursor = HEADER_TEXT_X + int(draw.textlength(player_name, font=name_font)) + 12
|
||||
if extra.get("level") is not None:
|
||||
cursor = _draw_level_badge(draw, cursor, name_y + 21, extra["level"]) + 10
|
||||
if extra.get("ban_text"):
|
||||
_draw_ban_tag(
|
||||
draw, cursor, name_y + 10, extra["ban_text"], extra.get("ban_kind") or "limited"
|
||||
)
|
||||
|
||||
# 画简介
|
||||
# 简介(最多 3 行)
|
||||
desc_font = get_font(font_light_path, 20)
|
||||
desc_top = name_y + 60
|
||||
max_desc_width = CARD_WIDTH - CARD_MARGIN - HEADER_TEXT_X
|
||||
line_width = 0
|
||||
offset = 0
|
||||
line = ""
|
||||
for idx, char in enumerate(player_description):
|
||||
line += char
|
||||
line_width += get_font(font_light_path, 22).getlength(char)
|
||||
if line_width > 640 or idx == len(player_description) - 1 or char == "\n":
|
||||
line_width += desc_font.getlength(char)
|
||||
if (
|
||||
line_width > max_desc_width
|
||||
or idx == len(player_description) - 1
|
||||
or char == "\n"
|
||||
):
|
||||
draw.text(
|
||||
(280, 132 + offset),
|
||||
(HEADER_TEXT_X, desc_top + offset),
|
||||
line,
|
||||
font=get_font(font_light_path, 22),
|
||||
fill=(255, 255, 255),
|
||||
font=desc_font,
|
||||
fill=(235, 235, 235),
|
||||
)
|
||||
line = ""
|
||||
offset += 25
|
||||
line_width = 0
|
||||
if offset >= 25 * 4:
|
||||
offset += 27
|
||||
if offset >= 27 * 3:
|
||||
break
|
||||
|
||||
# 画游戏
|
||||
# 好友代码 / 注册时间
|
||||
meta_font = get_font(font_regular_path, 16)
|
||||
meta_y = desc_top + offset + 14
|
||||
meta_parts = [("好友代码: ", text_gray), (player_id, (255, 255, 255))]
|
||||
if extra.get("member_since"):
|
||||
meta_parts += [
|
||||
(" 注册时间: ", text_gray),
|
||||
(extra["member_since"], (255, 255, 255)),
|
||||
]
|
||||
_draw_kv_parts(draw, HEADER_TEXT_X, meta_y, meta_parts, meta_font)
|
||||
|
||||
header_h = max(AVATAR_X + AVATAR_SIZE + 26, meta_y + 22) + 14
|
||||
draw.rectangle(
|
||||
(CARD_MARGIN, header_h - 1, CARD_WIDTH - CARD_MARGIN, header_h),
|
||||
fill=hex_to_rgb("333439"),
|
||||
)
|
||||
|
||||
# ---------- 左栏:游戏动态 ----------
|
||||
body_top = header_h + 16
|
||||
brightest_color, darkest_color = get_brightest_and_darkest_color(player_bg)
|
||||
brightest_color = tuple(map(lambda x: x - 30 if x >= 30 else 0, brightest_color))
|
||||
darkest_color = tuple(
|
||||
map(lambda x: x + 30 if x <= 255 - 30 else 255, darkest_color)
|
||||
)
|
||||
brightest_color = (brightest_color[0], brightest_color[1], brightest_color[2], 128)
|
||||
brightest_color = random_color_offset(brightest_color, 20)
|
||||
darkest_color = (darkest_color[0], darkest_color[1], darkest_color[2], 128)
|
||||
darkest_color = random_color_offset(darkest_color, 20)
|
||||
|
||||
# 画游戏信息
|
||||
hsv_achievement_color = rgb_to_hsv(*brightest_color[:3])
|
||||
# 成就条底色:取背景亮色的深色变体(压暗一些,保证白色文字和数字读得清)
|
||||
hsv_achievement_color = rgb_to_hsv(*brightest_color)
|
||||
achievement_color = tuple(
|
||||
map(
|
||||
int,
|
||||
hsv_to_rgb(
|
||||
hsv_achievement_color[0],
|
||||
hsv_achievement_color[1] * 0.85,
|
||||
hsv_achievement_color[2] * 0.6,
|
||||
hsv_achievement_color[1] * 0.8,
|
||||
hsv_achievement_color[2] * 0.35,
|
||||
),
|
||||
)
|
||||
)
|
||||
game_images: List[Image.Image] = []
|
||||
for idx, game in enumerate(player_games):
|
||||
game_image = Image.open(BytesIO(game["game_header"]))
|
||||
game_info = draw_game_info(
|
||||
game_image,
|
||||
game["game_name"],
|
||||
game["game_time"],
|
||||
game["last_play_time"],
|
||||
game["achievements"],
|
||||
game["completed_achievement_number"],
|
||||
game["total_achievement_number"],
|
||||
achievement_color,
|
||||
|
||||
cards: List[Image.Image] = []
|
||||
for game in player_games:
|
||||
cards.append(
|
||||
draw_game_info(
|
||||
Image.open(BytesIO(game["game_header"])),
|
||||
game["game_name"],
|
||||
game["game_time"],
|
||||
game["last_play_time"],
|
||||
game["achievements"],
|
||||
game["completed_achievement_number"],
|
||||
game["total_achievement_number"],
|
||||
achievement_color,
|
||||
two_week_time=game.get("two_week_time"),
|
||||
)
|
||||
)
|
||||
game_images.append(game_info)
|
||||
|
||||
# 画半透明黑色背景
|
||||
bg_game = Image.new(
|
||||
"RGBA", (920, 106 + sum([game_image.height + 26 for game_image in game_images]))
|
||||
# 背景太矮时先向下补一段(拉伸底部横条),避免内容整体被裁
|
||||
min_height = body_top + 58 + 110 + CARD_MARGIN
|
||||
if bg.height < min_height:
|
||||
strip = bg.crop((0, bg.height - 8, CARD_WIDTH, bg.height)).resize(
|
||||
(CARD_WIDTH, min_height - bg.height), Image.BICUBIC
|
||||
)
|
||||
canvas = Image.new("RGB", (CARD_WIDTH, min_height))
|
||||
canvas.paste(bg, (0, 0))
|
||||
canvas.paste(strip, (0, bg.height))
|
||||
bg = canvas
|
||||
draw = ImageDraw.Draw(bg)
|
||||
|
||||
# 按可用高度筛出画得下的游戏卡片(至少画第一条)
|
||||
y = body_top + 58
|
||||
max_bottom = bg.height - CARD_MARGIN
|
||||
shown_cards: List[Image.Image] = []
|
||||
for card in cards:
|
||||
if shown_cards and y + card.height > max_bottom:
|
||||
break
|
||||
shown_cards.append(card)
|
||||
y += card.height + GAME_GAP
|
||||
left_bottom = (y - GAME_GAP + 12) if shown_cards else (body_top + 66)
|
||||
|
||||
# 左右两栏等高的半透明底板
|
||||
left_panel = Image.new("RGBA", (LEFT_W, left_bottom - body_top), (0, 0, 0, 120))
|
||||
bg.paste(left_panel, (CARD_MARGIN, body_top), left_panel)
|
||||
right_panel = Image.new("RGBA", (RIGHT_W, left_bottom - body_top), (0, 0, 0, 120))
|
||||
bg.paste(right_panel, (RIGHT_X, body_top), right_panel)
|
||||
|
||||
# 左栏标题条(渐变)+ 近两周总时长
|
||||
gradient = create_gradient_image(
|
||||
(LEFT_W, 50),
|
||||
(brightest_color[0], brightest_color[1], brightest_color[2], 128),
|
||||
(darkest_color[0], darkest_color[1], darkest_color[2], 128),
|
||||
)
|
||||
draw_game = ImageDraw.Draw(bg_game)
|
||||
draw_game.rectangle(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
920,
|
||||
bg_game.height,
|
||||
),
|
||||
fill=(0, 0, 0, 120),
|
||||
)
|
||||
bg.paste(bg_game, (20, 272), bg_game)
|
||||
|
||||
# 画渐变条
|
||||
gradient = create_gradient_image((920, 50), brightest_color, darkest_color)
|
||||
bg.paste(gradient, (20, 272), gradient)
|
||||
|
||||
# 画渐变条的文字:最新动态,最近游戏
|
||||
bg.paste(gradient, (CARD_MARGIN, body_top), gradient)
|
||||
draw.text(
|
||||
(34, 279),
|
||||
"最新动态",
|
||||
(CARD_MARGIN + 14, body_top + 7),
|
||||
"游戏动态",
|
||||
font=get_font(font_light_path, 26),
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
if player_last_two_weeks_time is not None:
|
||||
width = get_font(font_light_path, 26).getlength(
|
||||
player_last_two_weeks_time
|
||||
)
|
||||
if player_last_two_weeks_time:
|
||||
total_font = get_font(font_light_path, 22)
|
||||
total_width = total_font.getlength(player_last_two_weeks_time)
|
||||
draw.text(
|
||||
(960 - width - 34, 279),
|
||||
(CARD_MARGIN + LEFT_W - total_width - 14, body_top + 9),
|
||||
player_last_two_weeks_time,
|
||||
font=get_font(font_light_path, 26),
|
||||
font=total_font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
y = body_top + 58
|
||||
for card in shown_cards:
|
||||
bg.paste(card, (CARD_MARGIN, y), card.convert("RGBA"))
|
||||
y += card.height + GAME_GAP
|
||||
|
||||
y = 350
|
||||
for idx, game_image in enumerate(game_images):
|
||||
bg.paste(
|
||||
game_image,
|
||||
((920 - game_image.width) // 2 + 20, y),
|
||||
game_image.convert("RGBA"),
|
||||
# ---------- 右栏:当前在线 + 计数 ----------
|
||||
status_title = status_titles.get(
|
||||
extra.get("status_kind") or "online", "当前在线"
|
||||
)
|
||||
draw.text(
|
||||
(RIGHT_X + 20, body_top + 14),
|
||||
status_title,
|
||||
font=get_font(font_light_path, 20),
|
||||
fill=hex_to_rgb("c5d6d4"),
|
||||
)
|
||||
status_extra_h = 0
|
||||
if extra.get("status_text"):
|
||||
status_font = get_font(font_regular_path, 17)
|
||||
status_lines = _split_two_lines(
|
||||
draw, extra["status_text"], status_font, RIGHT_W - 60
|
||||
)
|
||||
y += game_image.height + 26
|
||||
status_extra_h = 22 * (len(status_lines) - 1)
|
||||
_draw_status_row(
|
||||
draw,
|
||||
RIGHT_X + 20,
|
||||
body_top + 46,
|
||||
status_lines[0],
|
||||
extra.get("status_kind") or "online",
|
||||
status_lines[1] if len(status_lines) > 1 else "",
|
||||
)
|
||||
divider_y = body_top + 76 + status_extra_h
|
||||
draw.rectangle(
|
||||
(RIGHT_X + 20, divider_y, RIGHT_X + RIGHT_W - 20, divider_y + 1),
|
||||
fill=hex_to_rgb("333439"),
|
||||
)
|
||||
|
||||
player_bg.paste(bg, ((player_bg.width - 960) // 2, 0), bg.convert("RGBA"))
|
||||
count_rows = list((extra.get("counts") or {}).items())
|
||||
panel_h = left_bottom - body_top - status_extra_h
|
||||
step = COUNT_ROW_MAX_STEP
|
||||
if count_rows:
|
||||
step = min(COUNT_ROW_MAX_STEP, max(26, (panel_h - 114) // len(count_rows)))
|
||||
row_font = get_font(font_regular_path, 15)
|
||||
ry = divider_y + 14
|
||||
for label, value in count_rows:
|
||||
if ry + 20 > left_bottom - 12: # 面板放不下就不画,保证不越界
|
||||
break
|
||||
draw.text((RIGHT_X + 20, ry), label, font=row_font, fill=hex_to_rgb("8f989b"))
|
||||
value_text = str(value)
|
||||
draw.text(
|
||||
(RIGHT_X + RIGHT_W - 20 - row_font.getlength(value_text), ry),
|
||||
value_text,
|
||||
font=row_font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
ry += step
|
||||
|
||||
# 内容高于背景时(极端情况),背景向下拉伸补足后再贴回
|
||||
if bg.height > player_bg.height:
|
||||
strip = player_bg.crop(
|
||||
(0, player_bg.height - 8, player_bg.width, player_bg.height)
|
||||
).resize((player_bg.width, bg.height - player_bg.height), Image.BICUBIC)
|
||||
canvas = Image.new("RGB", (player_bg.width, bg.height))
|
||||
canvas.paste(player_bg, (0, 0))
|
||||
canvas.paste(strip, (0, player_bg.height))
|
||||
player_bg = canvas
|
||||
|
||||
player_bg.paste(bg, ((player_bg.width - CARD_WIDTH) // 2, 0), bg.convert("RGBA"))
|
||||
return player_bg
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user