Skip to content

Instantly share code, notes, and snippets.

@monsterxcn
Created October 18, 2021 03:08
Show Gist options
  • Save monsterxcn/9f7b188583eb0ff7b13c6b4cae2b8660 to your computer and use it in GitHub Desktop.
Save monsterxcn/9f7b188583eb0ff7b13c6b4cae2b8660 to your computer and use it in GitHub Desktop.
PIL 绘制原神实时便笺草稿(原谅我两空格缩进还手搓 CQ 码
import base64
import hashlib
import json
import os
import sys
import time
from io import BytesIO
from random import randint
import nonebot
from httpx import AsyncClient
from nonebot.log import logger
from PIL import Image, ImageDraw, ImageFont
resPath = nonebot.get_driver().config.resources_dir
if not resPath:
raise ValueError(f'请在环境变量中添加 resources_dir 参数')
# 检查 UID 是否合法
def uidChecker(uid):
try:
uid = str(int(uid))
if len(uid) == 9:
# 判断 9 位 UID 首位得到所在服务器
if uid[0] == "1" or uid[0] == "2":
return uid, "cn_gf01"
if uid[0] == "5":
return uid, "cn_qd01"
elif len(uid) < 9:
# 少于 9 位 UID 自动补成官服形式
uid = str(int(uid.zfill(9)) + 100000000)
return uid, "cn_gf01"
except:
pass
# 输入 UID 不合法返回所在服务器为空
return uid, None
# 计算文本 MD5 字符串
def md5(text):
md5 = hashlib.md5()
md5.update(text.encode())
return md5.hexdigest()
# 设置 Pillow 绘制字体
def font(size):
return ImageFont.truetype(f'{resPath}info/zh-cn.ttf', size=size)
# 转换 Image 对象图片为 Base64 编码字符串
def img2Base64(pic: Image.Image) -> str:
buf = BytesIO()
pic.save(buf, format="PNG", quality=100)
base64_str = base64.b64encode(buf.getbuffer()).decode()
return "base64://" + base64_str
# 转换网络图片为 Image 对象
# 探索派遣的小头像会缓存到 {resPath}ck/character_side_icon/
async def pic2Image(url):
localPic = url.replace("https://upload-bbs.mihoyo.com/game_record/genshin/", f"{resPath}ck/")
# 本地文件存在,读取
if os.path.exists(localPic):
userImage = Image.open(localPic)
# 本地文件不存在,从服务器下载并保存
else:
async with AsyncClient() as client:
try:
res = await client.get(url, timeout=20.0)
userImage = Image.open(BytesIO(res.content))
userImage.save(localPic, quality=100)
except Exception as e:
logger.error(str(sys.exc_info()[0]) + '\n' + str(e))
return None
# 返回 URL 图片的 Image 对象
return userImage
# 绘制实时便笺模版
async def drawNotifyTpl():
## 创建新图片、选择模板
result = Image.new("RGBA", (600, 780), "#f1eae2")
txtDraw = ImageDraw.Draw(result)
## 绘制可莉图标
avatar = Image.open(f"{resPath}ck/keli.150.png")
avatar = avatar.resize((100, 100), Image.ANTIALIAS).convert("RGBA")
avatar.save(f"{resPath}ck/keli.100.png", format="PNG", quality=100)
result.paste(avatar, (30, 30), avatar)
## 绘制原粹树脂,540*100(400*100 + 140*100)
startHeight = 150
pureBg = ((30, startHeight), (30 + 400 - 1, startHeight + 100 - 1))
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0)
pureBg = ((430, startHeight), (430 + 140 - 1, startHeight + 100 - 1))
ImageDraw.Draw(result).rectangle(pureBg, fill="#ece2d8", width=0)
icon = Image.open(f"{resPath}ck/Item_Fragile_Resin-2.png")
icon = icon.resize((50, 50), Image.ANTIALIAS).convert("RGBA")
icon.save(f"{resPath}ck/Item_Fragile_Resin.50.png", format="PNG", quality=100)
result.paste(icon, (45, startHeight + 25), icon)
txtDraw.text((110, startHeight + 20), "原粹树脂", font=font(26), fill="#4f4f4f")
## 绘制每日委托,540*100(400*100 + 140*100)
startHeight += 100+30
pureBg = ((30, startHeight), (30 + 400 - 1, startHeight + 100 - 1))
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0)
pureBg = ((430, startHeight), (430 + 140 - 1, startHeight + 100 - 1))
ImageDraw.Draw(result).rectangle(pureBg, fill="#ece2d8", width=0)
icon = Image.open(f"{resPath}ck/Icon_Commission.png")
icon = icon.resize((40, 40), Image.ANTIALIAS).convert("RGBA")
icon.save(f"{resPath}ck/Icon_Commission.50.png", format="PNG", quality=100)
result.paste(icon, (50, startHeight + 30), icon)
txtDraw.text((110, startHeight + 20), "每日委托", font=font(26), fill="#4f4f4f")
## 绘制每周消耗减半,540*100(400*100 + 140*100)
startHeight += 100+30
pureBg = ((30, startHeight), (30 + 400 - 1, startHeight + 100 - 1))
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0)
pureBg = ((430, startHeight), (430 + 140 - 1, startHeight + 100 - 1))
ImageDraw.Draw(result).rectangle(pureBg, fill="#ece2d8", width=0)
icon = Image.open(f"{resPath}ck/45px-秘境logo.png")
icon = icon.resize((40, 40), Image.ANTIALIAS).convert("RGBA")
icon.save(f"{resPath}ck/Icon_Domain.40.png", format="PNG", quality=100)
result.paste(icon, (50, startHeight + 30), icon)
txtDraw.text((110, startHeight + 20), "消耗减半", font=font(26), fill="#4f4f4f")
## 绘制探索派遣,540*210
startHeight += 100+30
pureBg = ((30, startHeight), (30 + 540 - 1, startHeight + 210 - 1))
ImageDraw.Draw(result).rectangle(pureBg, fill="#f6f2eb", width=0)
txtDraw.text((60, startHeight + 20), "探索派遣", font=font(26), fill="#4f4f4f")
result.save(f"{resPath}ck/NotifyTpl.png", quality=100)
# 绘制实时便笺
async def drawNotify(uid, jsonData):
## 创建新图片、选择模板
resData = jsonData["data"]
result = Image.open(f"{resPath}ck/NotifyTpl.png")
txtDraw = ImageDraw.Draw(result)
## 绘制 UID
txtDraw.text((164, 64), f"UID {uid} 实时便笺", font=font(30), fill="#1d1f21")
# txt = "* 数据刷新可能存在一定延迟"
# txtDraw.text((570 - font(16).getsize(txt)[0], 110), txt, font=font(16), fill="#84603d")
## 绘制原粹树脂,540*100
startHeight = 150
if resData["current_resin"] == resData["max_resin"]:
txt = "原粹树脂已满,抽空清清吧"
else:
remainHour = int(resData["resin_recovery_time"]) // 3600
remainMinute = int(resData["resin_recovery_time"]) % 60
remainNote = f"{str(remainHour) + ' 时' if remainHour else ''}{' ' + str(remainMinute) + ' 分' if remainMinute else ''}"
txt = f"完全恢复还需 {remainNote}"
txtDraw.text((110, startHeight + 56), txt, font=font(22), fill="#675855") #c1b1a1
txt = f"{str(resData['current_resin'])} / {str(resData['max_resin'])}"
textWidth, textHeight = font(24).getsize(txt)
txtDraw.text((int(430 + (140 - textWidth) / 2), int(startHeight + (100 - textHeight) / 2)), txt, font=font(24), fill="#84603d") #675855
## 绘制每日委托,540*100
startHeight += 130
if not resData["is_extra_task_reward_received"]:
txt = "「每日委托」奖励未领取!" if resData["finished_task_num"] == resData["total_task_num"] else "今日完成委托数量不足"
else:
txt = "所有奖励均已领取~"
txtDraw.text((110, startHeight + 56), txt, font=font(22), fill="#675855") #c1b1a1
txt = f"{str(resData['finished_task_num'])} / {str(resData['total_task_num'])}"
textWidth, textHeight = font(24).getsize(txt)
txtDraw.text((int(430 + (140 - textWidth) / 2), int(startHeight + (100 - textHeight) / 2)), txt, font=font(24), fill="#84603d") #675855
## 绘制每周消耗减半,540*100
startHeight += 130
if resData["remain_resin_discount_num"]:
txt = "本周剩余消耗减半次数"
else:
txt = "本周消耗减半次数已耗尽~"
txtDraw.text((110, startHeight + 56), txt, font=font(22), fill="#675855") #c1b1a1
txt = f"{str(resData['remain_resin_discount_num'])} / 3"
textWidth, textHeight = font(24).getsize(txt)
txtDraw.text((int(430 + (140 - textWidth) / 2), int(startHeight + (100 - textHeight) / 2)), txt, font=font(24), fill="#84603d") #675855
## 绘制探索派遣,540*250
startHeight += 130
txt = f"{str(resData['current_expedition_num'])} / {str(resData['max_expedition_num'])}"
txtDraw.text((int(430 + (140 - font(24).getsize(txt)[0]) / 2), startHeight + 20), txt, font=font(26), fill="#84603d") #675855
startWidth = 30 + 15
for avatar in resData["expeditions"]:
charImg = await pic2Image(avatar["avatar_side_icon"])
if charImg is None:
# 获取角色头像失败则创建与透明图片
charImg = Image.new("RGBA", (90, 90), (0, 0, 0, 0))
charImg = charImg.resize((90, 90), Image.ANTIALIAS).convert("RGBA")
result.paste(charImg, (startWidth, startHeight + 60), charImg)
if avatar["remained_time"] == "0":
txt, color = "探险完成", "#7fbb2f"
else:
remainHour = int(avatar["remained_time"]) // 3600
remainMinute = int(avatar["remained_time"]) % 60
txt, color = f"{str(remainHour) + 'H' if remainHour else ''}{' ' + str(remainMinute) + 'M' if remainMinute else ''}", "#675855"
txtWidth = font(20).getsize(txt)[0]
txtDraw.text((int(startWidth + (90 - txtWidth) / 2), startHeight + 60 + 90 + 15), txt, font=font(20), fill=color)
startWidth += 90 + 15
## 返回图片
# result.save(f"{resPath}ck/drawNotify.png", quality=100)
return result
# 请求米游社实时便笺接口
async def queryNotify(uid, server, cookie):
mhyUrl = f"https://api-takumi.mihoyo.com/game_record/app/genshin/api/dailyNote?role_id={uid}&server={server}"
# 计算拼接请求头 DS 字段
salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"
t = str(int(time.time()))
r = str(randint(100001, 200000))
c = md5("salt=" + salt + "&t=" + t + "&r=" + r + "&b=" + "&q=role_id=" + uid + "&server=" + server)
dsHeader = t + "," + r + "," + c
# 组织请求的 Header 信息
mhyHeaders = {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,en-US;q=0.8",
"DS": dsHeader,
"x-rpc-app_version": "2.11.1",
"x-rpc-client_type": "5",
"X-Requested-With": "com.mihoyo.hyperion",
"Cookie": cookie,
"Origin": "https://webstatic.mihoyo.com",
"Referer": "https://webstatic.mihoyo.com/",
"User-Agent": "miHoYoBBS/2.11.1",
}
# 请求
async with AsyncClient() as client:
try:
res = await client.get(mhyUrl, headers=mhyHeaders, timeout=10)
resJson = res.json()
# import json
# with open(f"{resPath}ck/res.json", "w", encoding="UTF-8") as f:
# json.dump(resJson, f, ensure_ascii=False, indent=2)
# f.close()
except Exception as e:
logger.error(str(sys.exc_info()[0]) + "\n" + str(e))
# 请求米哈游 API 出错返回空
return []
# 返回米哈游 API 查询结果
return resJson
# 生成实时便笺消息
async def genrNotify(uid, cookie):
uid, server = uidChecker(uid)
# UID 检查不合法返回空
if server == None:
return f"UID「{uid}」不合法!"
resJson = await queryNotify(uid, server, cookie)
try:
ntfImage = await drawNotify(uid, resJson)
b64 = img2Base64(ntfImage)
# return f"[CQ:image,file={b64}]"
return b64
except (KeyError, TypeError):
return "米游社接口返回不完整!"
except Exception as e:
logger.error("生成实时便笺消息出错:" + str(sys.exc_info()[0]) + "\n" + str(e))
return f"生成实时便笺消息出错\n{str(sys.exc_info()[0])}"
@monsterxcn
Copy link
Author

monsterxcn commented Oct 18, 2021

msg = await genrNotify(uid, cookie) 生成消息即可

uid 是作为命令输入读进来的,cookie 貌似和原神米游社签到那些用的是一样的,网页用的可能出错

图片素材传到 repo 了 https://github.com/monsterxcn/MyBucket/tree/master/Pictures/Genshin/ck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment