This commit is contained in:
2025-03-04 20:48:27 +08:00
parent 1a5d161b5a
commit b0e71cf4f7
4192 changed files with 0 additions and 268307 deletions

View File

@@ -0,0 +1,512 @@
import argparse
import logging
import os
import math
from datetime import datetime
import nonebot
import colorsys
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from nonebot_plugin_maimai_helper.helper.simai import *
from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message , MessageSegment
from nonebot_plugin_maimai_helper.util.utils import *
logger = logging.getLogger(__name__)
config = nonebot.get_driver().config
maimaiImgPath = getattr(config, 'user_imgpath', 'E:/res/images') + '/'
materialPath = getattr(config, 'user_fontpath', 'E:/res/material') + '/'
def get_user_preview(event: GroupMessageEvent):
result = {"is_success": False, "is_error": False, "user_id": 0, "data": {}, "msg_body": "",
"is_in_whitelist": False, "is_got_qr_code": False}
user_qq = event.get_user_id()
user_id = None
if is_userid_exist(user_qq):
user_id = get_userid(user_qq)
else:
logger.error("查无此人")
if user_id != -1:
data = get_preview_detailed(user_id)
if data["is_got_qr_code"]:
user_preview = data
else:
logger.error("用户未获取二维码")
result["msg_body"] = "请在微信「舞萌 | 中二」公众号上点击一次「玩家二维码」按钮后再试一遍吧~"
logger.error("获取用户数据失败")
user_preview = user_preview
return user_preview, result
user_data = {
"nickname": {get_user_preview['user_preview']['userName']},
"title": {get_user_preview['user_preview']['titleName']},
"icon": {get_user_preview['user_preview']['iconId']},
"frame": {get_user_preview['user_preview']['frameId']},
"plate": {get_user_preview['user_preview']['plateId']},
"rating": {get_user_preview['user_preview']['playerRating']},
"classRank": 5,
"courseRank": 8,
"titleRare": {get_user_preview['user_preview']['titleRare']},
"chara": {get_user_preview['user_preview']['charaId']},
"charaLevel": {get_user_preview['user_preview']['charaLevel']},
}
def get_star_info(level):
# 定义星级觉醒的等级区间
awakening_levels = [9, 49, 99, 299, 999, 9999]
max_stars = len(awakening_levels)
# 计算当前星级
current_star = 0
for awakening_level in awakening_levels:
if level >= awakening_level:
current_star += 1
else:
break
# 如果已经满星
if current_star == max_stars:
return current_star, 100
# 计算距离下一星级的百分比
current_awakening_level = awakening_levels[current_star - 1] if current_star > 0 else 0
next_awakening_level = awakening_levels[current_star]
progress = (level - current_awakening_level) / (next_awakening_level - current_awakening_level) * 100
return current_star, int((progress // 10) * 10)
def circle_corner(img, radii=30):
# 白色区域透明可见,黑色区域不可见
circle = Image.new('L', (radii * 2, radii * 2), 0)
draw = ImageDraw.Draw(circle)
draw.ellipse((0, 0, radii * 2, radii * 2), fill=255)
img = img.convert("RGBA")
w, h = img.size
# 画角
alpha = Image.new('L', img.size, 255)
alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角
alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) # 右上角
alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), (w - radii, h - radii)) # 右下角
alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) # 左下角
img.putalpha(alpha)
return img
def drawUserImg(title,totalRating,rankRating,userName,icon,plate,title_rare="Normal",classRank=-1):
UserImg = Image.new('RGBA', (724, 120))
plateImg = Image.open(plate).resize((720,116))
UserImg.paste(plateImg, (0, 2), plateImg)
iconImg = Image.open(icon).resize((100,100))
UserImg.paste(iconImg,(8,10),iconImg)
ratingPlateImg = Image.open(rf"{maimaiImgPath}/Rating/{getRatingBgImg(totalRating)}").resize((174,36))
UserImg.paste(ratingPlateImg, (110, 6), ratingPlateImg)
# 定义偏移量和初始x坐标
offset = 14
start_x = 250
x_positions = [start_x - i * offset for i in range(len(str(totalRating)))][:5]
# 根据totalRating的位数处理图片
for i, x_pos in enumerate(x_positions):
# 计算当前位上的数字
digit = int(totalRating / (10 ** i) % 10)
# 打开并调整图片大小
numImg = Image.open(rf"{maimaiImgPath}/num/UI_NUM_Drating_{digit}.png").resize((19, 23))
# 粘贴图片
UserImg.paste(numImg, (x_pos, 13), numImg)
if 25 >= int(classRank) >= 0:
classRankImg = Image.open(rf"{maimaiImgPath}/classRank/UI_CMN_Class_S_{int(classRank):02d}.png").resize((100, 60))
UserImg.paste(classRankImg, (284, -8), classRankImg)
UserIdImg = circle_corner(Image.new('RGBA', (270, 40), color=(255, 255, 255)),5)
UserIdDraw = ImageDraw.Draw(UserIdImg)
UserIdDraw.text((7, 8), f"{userName}", font=ImageFont.truetype(rf'{materialPath}/GenSenMaruGothicTW-Medium.ttf', 20),fill=(0, 0, 0))
UserImg.paste(UserIdImg, (113, 44), UserIdImg)
if 23 >= int(rankRating) >= 0:
rankImg = Image.open(rf"{maimaiImgPath}/Ranks/{int(rankRating)}.png").resize((94, 44))
UserImg.paste(rankImg, (290, 42), rankImg)
totalRatingImg = Image.open(rf"{maimaiImgPath}/shougou/UI_CMN_Shougou_{title_rare.title()}.png")
totalRatingDraw = ImageDraw.Draw(totalRatingImg)
font = ImageFont.truetype(rf'{materialPath}/GenSenMaruGothicTW-Bold.ttf', 14)
_, _, text_width, text_height = totalRatingDraw.textbbox((0, 0), title, font=font)
draw_text_with_stroke_and_spacing(
totalRatingDraw,
(abs(250-text_width)//2, 7),
title,
font=font,
fill_color="white",
stroke_width=2,
stroke_color='black',
spacing=1
)
UserImg.paste(totalRatingImg, (113, 83), totalRatingImg)
return UserImg
def _getCharWidth(o) -> int:
widths = [
(126, 1), (159, 0), (687, 1), (710, 0), (711, 1), (727, 0), (733, 1), (879, 0), (1154, 1), (1161, 0),
(4347, 1), (4447, 2), (7467, 1), (7521, 0), (8369, 1), (8426, 0), (9000, 1), (9002, 2), (11021, 1),
(12350, 2), (12351, 1), (12438, 2), (12442, 0), (19893, 2), (19967, 1), (55203, 2), (63743, 1),
(64106, 2), (65039, 1), (65059, 0), (65131, 2), (65279, 1), (65376, 2), (65500, 1), (65510, 2),
(120831, 1), (262141, 2), (1114109, 1),
]
if o == 0xe or o == 0xf:
return 0
for num, wid in widths:
if o <= num:
return wid
return 1
def getRatingBgImg(rating):
totalRating = int(rating)
if 0 <= totalRating <= 999:
ratingPlate = "UI_CMN_DXRating_01.png"
elif 1000 <= totalRating <= 1999:
ratingPlate = "UI_CMN_DXRating_02.png"
elif 2000 <= totalRating <= 3999:
ratingPlate = "UI_CMN_DXRating_03.png"
elif 4000 <= totalRating <= 6999:
ratingPlate = "UI_CMN_DXRating_04.png"
elif 7000 <= totalRating <= 9999:
ratingPlate = "UI_CMN_DXRating_05.png"
elif 10000 <= totalRating <= 11999:
ratingPlate = "UI_CMN_DXRating_06.png"
elif 12000 <= totalRating <= 12999:
ratingPlate = "UI_CMN_DXRating_07.png"
elif 13000 <= totalRating <= 13999:
ratingPlate = "UI_CMN_DXRating_08.png"
elif 14000 <= totalRating <= 14499:
ratingPlate = "UI_CMN_DXRating_09.png"
elif 14500 <= totalRating <= 14999:
ratingPlate = "UI_CMN_DXRating_10.png"
else:
ratingPlate = "UI_CMN_DXRating_11.png"
return ratingPlate
def apply_gradient_blur(image, blur_radius=10, start_height=0, end_height=35):
# 创建一个与原始图像大小相同的空白渐变蒙版
mask = Image.new("L", image.size, 0)
draw = ImageDraw.Draw(mask)
# 将 start_height 和 end_height 转换为绝对像素位置
start_pixel = int(start_height * image.height)
end_pixel = int(end_height * image.height)
# 绘制渐变蒙版,模糊从 start_pixel 开始,到 end_pixel 结束
for y in range(image.height):
if y < start_pixel:
intensity = 0
elif y > end_pixel:
intensity = 255
else:
# 根据 y 位置线性插值计算模糊程度
intensity = int(255 * (y - start_pixel) / (end_pixel - start_pixel))
draw.line((0, y, image.width, y), fill=intensity)
# 对图像进行模糊处理
blurred_image = image.filter(ImageFilter.GaussianBlur(blur_radius))
# 将模糊图像和原始图像混合,使用渐变蒙版控制模糊区域
result = Image.composite(blurred_image, image, mask)
return result
def drawCharaImgNewSub(charaId, charaLevel):
if int(charaLevel) > 9999:
charaLevel = 9999
if int(charaLevel) < 0:
charaLevel = 0
star, progress = get_star_info(int(charaLevel))
alpha = Image.open(rf"{maimaiImgPath}/maicard/alpha.png").convert("L")
base = Image.open(rf"{maimaiImgPath}/maicard/UI_Chara_RBase.png").convert("RGBA")
frame = Image.open(rf"{maimaiImgPath}/maicard/UI_Chara_RFrame.png").convert("RGBA").resize((152, 268))
level_img = Image.open(rf"{maimaiImgPath}/maicard/UI_NUM_MLevelDAMMY_14.png").convert("RGBA").resize((20, 14))
if not os.path.exists(rf"{maimaiImgPath}/Chara/UI_Chara_{charaId:06d}.png"):
charaId = 0
chara_img = Image.new("RGBA", (264, 300), (255, 255, 255, 0))
c = Image.open(rf"{maimaiImgPath}/Chara/UI_Chara_{charaId:06d}.png").convert("RGBA").resize((170, 170))
chara_img.paste(c, (0, 40), c)
chara_img = chara_img.crop((12, -32, 12 + alpha.width, alpha.height - 32))
base.paste(chara_img, (0, 0), chara_img)
base.paste(frame, (-2, -2), frame)
base.putalpha(alpha)
new_base = Image.new(mode="RGBA", size=(152, 298), color=(255, 255, 255, 0))
new_base.paste(base, (0, 0), base)
if star >= 6:
main_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_star_big_MAX.png").convert("RGBA").resize((53, 50))
sub_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_star_small_MAX.png").convert("RGBA").resize((35, 35))
elif star >= 5:
main_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_big.png").convert("RGBA").resize((53, 50))
sub_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_Small.png").convert("RGBA").resize((35, 35))
else:
main_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_Big_Gauge01_{progress}.png").convert("RGBA").resize((53, 50))
sub_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_Small.png").convert("RGBA").resize((35, 35))
if star >= 1:
new_base.paste(main_star, (47, 234), main_star)
if star >= 2:
new_base.paste(sub_star, (16, 226), sub_star)
if star >= 3:
new_base.paste(sub_star, (97, 226), sub_star)
if star >= 4:
sub_star = sub_star.resize((26, 26))
new_base.paste(sub_star, (6, 206), sub_star)
if star >= 5:
new_base.paste(sub_star, (116, 206), sub_star)
new_base.paste(level_img, (20, 17), level_img)
num_x = 46
for num in str(charaLevel):
num_img = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Num_26p_{num}.png").convert("RGBA").resize((25, 28))
new_base.paste(num_img, (num_x, 6), num_img)
num_x += 18
return new_base
def drawCharaImgNewMain(charaId, charaLevel):
if int(charaLevel) > 9999:
charaLevel = 9999
if int(charaLevel) < 0:
charaLevel = 0
star, progress = get_star_info(int(charaLevel))
frame = Image.open(rf"{maimaiImgPath}/maicard/UI_Chara_LFrame.png").convert("RGBA").resize((235, 101))
level_img = Image.open(rf"{maimaiImgPath}/maicard/UI_NUM_MLevelDAMMY_14.png").convert("RGBA").resize((36, 23))
if not os.path.exists(rf"{maimaiImgPath}/Chara/UI_Chara_{charaId:06d}.png"):
charaId = 0
base = Image.open(rf"{maimaiImgPath}/Chara/UI_Chara_{charaId:06d}.png").convert("RGBA").resize((512, 512))
base.paste(frame, (140, 381), frame)
if star >= 6:
main_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_star_big_MAX.png").convert("RGBA").resize((65, 61))
sub_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_star_small_MAX.png").convert("RGBA").resize((45, 45))
elif star >= 5:
main_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_big.png").convert("RGBA").resize((65, 61))
sub_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_Small.png").convert("RGBA").resize((45, 45))
else:
main_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_Big_Gauge01_{progress}.png").convert("RGBA").resize((65, 61))
sub_star = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Chara_Star_Small.png").convert("RGBA").resize((45, 45))
if star >= 1:
base.paste(main_star, (225, 443), main_star)
if star >= 2:
base.paste(sub_star, (185, 446), sub_star)
if star >= 3:
base.paste(sub_star, (285, 446), sub_star)
if star >= 4:
sub_star = sub_star.resize((30, 30))
base.paste(sub_star, (158, 438), sub_star)
if star >= 5:
base.paste(sub_star, (327, 438), sub_star)
base.paste(level_img, (170, 405), level_img)
num_x = 229
for num in str(charaLevel):
num_img = Image.open(rf"{maimaiImgPath}/maicard/UI_CMN_Num_26p_{num}.png").convert("RGBA").resize((38, 44))
base.paste(num_img, (num_x, 394), num_img)
num_x += 27
return apply_gradient_blur(base.resize((264, 264)), 100)
def get_dominant_color(image):
# 颜色模式转换以便输出rgb颜色值
image = image.convert('RGBA')
# 生成缩略图减少计算量减小cpu压力
image.thumbnail((200, 200))
max_score = 0
dominant_color = (0,0,0)
for count, (r, g, b, a) in image.getcolors(image.size[0] * image.size[1]):
# 跳过纯黑色
if (a == 0) or (sum((r, g, b, a)) == 0):
continue
saturation = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)[1]
y = min(abs(r * 2104 + g * 4130 + b * 802 + 4096 + 131072) >> 13, 235)
y = (y - 16.0) / (235 - 16)
# 忽略高亮色
if y > 0.9:
continue
score = (saturation + 0.1) * count
if score > max_score:
max_score = score
dominant_color = (r, g, b)
return dominant_color
def draw_text_with_stroke(draw: ImageDraw, pos, text, font, fill_color, stroke_width=2, stroke_color='white'):
# 绘制描边
for x_offset in range(-stroke_width+1, stroke_width):
for y_offset in range(-stroke_width+1, stroke_width):
if x_offset == 0 and y_offset == 0:
continue
draw.text((pos[0] + x_offset, pos[1] + y_offset), text, font=font, fill=stroke_color)
# 在正确位置绘制文本
draw.text(pos, text, font=font, fill=fill_color)
def draw_text_with_stroke_and_spacing(draw: ImageDraw.ImageDraw, pos, text, font, fill_color, stroke_width=2, stroke_color='white', spacing=5):
# 绘制描边
for x_offset in range(-stroke_width+1, stroke_width):
for y_offset in range(-stroke_width+1, stroke_width):
if x_offset == 0 and y_offset == 0:
continue
xx, yy = (pos[0] + x_offset, pos[1] + y_offset)
draw_text_with_spacing(draw, (xx, yy), text, font, stroke_color, spacing)
draw_text_with_spacing(draw, pos, text, font, fill_color, spacing)
def draw_text_with_spacing(draw: ImageDraw.ImageDraw, pos, text, font, fill_color, spacing=5):
# 逐字符绘制并调整位置
x, y = pos
for char in text:
_, _, char_width, _ = draw.textbbox((0, 0), char, font=font)
draw.text((x, y), char, font=font, fill=fill_color)
x += char_width + spacing # 增加间距
def call_user_img(user_data, no_chara=False):
try:
frame_path = rf"{maimaiImgPath}/Frame/UI_Frame_{user_data['frame']:06d}.png"
frame_img = Image.open(frame_path).resize((1080, 452))
except:
frame_path = rf"{maimaiImgPath}/Frame/UI_Frame_000000.png"
frame_img = Image.open(frame_path).resize((1080, 452))
theme_color = get_dominant_color(frame_img)
img = Image.new("RGBA", (1080, 477), theme_color)
text_color = tuple(abs(c-100)%255 for c in theme_color)
img.paste(frame_img, (0, 0))
plate = rf"{maimaiImgPath}/Plate/UI_Plate_{user_data['plate']:06d}.png"
icon = rf"{maimaiImgPath}/Icon/UI_Icon_{user_data['icon']:06d}.png"
UserImg: Image = drawUserImg(user_data["title"], user_data["rating"], user_data['courseRank'], user_data['nickname'], icon, plate,user_data["titleRare"],user_data["classRank"])
img.paste(UserImg, (25, 25), UserImg)
network_status_img = Image.open(rf"{maimaiImgPath}/network/on.png")
img.paste(network_status_img, (1014, 25), network_status_img)
if not no_chara:
main_chara = drawCharaImgNewMain(user_data["chara"][0], user_data["charaLevel"][0]).resize((309, 309))
img.paste(main_chara, (-36, 143), main_chara)
chara_start_x = 204
chara_start_y = 170
for chara in zip(user_data["chara"][1:], user_data["charaLevel"][1:]):
chara_img = drawCharaImgNewSub(*chara).resize((147, 290))
img.paste(chara_img, (chara_start_x, chara_start_y), chara_img)
chara_start_x += 138
designDraw = ImageDraw.Draw(img)
# 保留这些可以喵?
designDraw.text((20, 457),
f"Generated by SaltBot at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} Code by Error063 Plugin Edit by NingYu - 图片仅供参考",
font=ImageFont.truetype(rf'{materialPath}/GenSenMaruGothicTW-Bold.ttf', 12), fill=text_color)
return img
def call_user_img_preview(user_data):
base_img = Image.open(f"{maimaiImgPath}/prof/UI_ENT_Base_Myprof.png")
draw = ImageDraw.Draw(base_img)
draw.text((268,101), f"{user_data['nickname']}", font=ImageFont.truetype(rf'{materialPath}/GenSenMaruGothicTW-Medium.ttf', 20), fill="black")
icon = rf"{maimaiImgPath}/Icon/UI_Icon_{user_data['icon']:06d}.png"
iconImg = Image.open(icon).resize((156, 156))
base_img.paste(iconImg, (68, 114), iconImg)
awake_star_img = Image.open(rf"{maimaiImgPath}/prof/UI_ENT_Base_Myprof_Starchip.png").resize((80, 52))
base_img.paste(awake_star_img, (311, 253), awake_star_img)
draw_text_with_stroke(
draw,
(411, 256),
str(user_data["awake"]),
ImageFont.truetype(rf'{materialPath}/GenSenMaruGothicTW-Bold.ttf', 46),
"white",
stroke_width=2,
stroke_color='black'
)
ratingPlateImg = Image.open(rf"{maimaiImgPath}/Rating_big/{getRatingBgImg(int(user_data['rating']))}").resize((312,58))
base_img.paste(ratingPlateImg, (253, 163), ratingPlateImg)
# 定义偏移量和初始x坐标
offset = 25
start_x = 496
start_y = 177
x_positions = [start_x - i * offset for i in range(len(str(int(user_data["rating"]))))][-5:]
# 根据totalRating的位数处理图片
for i, x_pos in enumerate(x_positions):
# 计算当前位上的数字
digit = int(int(user_data["rating"]) / (10 ** i) % 10)
# 打开并调整图片大小
numImg = Image.open(rf"{maimaiImgPath}/num/UI_NUM_Drating_{digit}.png")
# 粘贴图片
base_img.paste(numImg, (x_pos, start_y), numImg)
return base_img
a = call_user_img(user_data, False)
a.save("E:/img/output.png")
'''
def main():
parser = argparse.ArgumentParser(description="基于Python的玩家收藏品组合的图片生成器", formatter_class=argparse.ArgumentDefaultsHelpFormatter, add_help=True)
parser.add_argument("--nickname", type=str, default="NingYu☆", help="玩家昵称")
parser.add_argument("--title", type=str, default="ソルト 推し", help="玩家称号")
parser.add_argument("--icon", type=int, default=302, help="玩家头像ID")
parser.add_argument("--frame", type=int, default=300501, help="玩家背景板ID")
parser.add_argument("--plate", type=int, default=50702, help="玩家姓名框ID")
parser.add_argument("--rating", type=int, default=14225, help="玩家Rating")
parser.add_argument("--classRank", type=int, default=7, help="玩家友人对战等级")
parser.add_argument("--courseRank", type=int, default=10, help="玩家段位认定等级")
parser.add_argument("--titleRare", type=str, default="Sliver", help="玩家称号稀有度")
parser.add_argument("--chara", nargs='+', type=int, default=[101, 104, 355610, 355611, 355612], help="玩家设置的旅行伙伴ID列表")
parser.add_argument("--charaLevel", nargs='+', type=int, default=[1000, 9999, 1000, 9999, 9999], help="玩家设置的旅行伙伴等级列表")
parser.add_argument("--output", type=str, default="./output.png", help="图片输出路径")
args = parser.parse_args()
user_data = {
"nickname": 1,
"title": args.title,
"icon": args.icon,
"frame": args.frame,
"plate": args.plate,
"rating": args.rating,
"classRank": args.classRank,
"courseRank": args.courseRank,
"titleRare": args.titleRare,
"chara": args.chara,
"charaLevel": args.charaLevel,
}
for k, v in user_data.items():
print(f"{k}: {v}")
a = call_user_img(user_data, False)
a.save(args.output)
print("\nDone")
print(f"File path: {args.output}")
# a.show()
if __name__ == "__main__":
main()
'''