Files
starbot/starbot/painter/DynamicPicGenerator.py
2024-10-26 12:36:08 -04:00

860 lines
35 KiB
Python

import asyncio
import json
import os
from typing import Optional, Union, Tuple, List, Dict, Any
from PIL import Image, ImageDraw, ImageFont
from PIL.Image import Resampling
from emoji import is_emoji
from .PicGenerator import Color, PicGenerator
from ..utils import config
from ..utils.network import request
from ..utils.utils import open_url_image, timestamp_format, split_list, limit_str_length, \
mask_round, mask_rounded_rectangle, get_credential
class DynamicPicGenerator:
"""
动态图片生成器
"""
__resource_base_path = os.path.dirname(os.path.dirname(__file__))
@classmethod
async def generate(cls, param: Dict[str, Any]) -> str:
"""
根据传入动态信息生成动态图片
Args:
param: 动态信息
Returns:
动态图片的 Base64 字符串
"""
width = 740
height = 100000
text_margin = 25
img_margin = 10
generator = PicGenerator(width, height)
pic = generator.set_pos(175, 60).draw_rounded_rectangle(0, 0, width, height, 35, Color.WHITE).copy_bottom(35)
# 提取参数
desc = param["desc"]
dynamic_id = desc["dynamic_id"]
origin_dynamic_id = desc["orig_dy_id"] if "orig_dy_id" in desc else None
dynamic_type = desc["type"]
user_profile = desc["user_profile"]
card = json.loads(param["card"])
display = param["display"]
# 动态头部
face, pendant = await asyncio.gather(open_url_image(user_profile["info"]["face"]),
open_url_image(user_profile["pendant"]["image"]))
official = user_profile["card"]["official_verify"]["type"]
vip = user_profile["vip"]["nickname_color"] != ""
uname = user_profile["info"]["uname"]
timestamp = desc["timestamp"]
await cls.__draw_header(pic, face, pendant, official, vip, uname, timestamp)
# 动态主体
pic.set_pos(x=text_margin)
pic.set_row_space(10)
await cls.__draw_by_type(pic, dynamic_type, card, dynamic_id, display,
text_margin, img_margin, False, origin_dynamic_id)
# 底部版权信息,请务必保留此处
pic.move_pos(0, 15)
pic.draw_text_right(25, "Designed By StarBot", Color.GRAY)
pic.draw_text_right(25, "https://github.com/Starlwr/StarBot", Color.LINK)
pic.draw_text_right(25, "本Bot由薛定谔的大喵维护", Color.LIGHTBLUE)
pic.draw_text_right(25, "https://gitea.phywyj.dynv6.net/wyj/starbot", Color.LINK)
pic.crop_and_paste_bottom()
return pic.base64()
@classmethod
def __remove_illegal_char(cls, s: str) -> str:
"""
移除动态中的非法字符
Args:
s: 源字符串
Returns:
移除非法字符后的字符串
"""
return s.replace(chr(8203), "").replace(chr(65039), "")
@classmethod
async def __draw_header(cls,
pic: PicGenerator,
face: Image.Image,
pendant: Image.Image,
official: int,
vip: bool,
uname: str,
timestamp: int) -> PicGenerator:
"""
绘制动态头部
Args:
pic: 绘图器实例
face: 头像图片
pendant: 头像挂件图片
official: 认证类型
vip: 是否为大会员
uname: 昵称
timestamp: 动态时间戳
"""
face_size = (100, 100)
face = face.resize(face_size, Resampling.LANCZOS).convert("RGBA")
face = mask_round(face)
pic.draw_img_alpha(face, (50, 50))
if pendant is not None:
pendant_size = (170, 170)
pendant = pendant.resize(pendant_size, Resampling.LANCZOS).convert('RGBA')
pic.draw_img_alpha(pendant, (15, 15))
pic.move_pos(15, 0)
if official == 0:
pic.draw_img_alpha(Image.open(f"{cls.__resource_base_path}/resource/personal.png"), (118, 118))
elif official == 1:
pic.draw_img_alpha(Image.open(f"{cls.__resource_base_path}/resource/business.png"), (118, 118))
elif vip:
pic.draw_img_alpha(Image.open(f"{cls.__resource_base_path}/resource/vip.png"), (118, 118))
if vip:
pic.draw_text(uname, Color.PINK)
else:
pic.draw_text(uname, Color.BLACK)
pic.draw_tip(timestamp_format(timestamp, "%Y-%m-%d %H:%M"))
pic.set_pos(y=200)
return pic
@classmethod
async def __draw_by_type(cls,
pic: PicGenerator,
dynamic_type: int,
card: Dict[str, Any],
dynamic_id: int,
display: Dict[str, Any],
text_margin: int,
img_margin: int,
forward: bool,
origin_dynamic_id: Optional[int] = None):
"""
根据动态类型绘制动态图片
Args:
pic: 绘图器实例
dynamic_type: 动态类型
card: 动态信息
dynamic_id: 动态 ID
display: 动态绘制附加信息
text_margin: 文字外边距
img_margin: 图片外边距
forward: 当前是否为转发动态的源动态
"""
async def download_img(mod: Dict[str, Any]):
"""
下载表情图片
Args:
mod: 表情区块字典
"""
mod["img"] = await open_url_image(mod["emoji"]["icon_url"])
if dynamic_type == 2 or dynamic_type == 4:
# 有些动态带有标题,不显示标题会缺少上下文
modules_url =f"https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?timezone_offset=-480&id={dynamic_id}&features=itemOpusStyle,opusBigCover,onlyfansVote"
modules = (await request("GET", modules_url, credential=get_credential()))["item"]["modules"]["module_dynamic"]["major"]["opus"]
title = modules["title"]
modules = modules["summary"]
if modules:
modules = modules["rich_text_nodes"]
if title is not None:
modules.insert(0, {
'orig_text': f"{title}\n",
'text': f"{title}\n",
'type': 'RICH_TEXT_NODE_TYPE_TEXT'
})
else:
modules = []
else :
modules_url = f"https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?timezone_offset=-480&id={dynamic_id}"
modules = (await request("GET", modules_url, credential=get_credential()))["item"]["modules"]["module_dynamic"]["desc"]
modules = modules["rich_text_nodes"] if modules else []
# 下载表情
download_picture_tasks = []
for module in modules:
if module["type"] == "RICH_TEXT_NODE_TYPE_EMOJI":
download_picture_tasks.append(download_img(module))
await asyncio.gather(*download_picture_tasks)
if dynamic_type == 1:
# 转发动态
await cls.__draw_content(pic, modules, text_margin, forward)
if "origin" in card:
origin = json.loads(card["origin"])
origin_type = card["item"]["orig_type"]
origin_display = display["origin"]
origin_name = card["origin_user"]["info"]["uname"]
origin_name_at_param = [{"type": "RICH_TEXT_NODE_TYPE_AT", "text": f"@{origin_name}"}]
await cls.__draw_content(pic, origin_name_at_param, text_margin, True)
await cls.__draw_by_type(pic, origin_type, origin, origin_dynamic_id, origin_display,
text_margin, img_margin, True)
elif dynamic_type == 2:
# 带图动态
await cls.__draw_content(pic, modules, text_margin, forward)
await cls.__draw_picture_area(pic, card["item"]["pictures"], img_margin, forward)
elif dynamic_type == 4:
# 纯文字动态
await cls.__draw_content(pic, modules, text_margin, forward)
elif dynamic_type == 8:
# 视频
await cls.__draw_content(pic, modules, text_margin, forward)
await cls.__draw_video_cover(pic, card["pic"], card["duration"], img_margin, forward)
title_param = [{"type": "RICH_TEXT_NODE_TYPE_TEXT", "text": card["title"]}]
await cls.__draw_content(pic, title_param, img_margin, forward)
elif dynamic_type == 64:
# 专栏
title_param = [{"type": "RICH_TEXT_NODE_TYPE_TEXT", "text": card["title"]}]
await cls.__draw_content(pic, title_param, text_margin, forward)
await cls.__draw_article_cover(pic, card["origin_image_urls"], img_margin, forward)
summary = limit_str_length(card['summary'].replace("\n", " "), 60)
summary_param = [{"type": "RICH_TEXT_NODE_TYPE_TEXT", "text": summary}]
await cls.__draw_content(pic, summary_param, text_margin, forward)
elif dynamic_type == 256:
# 音频
await cls.__draw_content(pic, modules, text_margin, forward)
title = limit_str_length(card["title"], 15)
await cls.__draw_audio_area(pic, card["cover"], title, card["typeInfo"], text_margin, forward)
elif dynamic_type == 2048:
# 分享
await cls.__draw_content(pic, modules, text_margin, forward)
title = limit_str_length(card["sketch"]["title"], 15)
desc = limit_str_length(card["sketch"]["desc_text"], 18)
await cls.__draw_share_area(pic, card["sketch"]["cover_url"], title, desc, text_margin, forward)
elif dynamic_type == 4200:
# 直播
title = limit_str_length(card["title"], 14)
desc = f"{card['area_v2_name']} · {card['watched_show']}"
await cls.__draw_live_area(pic, card["cover"], title, desc, text_margin, forward)
elif dynamic_type == 4300:
# 收藏
title = limit_str_length(card["title"], 14)
desc = limit_str_length(f"{card['media_count']}个内容", 17)
await cls.__draw_live_area(pic, card["cover"], title, desc, text_margin, forward)
elif dynamic_type == 4308:
# 直播
base = card["live_play_info"]
title = limit_str_length(base["title"], 14)
desc = f"{base['area_name']} {base['online']}人气"
await cls.__draw_live_area(pic, base["cover"], title, desc, text_margin, forward)
else:
notice_param = [{"type": "RICH_TEXT_NODE_TYPE_TEXT", "text": "暂不支持的动态类型"}]
await cls.__draw_content(pic, notice_param, text_margin, forward)
# 附加卡片
add_on_card = display["add_on_card_info"] if "add_on_card_info" in display else []
await cls.__draw_add_on_card(pic, add_on_card, text_margin, forward)
@classmethod
async def __get_content_line_imgs(cls, modules: List[Dict[str, Any]], width: int) -> List[Image.Image]:
"""
逐行绘制动态内容主体
Args:
modules: 动态内容各区块信息列表
width: 每行最大宽度
Returns:
绘制好的每行文字的透明背景图片列表
"""
imgs = []
if not modules:
return imgs
line_height = 40
text_img_size = (40, 40)
img = Image.new("RGBA", (width, line_height))
draw = ImageDraw.Draw(img)
normal_font = config.get("PAINTER_NORMAL_FONT")
font = ImageFont.truetype(f"{cls.__resource_base_path}/resource/{normal_font}", 30)
emoji_font = ImageFont.truetype(f"{cls.__resource_base_path}/resource/emoji.ttf", 109)
x, y = 0, 0
def next_line():
"""
换行
"""
nonlocal img, draw, x, y
imgs.append(img)
img = Image.new("RGBA", (width, line_height))
draw = ImageDraw.Draw(img)
x, y = 0, 0
def auto_next_line(next_element_width: int):
"""
超出画布宽度自动换行
Args:
next_element_width: 下一绘制元素宽度
"""
if x + next_element_width > width:
next_line()
def draw_pic(pic: Image, size: Optional[Tuple[int, int]] = None):
"""
绘制图片
Args:
pic: 要绘制的图片
size: 图片尺寸
"""
nonlocal img, draw, x, y
if size is None:
size = pic.size
else:
pic = pic.resize(size, Resampling.LANCZOS).convert('RGBA')
auto_next_line(size[0])
img.paste(pic, (x, y), pic)
pic.close()
x = int(x + size[0])
def draw_char(c: str, color: Union[Color, Tuple[int, int, int]] = Color.BLACK):
"""
绘制字符
Args:
c: 要绘制的字符
color: 字符颜色
"""
nonlocal x
if isinstance(color, Color):
color = color.value
if c == "\n":
next_line()
else:
if is_emoji(c):
emoji_img = Image.new("RGBA", (130, 130))
emoji_draw = ImageDraw.Draw(emoji_img)
emoji_draw.text((0, 0), c, font=emoji_font, embedded_color=True)
emoji_img = emoji_img.resize((30, 30), Resampling.LANCZOS)
draw_pic(emoji_img)
else:
text_width = draw.textlength(c, font)
auto_next_line(text_width)
draw.text((x, y), c, color, font)
x = int(x + text_width)
for module in modules:
module_type = module["type"]
if module_type == "RICH_TEXT_NODE_TYPE_TEXT":
for char in cls.__remove_illegal_char(module["text"]):
draw_char(char)
elif module_type == "RICH_TEXT_NODE_TYPE_EMOJI":
draw_pic(module["img"], text_img_size)
elif module_type in ["RICH_TEXT_NODE_TYPE_AT", "RICH_TEXT_NODE_TYPE_WEB", "RICH_TEXT_NODE_TYPE_BV",
"RICH_TEXT_NODE_TYPE_TOPIC", "RICH_TEXT_NODE_TYPE_LOTTERY",
"RICH_TEXT_NODE_TYPE_VOTE", "RICH_TEXT_NODE_TYPE_GOODS"]:
if module_type == "RICH_TEXT_NODE_TYPE_WEB":
draw_pic(Image.open(f"{cls.__resource_base_path}/resource/link.png"), text_img_size)
elif module_type == "RICH_TEXT_NODE_TYPE_BV":
draw_pic(Image.open(f"{cls.__resource_base_path}/resource/video.png"), text_img_size)
elif module_type == "RICH_TEXT_NODE_TYPE_LOTTERY":
draw_pic(Image.open(f"{cls.__resource_base_path}/resource/box.png"), text_img_size)
elif module_type == "RICH_TEXT_NODE_TYPE_VOTE":
draw_pic(Image.open(f"{cls.__resource_base_path}/resource/tick.png"), text_img_size)
elif module_type == "RICH_TEXT_NODE_TYPE_GOODS":
draw_pic(Image.open(f"{cls.__resource_base_path}/resource/tb.png"), text_img_size)
for char in cls.__remove_illegal_char(module["text"]):
draw_char(char, Color.LINK)
imgs.append(img)
return imgs
@classmethod
async def __draw_content(cls,
pic: PicGenerator,
modules: List[Dict[str, Any]],
text_margin: int,
forward: bool) -> PicGenerator:
"""
绘制动态主体内容
Args:
pic: 绘图器实例
modules: @ 信息
text_margin: 文字外边距
forward: 当前是否为转发动态的源动态
"""
pic.set_pos(x=text_margin)
content_imgs = await cls.__get_content_line_imgs(modules, pic.width - (text_margin * 2))
if forward and content_imgs:
heights = (content_imgs[0].height + pic.row_space) * len(content_imgs)
pic.draw_rectangle(0, pic.y, pic.width, heights, Color.LIGHTGRAY)
for img in content_imgs:
pic.draw_img_alpha(img)
return pic
@classmethod
async def __draw_picture_area(cls,
pic: PicGenerator,
pictures: List[Dict[str, Any]],
img_margin: int,
forward: bool) -> PicGenerator:
"""
绘制动态主体下方图片区域
Args:
pic: 绘图器实例
pictures: 图片信息字典
img_margin: 图片外边距
forward: 当前是否为转发动态的源动态
"""
pic.set_pos(x=img_margin)
# 下载图片
picture_count = len(pictures)
if picture_count == 1:
line_count = 1
size = 0
elif picture_count == 2 or picture_count == 4:
line_count = 2
size = int((pic.width - (img_margin * 3)) / 2)
else:
line_count = 3
size = int((pic.width - (img_margin * 4)) / 3)
download_picture_tasks = []
for picture in pictures:
if line_count == 1:
download_picture_tasks.append(open_url_image(f"{picture['img_src']}@518w.webp"))
elif line_count == 2:
src = picture['img_src']
if picture["img_height"] / picture["img_width"] >= 3:
download_picture_tasks.append(open_url_image(f"{src}@{size}w_{size}h_!header.webp"))
else:
download_picture_tasks.append(open_url_image(f"{src}@{size}w_{size}h_1e_1c.webp"))
else:
src = picture['img_src']
if picture["img_height"] / picture["img_width"] >= 3:
download_picture_tasks.append(open_url_image(f"{src}@{size}w_{size}h_!header.webp"))
else:
download_picture_tasks.append(open_url_image(f"{src}@{size}w_{size}h_1e_1c.webp"))
imgs = await asyncio.gather(*download_picture_tasks)
img_list = []
for i, img in enumerate(imgs):
if size != 0 and (img.width != size or img.height != size):
if img.width == img.height:
img_list.append(img.resize((size, size)))
elif img.width > img.height:
img = img.resize((int(img.width * (size / img.height)), size))
crop_area = (int((img.width - size) / 2), 0, int((img.width - size) / 2) + size, size)
img_list.append(img.crop(crop_area))
else:
img = img.resize((size, int(img.height * (size / img.width))))
crop_area = (0, int((img.height - size) / 2), size, int((img.height - size) / 2) + size)
img_list.append(img.crop(crop_area))
else:
img_list.append(img)
imgs = tuple(img_list)
if picture_count == 1:
img = imgs[0]
img = img.resize(((pic.width - (img_margin * 2)),
int((pic.width - (img_margin * 4)) / (img.size[0] / img.size[1]))),
Resampling.LANCZOS)
imgs = [img]
# 绘制图片
imgs = split_list(imgs, line_count)
if forward:
heights = (imgs[0][0].height + pic.row_space) * len(imgs)
pic.draw_rectangle(0, pic.y, pic.width, heights, Color.LIGHTGRAY)
for line in imgs:
for index, img in enumerate(line):
if index == len(line) - 1:
pic.draw_img(img).set_pos(x=img_margin)
else:
pic.draw_img(img, (pic.x, pic.y)).move_pos(img.width + img_margin, 0)
return pic
@classmethod
async def __draw_video_cover(cls,
pic: PicGenerator,
url: str,
duration: int,
img_margin: int,
forward: bool) -> PicGenerator:
"""
绘制视频封面
Args:
pic: 绘图器实例
url: 视频封面 URL
duration: 视频时长
img_margin: 图片外边距
forward: 当前是否为转发动态的源动态
"""
pic.set_pos(x=img_margin)
cover = await open_url_image(f"{url}@480w.webp")
cover = cover.resize(((pic.width - (img_margin * 2)),
int((pic.width - (img_margin * 4)) / (cover.size[0] / cover.size[1]))),
Resampling.LANCZOS)
cover = mask_rounded_rectangle(cover)
mask = Image.open(f"{cls.__resource_base_path}/resource/mask.png")
mask = mask.crop((0, 0, cover.width, mask.height))
time = Image.open(f"{cls.__resource_base_path}/resource/time.png")
tv = Image.open(f"{cls.__resource_base_path}/resource/tv.png")
cover.paste(mask, (0, cover.height - mask.height - 1), mask)
cover.paste(time, (13, cover.height - time.height - 14), time)
cover.paste(tv, (cover.width - tv.width - 16, cover.height - tv.height - 5), tv)
mask.close()
time.close()
tv.close()
cover_draw = ImageDraw.Draw(cover)
normal_font = config.get("PAINTER_NORMAL_FONT")
time_font = ImageFont.truetype(f"{cls.__resource_base_path}/resource/{normal_font}", 25)
cover_draw.text((21, cover.height - time.height - 11),
timestamp_format(duration + 57600, "%H:%M:%S"), Color.WHITE.value, time_font)
if forward:
cover_height = cover.height + pic.row_space
pic.draw_rectangle(0, pic.y, pic.width, cover_height, Color.LIGHTGRAY)
pic.draw_img_alpha(cover)
return pic
@classmethod
async def __draw_article_cover(cls,
pic: PicGenerator,
urls: List[str],
img_margin: int,
forward: bool) -> PicGenerator:
"""
绘制专栏封面
Args:
pic: 绘图器实例
urls: 专栏封面 URL 列表
img_margin: 图片外边距
forward: 当前是否为转发动态的源动态
"""
pic.set_pos(x=img_margin)
img_count = len(urls)
download_picture_tasks = []
for url in urls:
if img_count == 1:
download_picture_tasks.append(open_url_image(f"{url}@518w.webp"))
else:
size = int((pic.width - (img_margin * 4)) / 3)
download_picture_tasks.append(open_url_image(f"{url}@{size}w_{size}h_1e_1c.webp"))
imgs = await asyncio.gather(*download_picture_tasks)
if img_count == 1:
img = imgs[0]
img = img.resize(((pic.width - (img_margin * 2)),
int((pic.width - (img_margin * 4)) / (img.size[0] / img.size[1]))),
Resampling.LANCZOS)
imgs = [img]
if forward:
heights = imgs[0].height + pic.row_space
pic.draw_rectangle(0, pic.y, pic.width, heights, Color.LIGHTGRAY)
for index, img in enumerate(imgs):
if index == len(imgs) - 1:
pic.draw_img(img).set_pos(x=img_margin)
else:
pic.draw_img(img, (pic.x, pic.y)).move_pos(img.width + img_margin, 0)
return pic
@classmethod
async def __draw_audio_area(cls,
pic: PicGenerator,
cover_url: str,
title: str,
audio_type: str,
margin: int,
forward: bool) -> PicGenerator:
"""
绘制音频卡片
Args:
pic: 绘图器实例
cover_url: 音频封面 URL
title: 音频标题
audio_type: 音频类型
margin: 外边距
forward: 当前是否为转发动态的源动态
"""
return await cls.__draw_share_area(pic, cover_url, title, audio_type, margin, forward)
@classmethod
async def __draw_share_area(cls,
pic: PicGenerator,
cover_url: str,
title: str,
desc: str,
margin: int,
forward: bool) -> PicGenerator:
"""
绘制分享卡片
Args:
pic: 绘图器实例
cover_url: 封面 URL
title: 标题
desc: 描述
margin: 外边距
forward: 当前是否为转发动态的源动态
"""
pic.set_pos(x=margin)
cover = await open_url_image(cover_url)
cover_size = int(pic.width / 4)
cover = cover.resize((cover_size, cover_size), Resampling.LANCZOS).convert("RGBA")
cover = mask_rounded_rectangle(cover)
if forward:
heights = cover.height + pic.row_space
pic.draw_rectangle(0, pic.y, pic.width, heights, Color.LIGHTGRAY)
border = Image.new("RGBA", (pic.width - (margin * 2) + 2, cover_size + 2))
ImageDraw.Draw(border).rounded_rectangle((0, 0, border.width - 1, border.height - 1),
10, (0, 0, 0, 0), Color.GRAY.value, 1)
pic.draw_img_alpha(border, (pic.x - 1, pic.y - 1))
x, y = pic.xy
pic.draw_img_alpha(cover)
pic.draw_text(title, Color.BLACK, (x + cover_size + margin, y + int(cover_size / 5)))
pic.draw_tip(desc, xy=(x + cover_size + margin, y + int(cover_size / 5 * 3)))
return pic
@classmethod
async def __draw_live_area(cls,
pic: PicGenerator,
cover_url: str,
title: str,
area: str,
margin: int,
forward: bool) -> PicGenerator:
"""
绘制直播卡片
Args:
pic: 绘图器实例
cover_url: 直播封面 URL
title: 直播标题
area: 直播分区
margin: 外边距
forward: 当前是否为转发动态的源动态
"""
pic.set_pos(x=margin)
cover = await open_url_image(f"{cover_url}@203w_127h_1e_1c.webp")
if forward:
heights = cover.height + pic.row_space
pic.draw_rectangle(0, pic.y, pic.width, heights, Color.LIGHTGRAY)
back = Image.new("RGBA", (pic.width - (margin * 2), cover.height))
ImageDraw.Draw(back).rectangle((0, 0, back.width, back.height), Color.WHITE.value)
pic.draw_img_alpha(back, (pic.x, pic.y))
x, y = pic.xy
pic.draw_img(cover)
pic.draw_text(title, Color.BLACK, (x + cover.width + margin, y + int(cover.height / 5)))
pic.draw_tip(area, xy=(x + cover.width + margin, y + int(cover.height / 5 * 3)))
return pic
@classmethod
async def __draw_add_on_card(cls,
pic: PicGenerator,
infos: List[Dict[str, Any]],
margin: int,
forward: bool) -> PicGenerator:
"""
绘制附加卡片
Args:
pic: 绘图器实例
infos: 附加卡片信息
margin: 外边距
forward: 当前是否为转发动态的源动态
"""
pic.set_pos(x=margin)
card_height = int(pic.width / 4)
padding = 10
img_height = card_height - padding * 2
edge = pic.width - margin - padding
for info in infos:
# 绘制背景
back_color = Color.LIGHTGRAY
if forward:
pic.draw_rectangle(0, pic.y, pic.width, card_height + pic.row_space + padding, Color.LIGHTGRAY)
back_color = Color.WHITE
pic.move_pos(0, padding)
back = Image.new("RGBA", (pic.width - (margin * 2), card_height))
ImageDraw.Draw(back).rectangle((0, 0, back.width, back.height), back_color.value)
back = mask_rounded_rectangle(back)
pic.draw_img_alpha(back, (pic.x, pic.y))
pic.move_pos(padding, padding)
# 根据类型绘制卡片
card_type = info["add_on_card_show_type"]
if card_type == 1:
# 淘宝
goods = json.loads(info["goods_card"])["list"]
download_picture_tasks = []
for good in goods:
url = good["img"]
download_picture_tasks.append(open_url_image(f"{url}@{img_height}w_{img_height}h_1e_1c.webp"))
imgs = await asyncio.gather(*download_picture_tasks)
if len(goods) == 1:
img = mask_rounded_rectangle(imgs[0])
x, y = pic.xy
pic.draw_img(img)
name = limit_str_length(goods[0]["name"], 15)
price = goods[0]["priceStr"]
price_length = pic.get_text_length(price)
pic.draw_text(name, Color.BLACK, (x + img.width + margin, y + int(img_height / 5)))
pic.draw_text(price, Color.LINK, (x + img.width + margin, y + int(img_height / 5 * 3)))
pic.draw_tip("", xy=(x + img.width + margin + price_length, y + int(img_height / 5 * 3) + 5))
else:
for index, img in enumerate(imgs):
overflow = False
img = mask_rounded_rectangle(img)
if pic.x + img.width > edge:
overflow = True
img = img.crop((0, 0, edge - pic.x, img.height))
if index == len(imgs) - 1 or overflow:
pic.draw_img(img).set_pos(x=margin)
if overflow:
break
else:
pic.draw_img(img, pic.xy).move_pos(img.height + margin, 0)
elif card_type == 2:
# 充电、相关装扮、相关游戏
base = info["attach_card"]
if base["type"] in ["decoration", "game"]:
url = base["cover_url"]
title = base["title"]
desc_first = base["desc_first"]
desc_second = base["desc_second"]
cover = await open_url_image(f"{url}@{img_height}w_{img_height}h_1e_1c.webp")
cover = mask_rounded_rectangle(cover)
x, y = pic.xy
pic.draw_img(cover)
pic.draw_text(title, Color.BLACK, (x + cover.width + margin, y + int(img_height / 10)))
pic.draw_tip(desc_first, xy=(x + cover.width + margin, y + int(img_height / 7 * 3)))
pic.draw_tip(desc_second, xy=(x + cover.width + margin, y + int(img_height / 7 * 5)))
elif base["type"] == "lottery":
title = base["title"]
desc_first = limit_str_length(base["desc_first"], 24)
icon = Image.open(f"{cls.__resource_base_path}/resource/box.png")
x, y = pic.xy
pic.draw_text(title, Color.BLACK, (x, y + int(img_height / 5)))
pic.draw_img_alpha(icon, (x, y + int(img_height / 5 * 3)))
pic.draw_tip(desc_first, Color.LINK, (x + icon.width + padding, y + int(img_height / 5 * 3)))
pic.set_pos(x=margin).move_pos(0, img_height + pic.row_space)
elif card_type == 3:
# 投票
vote = json.loads(info["vote_card"])
desc = limit_str_length(vote["desc"], 15)
join_num = vote["join_num"]
icon = Image.open(f"{cls.__resource_base_path}/resource/tick_big.png")
icon = mask_rounded_rectangle(icon.resize((img_height, img_height), Resampling.LANCZOS))
x, y = pic.xy
pic.draw_img_alpha(icon)
pic.draw_text(desc, Color.BLACK, (x + img_height + margin, y + int(img_height / 5)))
pic.draw_tip(f"{join_num}人参与", xy=(x + img_height + margin, y + int(img_height / 5 * 3)))
elif card_type == 5:
# 视频分享
base = info["ugc_attach_card"]
url = base["image_url"]
title = limit_str_length(base["title"], 12)
desc_second = base["desc_second"]
cover = await open_url_image(f"{url}@480w.webp")
cover = cover.resize((int(img_height / cover.height * cover.width), img_height), Resampling.LANCZOS)
cover = mask_rounded_rectangle(cover)
x, y = pic.xy
pic.draw_img(cover)
pic.draw_text(title, Color.BLACK, (x + cover.width + margin, y + int(img_height / 5)))
pic.draw_tip(desc_second, xy=(x + cover.width + margin, y + int(img_height / 5 * 3)))
elif card_type == 6:
# 视频、直播预约
base = info["reserve_attach_card"]
title = cls.__remove_illegal_char(base["title"])
desc_first = base["desc_first"]["text"]
desc_second = base["desc_second"]
desc = f"{desc_first} {desc_second}"
lottery = limit_str_length(base["reserve_lottery"]["text"], 24) if "reserve_lottery" in base else None
if lottery:
pic.draw_text(title, Color.BLACK, (pic.x, pic.y + int(img_height / 10)))
pic.draw_tip(desc, xy=(pic.x, pic.y + int(img_height / 7 * 3)))
icon = Image.open(f"{cls.__resource_base_path}/resource/box.png")
pic.draw_img_alpha(icon, (pic.x, pic.y + int(img_height / 7 * 5)))
pic.draw_tip(lottery, Color.LINK, (pic.x + icon.width + padding, pic.y + int(img_height / 7 * 5)))
else:
pic.draw_text(title, Color.BLACK, (pic.x, pic.y + int(img_height / 5)))
pic.draw_tip(desc, xy=(pic.x, pic.y + int(img_height / 5 * 3)))
pic.set_pos(x=margin).move_pos(0, img_height + pic.row_space)
return pic