预渲染数据,发送更快

This commit is contained in:
snowy 2024-04-22 21:05:35 +08:00
parent ae19113141
commit bd5f6c5205
14 changed files with 1178 additions and 93 deletions

View File

@ -27,13 +27,14 @@ superusers: [ "1919810" ] # 超级用户列表
```yaml
onebot_access_token: "" # 访问令牌,对公开放时建议设置
default_language: "zh-CN" # 默认语言
alconna_auto_completion: false # alconna是否自动补全指令默认false建议开启
# 开发者选项
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
alconna_auto_completion: false # alconna是否自动补全指令默认false建议开启
liteyuki_reload: false # 轻雪内置的重载选项,开启后在调试时修改代码或资源会自动重载相应内容
safe_mode: false # 安全模式,开启后将不会加载任何第三方插件
# 其他Nonebot插件的配置项
custom_config_1: "custom_value1"
custom_config_2: "custom_value2"

View File

@ -3,6 +3,7 @@ from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
from .runtime import *
from .dev_tools import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(

View File

@ -0,0 +1,43 @@
import time
import nonebot
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.reloader import Reloader
from liteyuki.utils.base.resource import load_resources
if get_config("liteyuki_reload", False):
nonebot.logger.info("Liteyuki Reload is enable, watching for file changes...")
class CodeModifiedHandler(FileSystemEventHandler):
def on_modified(self, event):
if "liteyuki/resources" not in event.src_path.replace("\\", "/"):
nonebot.logger.debug(f"{event.src_path} has been modified, reloading bot...")
Reloader.reload()
class ResourceModifiedHandler(FileSystemEventHandler):
def on_modified(self, event):
if not event.is_directory:
nonebot.logger.debug(f"{event.src_path} has been modified, reloading resource...")
load_resources()
code_modified_handler = CodeModifiedHandler()
resource_modified_handle = ResourceModifiedHandler()
observer = Observer()
observer.schedule(resource_modified_handle, path="liteyuki/resources", recursive=True)
observer.schedule(resource_modified_handle, path="resources", recursive=True)
observer.schedule(code_modified_handler, path="liteyuki", recursive=True)
observer.start()
# try:
# while True:
# time.sleep(1)
# except KeyboardInterrupt:
# observer.stop()
# observer.join()

View File

@ -3,18 +3,21 @@ import platform
import nonebot
import psutil
from cpuinfo import get_cpu_info
from nonebot import on_command
from nonebot import on_command, require
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.permission import SUPERUSER
from liteyuki.utils import __NAME__, __VERSION__, load_from_yaml
from liteyuki.utils.message.html_tool import template2image
from liteyuki.utils.base.language import Language, get_default_lang_code, get_user_lang
from liteyuki.utils.base.language import Language, get_all_lang, get_default_lang_code, get_user_lang
from liteyuki.utils.base.ly_typing import T_Bot, T_MessageEvent
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.tools import convert_size
from PIL import Image
from io import BytesIO
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler
stats = on_command("status", aliases={"状态"}, priority=5, permission=SUPERUSER)
config = load_from_yaml("config.yml")
@ -28,18 +31,52 @@ protocol_names = {
6: "Android Pad",
}
# 预存数据区
stats_data = {} # lang -> dict
image_data = {} # lang -> bytes
localization = {}
for lang in get_all_lang():
ulang = Language(lang)
localization[lang] = {
"cpu" : ulang.get("main.monitor.cpu"),
"mem" : ulang.get("main.monitor.memory"),
"swap" : ulang.get("main.monitor.swap"),
"disk" : ulang.get("main.monitor.disk"),
"used" : ulang.get("main.monitor.used"),
"free" : ulang.get("main.monitor.free"),
"total": ulang.get("main.monitor.total"),
}
@stats.handle()
async def _(bot: T_Bot, event: T_MessageEvent):
ulang = get_user_lang(str(event.user_id))
image = await template2image(
@scheduler.scheduled_job("cron", second="*/20")
async def _():
nonebot.logger.info("数据已刷新")
for lang_code in get_all_lang():
stats_data[lang_code] = await get_stats_data(lang=lang_code)
image_data[lang_code] = await template2image(
get_path("templates/stats.html", abs_path=True),
{
"data": await get_stats_data(bot.self_id, ulang.lang_code)
"data": stats_data[lang_code]
},
wait=1
)
image = await png_to_jpg(image)
@stats.handle()
async def _(bot: T_Bot, event: T_MessageEvent):
global stats_data
ulang = get_user_lang(str(event.user_id))
if ulang.lang_code not in image_data:
stats_data[ulang.lang_code] = await get_stats_data(lang=ulang.lang_code)
image_data[ulang.lang_code] = await template2image(
get_path("templates/stats.html", abs_path=True),
{
"data": stats_data[ulang.lang_code]
},
wait=1
)
image = await png_to_jpg(image_data[ulang.lang_code])
await stats.finish(MessageSegment.image(image))
@ -238,13 +275,6 @@ async def get_stats_data(self_id: str = None, lang: str = None) -> dict:
f"{ulang.get('main.monitor.free')} {convert_size(swap_info.free, 1)}",
f"{ulang.get('main.monitor.total')} {convert_size(swap_info.total, 1)}",
],
"cpu_trans" : ulang.get("main.monitor.cpu"),
"mem_trans" : ulang.get("main.monitor.memory"),
"swap_trans" : ulang.get("main.monitor.swap"),
"disk_trans" : ulang.get("main.monitor.disk"),
"used_trans" : ulang.get("main.monitor.used"),
"free_trans" : ulang.get("main.monitor.free"),
"total_trans": ulang.get("main.monitor.total"),
"localization": localization[ulang.lang_code]
}
return templ

View File

@ -0,0 +1,15 @@
from nonebot.plugin import PluginMetadata
from .rt_guide import *
__plugin_meta__ = PluginMetadata(
name="CRT生成工具",
description="一些CRT牌子生成器",
usage="我觉得你应该会用",
type="application",
homepage="https://github.com/snowykami/LiteyukiBot",
extra={
"liteyuki" : True,
"toggleable" : True,
"default_enable": True,
}
)

View File

@ -0,0 +1,575 @@
import os
import uuid
from typing import Tuple, Union, List
import nonebot
from PIL import Image, ImageFont, ImageDraw
default_color = (255, 255, 255, 255)
default_font = "resources/fonts/MiSans-Semibold.ttf"
def render_canvas_from_json(file: str, background: Image) -> "Canvas":
pass
class BasePanel:
def __init__(self,
uv_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
box_size: Tuple[Union[int, float], Union[int, float]] = (1.0, 1.0),
parent_point: Tuple[float, float] = (0.5, 0.5),
point: Tuple[float, float] = (0.5, 0.5)):
"""
:param uv_size: 底面板大小
:param box_size: 自身面板大小
:param parent_point: 底面板锚点
:param point: 自身面板锚点
"""
self.canvas: Canvas | None = None
self.uv_size = uv_size
self.box_size = box_size
self.parent_point = parent_point
self.point = point
self.parent: BasePanel | None = None
self.canvas_box: Tuple[float, float, float, float] = (0.0, 0.0, 1.0, 1.0)
# 此节点在父节点上的盒子
self.box = (
self.parent_point[0] - self.point[0] * self.box_size[0] / self.uv_size[0],
self.parent_point[1] - self.point[1] * self.box_size[1] / self.uv_size[1],
self.parent_point[0] + (1 - self.point[0]) * self.box_size[0] / self.uv_size[0],
self.parent_point[1] + (1 - self.point[1]) * self.box_size[1] / self.uv_size[1]
)
def load(self, only_calculate=False):
"""
将对象写入画布
此处仅作声明
由各子类重写
:return:
"""
self.actual_pos = self.canvas_box
def save_as(self, canvas_box, only_calculate=False):
"""
此函数执行时间较长建议异步运行
:param only_calculate:
:param canvas_box 此节点在画布上的盒子,并不是在父节点上的盒子
:return:
"""
for name, child in self.__dict__.items():
# 此节点在画布上的盒子
if isinstance(child, BasePanel) and name not in ["canvas", "parent"]:
child.parent = self
if isinstance(self, Canvas):
child.canvas = self
else:
child.canvas = self.canvas
dxc = canvas_box[2] - canvas_box[0]
dyc = canvas_box[3] - canvas_box[1]
child.canvas_box = (
canvas_box[0] + dxc * child.box[0],
canvas_box[1] + dyc * child.box[1],
canvas_box[0] + dxc * child.box[2],
canvas_box[1] + dyc * child.box[3]
)
child.load(only_calculate)
child.save_as(child.canvas_box, only_calculate)
class Canvas(BasePanel):
def __init__(self, base_img: Image.Image):
self.base_img = base_img
self.canvas = self
super(Canvas, self).__init__()
self.draw_line_list = []
def export(self, file, alpha=False):
self.base_img = self.base_img.convert("RGBA")
self.save_as((0, 0, 1, 1))
draw = ImageDraw.Draw(self.base_img)
for line in self.draw_line_list:
draw.line(*line)
if not alpha:
self.base_img = self.base_img.convert("RGB")
self.base_img.save(file)
def delete(self):
os.remove(self.file)
def get_actual_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
"""
获取控件实际相对大小
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
return sub_obj.actual_pos
def get_actual_pixel_size(self, path: str) -> Union[None, Tuple[int, int]]:
"""
获取控件实际像素长宽
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
dx = int(sub_obj.canvas.base_img.size[0] * (sub_obj.actual_pos[2] - sub_obj.actual_pos[0]))
dy = int(sub_obj.canvas.base_img.size[1] * (sub_obj.actual_pos[3] - sub_obj.actual_pos[1]))
return dx, dy
def get_actual_pixel_box(self, path: str) -> Union[None, Tuple[int, int, int, int]]:
"""
获取控件实际像素大小盒子
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
x1 = int(sub_obj.canvas.base_img.size[0] * sub_obj.actual_pos[0])
y1 = int(sub_obj.canvas.base_img.size[1] * sub_obj.actual_pos[1])
x2 = int(sub_obj.canvas.base_img.size[2] * sub_obj.actual_pos[2])
y2 = int(sub_obj.canvas.base_img.size[3] * sub_obj.actual_pos[3])
return x1, y1, x2, y2
def get_parent_box(self, path: str) -> Union[None, Tuple[float, float, float, float]]:
"""
获取控件在父节点的大小
函数执行时间较长
:param path: 控件路径
:return:
"""
sub_obj = self.get_control_by_path(path)
on_parent_pos = (
(sub_obj.actual_pos[0] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
(sub_obj.actual_pos[1] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1]),
(sub_obj.actual_pos[2] - sub_obj.parent.actual_pos[0]) / (sub_obj.parent.actual_pos[2] - sub_obj.parent.actual_pos[0]),
(sub_obj.actual_pos[3] - sub_obj.parent.actual_pos[1]) / (sub_obj.parent.actual_pos[3] - sub_obj.parent.actual_pos[1])
)
return on_parent_pos
def get_control_by_path(self, path: str) -> Union[BasePanel, "Img", "Rectangle", "Text"]:
sub_obj = self
self.save_as((0, 0, 1, 1), True)
control_path = ""
for i, seq in enumerate(path.split(".")):
if seq not in sub_obj.__dict__:
raise KeyError(f"{control_path}中找不到控件:{seq}")
control_path += f".{seq}"
sub_obj = sub_obj.__dict__[seq]
return sub_obj
def draw_line(self, path: str, p1: Tuple[float, float], p2: Tuple[float, float], color, width):
"""
画线
:param color:
:param width:
:param path:
:param p1:
:param p2:
:return:
"""
ac_pos = self.get_actual_box(path)
control = self.get_control_by_path(path)
dx = ac_pos[2] - ac_pos[0]
dy = ac_pos[3] - ac_pos[1]
xy_box = int((ac_pos[0] + dx * p1[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p1[1]) * control.canvas.base_img.size[1]), int(
(ac_pos[0] + dx * p2[0]) * control.canvas.base_img.size[0]), int((ac_pos[1] + dy * p2[1]) * control.canvas.base_img.size[1])
self.draw_line_list.append((xy_box, color, width))
class Panel(BasePanel):
def __init__(self, uv_size, box_size, parent_point, point):
super(Panel, self).__init__(uv_size, box_size, parent_point, point)
class TextSegment:
def __init__(self, text, **kwargs):
if not isinstance(text, str):
raise TypeError("请输入字符串")
self.text = text
self.color = kwargs.get("color", None)
self.font = kwargs.get("font", None)
@staticmethod
def text2text_segment_list(text: str):
"""
暂时没写好
:param text: %FFFFFFFF%1123%FFFFFFFF%21323
:return:
"""
pass
class Text(BasePanel):
def __init__(self, uv_size, box_size, parent_point, point, text: Union[str, list], font=default_font, color=(255, 255, 255, 255), vertical=False,
line_feed=False, force_size=False, fill=(0, 0, 0, 0), fillet=0, outline=(0, 0, 0, 0), outline_width=0, rectangle_side=0, font_size=None, dp: int = 5,
anchor: str = "la"):
"""
:param uv_size:
:param box_size:
:param parent_point:
:param point:
:param text: list[TextSegment] | str
:param font:
:param color:
:param vertical: 是否竖直
:param line_feed: 是否换行
:param force_size: 强制大小
:param dp: 字体大小递减精度
:param anchor : https://www.zhihu.com/question/474216280
:param fill: 底部填充颜色
:param fillet: 填充圆角
:param rectangle_side: 边框宽度
:param outline: 填充矩形边框颜色
:param outline_width: 填充矩形边框宽度
"""
self.actual_pos = None
self.outline_width = outline_width
self.outline = outline
self.fill = fill
self.fillet = fillet
self.font = font
self.text = text
self.color = color
self.force_size = force_size
self.vertical = vertical
self.line_feed = line_feed
self.dp = dp
self.font_size = font_size
self.rectangle_side = rectangle_side
self.anchor = anchor
super(Text, self).__init__(uv_size, box_size, parent_point, point)
def load(self, only_calculate=False):
"""限制区域像素大小"""
if isinstance(self.text, str):
self.text = [
TextSegment(text=self.text, color=self.color, font=self.font)
]
all_text = str()
for text in self.text:
all_text += text.text
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
font_size = limited_size[1] if self.font_size is None else self.font_size
image_font = ImageFont.truetype(self.font, font_size)
actual_size = image_font.getsize(all_text)
while (actual_size[0] > limited_size[0] or actual_size[1] > limited_size[1]) and not self.force_size:
font_size -= self.dp
image_font = ImageFont.truetype(self.font, font_size)
actual_size = image_font.getsize(all_text)
draw = ImageDraw.Draw(self.canvas.base_img)
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
self.parent.canvas_box = self.parent.actual_pos
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
dx1 = actual_size[0] / self.canvas.base_img.size[0]
dy1 = actual_size[1] / self.canvas.base_img.size[1]
start_point = [
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
]
self.actual_pos = (
start_point[0] / self.canvas.base_img.size[0],
start_point[1] / self.canvas.base_img.size[1],
(start_point[0] + actual_size[0]) / self.canvas.base_img.size[0],
(start_point[1] + actual_size[1]) / self.canvas.base_img.size[1],
)
self.font_size = font_size
if not only_calculate:
for text_segment in self.text:
if text_segment.color is None:
text_segment.color = self.color
if text_segment.font is None:
text_segment.font = self.font
image_font = ImageFont.truetype(font=text_segment.font, size=font_size)
if self.fill[-1] > 0:
rectangle = Shape.rectangle(size=(actual_size[0] + 2 * self.rectangle_side, actual_size[1] + 2 * self.rectangle_side), fillet=self.fillet, fill=self.fill,
width=self.outline_width, outline=self.outline)
self.canvas.base_img.paste(im=rectangle, box=(start_point[0] - self.rectangle_side,
start_point[1] - self.rectangle_side,
start_point[0] + actual_size[0] + self.rectangle_side,
start_point[1] + actual_size[1] + self.rectangle_side),
mask=rectangle.split()[-1])
draw.text((start_point[0] - self.rectangle_side, start_point[1] - self.rectangle_side),
text_segment.text, text_segment.color, font=image_font, anchor=self.anchor)
text_width = image_font.getsize(text_segment.text)
start_point[0] += text_width[0]
class Img(BasePanel):
def __init__(self, uv_size, box_size, parent_point, point, img: Image.Image, keep_ratio=True):
self.img_base_img = img
self.keep_ratio = keep_ratio
super(Img, self).__init__(uv_size, box_size, parent_point, point)
def load(self, only_calculate=False):
self.preprocess()
self.img_base_img = self.img_base_img.convert("RGBA")
limited_size = int((self.canvas_box[2] - self.canvas_box[0]) * self.canvas.base_img.size[0]), \
int((self.canvas_box[3] - self.canvas_box[1]) * self.canvas.base_img.size[1])
if self.keep_ratio:
"""保持比例"""
actual_ratio = self.img_base_img.size[0] / self.img_base_img.size[1]
limited_ratio = limited_size[0] / limited_size[1]
if actual_ratio >= limited_ratio:
# 图片过长
self.img_base_img = self.img_base_img.resize(
(int(self.img_base_img.size[0] * limited_size[0] / self.img_base_img.size[0]),
int(self.img_base_img.size[1] * limited_size[0] / self.img_base_img.size[0]))
)
else:
self.img_base_img = self.img_base_img.resize(
(int(self.img_base_img.size[0] * limited_size[1] / self.img_base_img.size[1]),
int(self.img_base_img.size[1] * limited_size[1] / self.img_base_img.size[1]))
)
else:
"""不保持比例"""
self.img_base_img = self.img_base_img.resize(limited_size)
# 占比长度
if isinstance(self.parent, Img) or isinstance(self.parent, Text):
self.parent.canvas_box = self.parent.actual_pos
dx0 = self.parent.canvas_box[2] - self.parent.canvas_box[0]
dy0 = self.parent.canvas_box[3] - self.parent.canvas_box[1]
dx1 = self.img_base_img.size[0] / self.canvas.base_img.size[0]
dy1 = self.img_base_img.size[1] / self.canvas.base_img.size[1]
start_point = (
int((self.parent.canvas_box[0] + dx0 * self.parent_point[0] - dx1 * self.point[0]) * self.canvas.base_img.size[0]),
int((self.parent.canvas_box[1] + dy0 * self.parent_point[1] - dy1 * self.point[1]) * self.canvas.base_img.size[1])
)
alpha = self.img_base_img.split()[3]
self.actual_pos = (
start_point[0] / self.canvas.base_img.size[0],
start_point[1] / self.canvas.base_img.size[1],
(start_point[0] + self.img_base_img.size[0]) / self.canvas.base_img.size[0],
(start_point[1] + self.img_base_img.size[1]) / self.canvas.base_img.size[1],
)
if not only_calculate:
self.canvas.base_img.paste(self.img_base_img, start_point, alpha)
def preprocess(self):
pass
class Rectangle(Img):
def __init__(self, uv_size, box_size, parent_point, point, fillet: Union[int, float] = 0, img: Union[Image.Image] = None, keep_ratio=True,
color=default_color, outline_width=0, outline_color=default_color):
"""
圆角图
:param uv_size:
:param box_size:
:param parent_point:
:param point:
:param fillet: 圆角半径浮点或整数
:param img:
:param keep_ratio:
"""
self.fillet = fillet
self.color = color
self.outline_width = outline_width
self.outline_color = outline_color
super(Rectangle, self).__init__(uv_size, box_size, parent_point, point, img, keep_ratio)
def preprocess(self):
limited_size = (int(self.canvas.base_img.size[0] * (self.canvas_box[2] - self.canvas_box[0])),
int(self.canvas.base_img.size[1] * (self.canvas_box[3] - self.canvas_box[1])))
if not self.keep_ratio and self.img_base_img is not None and self.img_base_img.size[0] / self.img_base_img.size[1] != limited_size[0] / limited_size[1]:
self.img_base_img = self.img_base_img.resize(limited_size)
self.img_base_img = Shape.rectangle(size=limited_size, fillet=self.fillet, fill=self.color, width=self.outline_width, outline=self.outline_color)
class Color:
GREY = (128, 128, 128, 255)
RED = (255, 0, 0, 255)
GREEN = (0, 255, 0, 255)
BLUE = (0, 0, 255, 255)
YELLOW = (255, 255, 0, 255)
PURPLE = (255, 0, 255, 255)
CYAN = (0, 255, 255, 255)
WHITE = (255, 255, 255, 255)
BLACK = (0, 0, 0, 255)
@staticmethod
def hex2dec(colorHex: str) -> Tuple[int, int, int, int]:
"""
:param colorHex: FFFFFFFF ARGB-> (R, G, B, A)
:return:
"""
return int(colorHex[2:4], 16), int(colorHex[4:6], 16), int(colorHex[6:8], 16), int(colorHex[0:2], 16)
class Shape:
@staticmethod
def circular(radius: int, fill: tuple, width: int = 0, outline: tuple = Color.BLACK) -> Image.Image:
"""
:param radius: 半径像素
:param fill: 填充颜色
:param width: 轮廓粗细像素
:param outline: 轮廓颜色
:return: 圆形Image对象
"""
img = Image.new("RGBA", (radius * 2, radius * 2), color=radius)
draw = ImageDraw.Draw(img)
draw.ellipse(xy=(0, 0, radius * 2, radius * 2), fill=fill, outline=outline, width=width)
return img
@staticmethod
def rectangle(size: Tuple[int, int], fill: tuple, width: int = 0, outline: tuple = Color.BLACK, fillet: int = 0) -> Image.Image:
"""
:param fillet: 圆角半径像素
:param size: 长宽像素
:param fill: 填充颜色
:param width: 轮廓粗细像素
:param outline: 轮廓颜色
:return: 矩形Image对象
"""
img = Image.new("RGBA", size, color=fill)
draw = ImageDraw.Draw(img)
draw.rounded_rectangle(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline, width=width, radius=fillet)
return img
@staticmethod
def ellipse(size: Tuple[int, int], fill: tuple, outline: int = 0, outline_color: tuple = Color.BLACK) -> Image.Image:
"""
:param size: 长宽像素
:param fill: 填充颜色
:param outline: 轮廓粗细像素
:param outline_color: 轮廓颜色
:return: 椭圆Image对象
"""
img = Image.new("RGBA", size, color=fill)
draw = ImageDraw.Draw(img)
draw.ellipse(xy=(0, 0, size[0], size[1]), fill=fill, outline=outline_color, width=outline)
return img
@staticmethod
def polygon(points: List[Tuple[int, int]], fill: tuple, outline: int, outline_color: tuple) -> Image.Image:
"""
:param points: 多边形顶点列表
:param fill: 填充颜色
:param outline: 轮廓粗细像素
:param outline_color: 轮廓颜色
:return: 多边形Image对象
"""
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
draw = ImageDraw.Draw(img)
draw.polygon(xy=points, fill=fill, outline=outline_color, width=outline)
return img
@staticmethod
def line(points: List[Tuple[int, int]], fill: tuple, width: int) -> Image:
"""
:param points: 线段顶点列表
:param fill: 填充颜色
:param width: 线段粗细像素
:return: 线段Image对象
"""
img = Image.new("RGBA", (max(points)[0], max(points)[1]), color=fill)
draw = ImageDraw.Draw(img)
draw.line(xy=points, fill=fill, width=width)
return img
class Utils:
@staticmethod
def central_clip_by_ratio(img: Image.Image, size: Tuple, use_cache=True):
"""
:param use_cache: 是否使用缓存剪切过一次后默认生成缓存
:param img:
:param size: 仅为比例满填充裁剪
:return:
"""
cache_file_path = str()
if use_cache:
filename_without_end = ".".join(os.path.basename(img.fp.name).split(".")[0:-1]) + f"_{size[0]}x{size[1]}" + ".png"
cache_file_path = os.path.join(".cache", filename_without_end)
if os.path.exists(cache_file_path):
nonebot.logger.info("本次使用缓存加载图片,不裁剪")
return Image.open(os.path.join(".cache", filename_without_end))
img_ratio = img.size[0] / img.size[1]
limited_ratio = size[0] / size[1]
if limited_ratio > img_ratio:
actual_size = (
img.size[0],
img.size[0] / size[0] * size[1]
)
box = (
0, (img.size[1] - actual_size[1]) // 2,
img.size[0], img.size[1] - (img.size[1] - actual_size[1]) // 2
)
else:
actual_size = (
img.size[1] / size[1] * size[0],
img.size[1],
)
box = (
(img.size[0] - actual_size[0]) // 2, 0,
img.size[0] - (img.size[0] - actual_size[0]) // 2, img.size[1]
)
img = img.crop(box).resize(size)
if use_cache:
img.save(cache_file_path)
return img
@staticmethod
def circular_clip(img: Image.Image):
"""
裁剪为alpha圆形
:param img:
:return:
"""
length = min(img.size)
alpha_cover = Image.new("RGBA", (length, length), color=(0, 0, 0, 0))
if img.size[0] > img.size[1]:
box = (
(img.size[0] - img[1]) // 2, 0,
(img.size[0] - img[1]) // 2 + img.size[1], img.size[1]
)
else:
box = (
0, (img.size[1] - img.size[0]) // 2,
img.size[0], (img.size[1] - img.size[0]) // 2 + img.size[0]
)
img = img.crop(box).resize((length, length))
draw = ImageDraw.Draw(alpha_cover)
draw.ellipse(xy=(0, 0, length, length), fill=(255, 255, 255, 255))
alpha = alpha_cover.split()[-1]
img.putalpha(alpha)
return img
@staticmethod
def open_img(path) -> Image.Image:
return Image.open(path, "RGBA")

View File

@ -0,0 +1,419 @@
import json
from typing import List, Any
from PIL import Image
from arclet.alconna import Alconna
from nb_cli import run_sync
from nonebot import on_command
from nonebot_plugin_alconna import on_alconna, Alconna, Subcommand, Args, MultiVar, Arparma, UniMessage
from pydantic import BaseModel
from .canvas import *
from ...utils.base.resource import get_path
resolution = 256
class Entrance(BaseModel):
identifier: str
size: tuple[int, int]
dest: List[str]
class Station(BaseModel):
identifier: str
chineseName: str
englishName: str
position: tuple[int, int]
class Line(BaseModel):
identifier: str
chineseName: str
englishName: str
color: Any
stations: List["Station"]
font_light = get_path("templates/fonts/MiSans/MiSans-Light.woff2")
font_bold = get_path("templates/fonts/MiSans/MiSans-Bold.woff2")
@run_sync
def generate_entrance_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution):
"""
Generates an entrance sign for the ride.
"""
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.WHITE))
# 加黑色图框
baseCanvas.outline = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0),
point=(0, 0),
img=Shape.rectangle(
size=(width, height),
fillet=0,
fill=(0, 0, 0, 0),
width=15,
outline=Color.BLACK
)
)
baseCanvas.contentPanel = Panel(
uv_size=(width, height),
box_size=(width - 28, height - 28),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
linePanelHeight = 0.7 * ratio[1]
linePanelWidth = linePanelHeight * 1.3
# 画线路面板部分
for i, line in enumerate(lineInfo):
linePanel = baseCanvas.contentPanel.__dict__[f"Line_{i}_Panel"] = Panel(
uv_size=ratio,
box_size=(linePanelWidth, linePanelHeight),
parent_point=(i * linePanelWidth / ratio[0], 1),
point=(0, 1),
)
linePanel.colorCube = Img(
uv_size=(1, 1),
box_size=(0.15, 1),
parent_point=(0.125, 1),
point=(0, 1),
img=Shape.rectangle(
size=(100, 100),
fillet=0,
fill=line.color,
),
keep_ratio=False
)
textPanel = linePanel.TextPanel = Panel(
uv_size=(1, 1),
box_size=(0.625, 1),
parent_point=(1, 1),
point=(1, 1)
)
# 中文线路名
textPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 2 / 3),
parent_point=(0, 0),
point=(0, 0),
)
nameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.namePanel".format(i))
textPanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.chineseName,
color=Color.BLACK,
font_size=int(nameSize[1] * 0.5),
force_size=True,
font=font_bold
)
# 英文线路名
textPanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 1 / 3),
parent_point=(0, 1),
point=(0, 1),
)
englishNameSize = baseCanvas.get_actual_pixel_size("contentPanel.Line_{}_Panel.TextPanel.englishNamePanel".format(i))
textPanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=line.englishName,
color=Color.BLACK,
font_size=int(englishNameSize[1] * 0.6),
force_size=True,
font=font_light
)
# 画名称部分
namePanel = baseCanvas.contentPanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.4),
parent_point=(0.5, 0),
point=(0.5, 0),
)
namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
text=name,
color=Color.BLACK,
font_size=int(height * 0.3),
force_size=True,
font=font_bold
)
aliasesPanel = baseCanvas.contentPanel.aliasesPanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(0.5, 1),
point=(0.5, 1),
)
for j, alias in enumerate(aliases):
aliasesPanel.__dict__[alias] = Text(
uv_size=(1, 1),
box_size=(0.35, 0.5),
parent_point=(0.5, 0.5 * j),
point=(0.5, 0),
text=alias,
color=Color.BLACK,
font_size=int(height * 0.15),
font=font_light
)
# 画入口标识
entrancePanel = baseCanvas.contentPanel.entrancePanel = Panel(
uv_size=(1, 1),
box_size=(0.2, 1),
parent_point=(1, 0.5),
point=(1, 0.5),
)
# 中文文本
entrancePanel.namePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 0),
point=(1, 0),
)
entrancePanel.namePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"{entranceIdentifier}出入口",
color=Color.BLACK,
font_size=int(height * 0.2),
force_size=True,
font=font_bold
)
# 英文文本
entrancePanel.englishNamePanel = Panel(
uv_size=(1, 1),
box_size=(1, 0.5),
parent_point=(1, 1),
point=(1, 1),
)
entrancePanel.englishNamePanel.text = Text(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0, 0.5),
point=(0, 0.5),
text=f"Entrance {entranceIdentifier}",
color=Color.BLACK,
font_size=int(height * 0.15),
force_size=True,
font=font_light
)
return baseCanvas.base_img.tobytes()
crt_alc = on_alconna(
Alconna(
"crt",
Subcommand(
"entrance",
Args["name", str]["lines", str, ""]["entrance", int, 1], # /crt entrance 璧山&Bishan 1号线&Line1&#ff0000,27号线&Line1&#ff0000 1A
)
)
)
@crt_alc.assign("entrance")
async def _(result: Arparma):
args = result.subcommands.get("entrance").args
name = args["name"]
lines = args["lines"]
entrance = args["entrance"]
line_info = []
for line in lines.split(","):
line_args = line.split("&")
line_info.append(Line(
identifier=1,
chineseName=line_args[0],
englishName=line_args[1],
color=line_args[2],
stations=[]
))
img_bytes = await generate_entrance_sign(
name=name,
aliases=name.split("&"),
lineInfo=line_info,
entranceIdentifier=entrance,
ratio=(8, 1),
reso=256,
)
await crt_alc.finish(
UniMessage.image(raw=img_bytes)
)
def generate_platform_line_pic(line: Line, station: Station, ratio=None, reso: int = resolution):
"""
生成站台线路图
:param line: 线路对象
:param station: 本站点对象
:param ratio: 比例
:param reso: 分辨率1reso
:return: 两个方向的站牌
"""
if ratio is None:
ratio = [4, 1]
width, height = ratio[0] * reso, ratio[1] * reso
baseCanvas = Canvas(Image.new("RGBA", (width, height), Color.YELLOW))
# 加黑色图框
baseCanvas.linePanel = Panel(
uv_size=(1, 1),
box_size=(0.8, 0.15),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
)
# 直线块
baseCanvas.linePanel.recLine = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=line.color,
),
keep_ratio=False
)
# 灰色直线块
baseCanvas.linePanel.recLineGrey = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.rectangle(
size=(10, 10),
fill=Color.GREY,
),
keep_ratio=False
)
# 生成各站圆点
outline_width = 40
circleForward = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
)
circleThisPanel = Canvas(Image.new("RGBA", (200, 200), (0, 0, 0, 0)))
circleThisPanel.circleOuter = Img(
uv_size=(1, 1),
box_size=(1, 1),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=line.color,
),
)
circleThisPanel.circleOuter.circleInner = Img(
uv_size=(1, 1),
box_size=(0.7, 0.7),
parent_point=(0.5, 0.5),
point=(0.5, 0.5),
img=Shape.circular(
radius=200,
fill=line.color,
width=0,
outline=line.color,
),
)
circleThisPanel.export("a.png", alpha=True)
circleThis = circleThisPanel.base_img
circlePassed = Shape.circular(
radius=200,
fill=Color.WHITE,
width=outline_width,
outline=Color.GREY,
)
arrival = False
distance = 1 / (len(line.stations) - 1)
for i, sta in enumerate(line.stations):
box_size = (1.618, 1.618)
if sta.identifier == station.identifier:
arrival = True
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=(1.8, 1.8),
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleThis,
keep_ratio=True
)
continue
if arrival:
# 后方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circleForward,
keep_ratio=True
)
else:
# 前方站绘制
baseCanvas.linePanel.recLine.__dict__["station_{}".format(sta.identifier)] = Img(
uv_size=(1, 1),
box_size=box_size,
parent_point=(distance * i, 0.5),
point=(0.5, 0.5),
img=circlePassed,
keep_ratio=True
)
return baseCanvas
def generate_platform_sign(name: str, aliases: List[str], lineInfo: List[Line], entranceIdentifier: str, ratio: tuple[int | float, int | float],
reso: int = resolution
):
pass
# def main():
# generate_entrance_sign(
# "璧山",
# aliases=["Bishan"],
# lineInfo=[
#
# Line(identifier="2", chineseName="1号线", englishName="Line 1", color=Color.RED, stations=[]),
# Line(identifier="3", chineseName="27号线", englishName="Line 27", color="#685bc7", stations=[]),
# Line(identifier="1", chineseName="璧铜线", englishName="BT Line", color="#685BC7", stations=[]),
# ],
# entranceIdentifier="1",
# ratio=(8, 1)
# )
#
#
# main()

View File

@ -6,10 +6,10 @@ from liteyuki.utils.base.config import get_config
from liteyuki.utils.base.ly_typing import T_MessageEvent
from .qw_api import *
from ...utils.base.data_manager import User, user_db
from ...utils.base.language import get_user_lang
from ...utils.base.resource import get_path
from ...utils.message.html_tool import template2image
from liteyuki.utils.base.data_manager import User, user_db
from liteyuki.utils.base.language import get_user_lang
from liteyuki.utils.base.resource import get_path
from liteyuki.utils.message.html_tool import template2image
require("nonebot_plugin_alconna")
from nonebot_plugin_alconna import on_alconna, Alconna, Args, MultiVar, Arparma

View File

@ -132,6 +132,4 @@ rpm.move_top=置顶
weather.city_not_found=未找到城市 {CITY}
weather.weather_not_found=未找到城市 {CITY} 的天气信息
weather.no_key=未设置天气api key请在配置文件添加weather-key
weather.no_key=未设置天气api key请在配置文件添加weather_key

View File

@ -25,9 +25,9 @@
infoDiv.appendChild(tagSpan);
});
}
cpuInfo.setOption(getPieOption(data.cpu_trans, cpuData));
memInfo.setOption(getPieOption(data.mem_trans, memData));
swapInfo.setOption(getPieOption(data.swap_trans, swapData));
cpuInfo.setOption(getPieOption(data.localization.cpu, cpuData));
memInfo.setOption(getPieOption(data.localization.mem, memData));
swapInfo.setOption(getPieOption(data.localization.swap, swapData));
// 在disks-info中插入每个disk的div用横向柱状图表示用量每一行div显示一个disk不加info-box
@ -40,7 +40,7 @@
diskDiv.appendChild(diskChart);
let diskInfo = echarts.init(diskChart);
// let diskTitle = disk.name + ' {{ FREE }} ' + disk.free + ' {{ TOTAL }} ' + disk.total;
let diskTitle = `${disk.name} ${data.free_trans} ${disk.free} ${data.total_trans} ${disk.total}`;
let diskTitle = `${disk.name} ${data.localization.free} ${disk.free} ${data.localization.total} ${disk.total}`;
diskInfo.setOption(getBarOption(diskTitle, disk.percent));
});

View File

@ -50,6 +50,7 @@ def init():
"""
# 检测python版本是否高于3.10
init_log()
if sys.version_info < (3, 10):
nonebot.logger.error("This project requires Python3.10+ to run, please upgrade your Python Environment.")
exit(1)

View File

@ -6,6 +6,7 @@ import aiohttp
from PIL import Image
from ..base.config import get_config
from ..base.data import LiteModel
from ..base.ly_typing import T_Bot
@ -195,3 +196,14 @@ class Mqqapi:
# 若命令前缀不为空,则使用配置的第一个命令前缀
cmd = f"{command_start[0]}{cmd}"
return f"[{text}](mqqapi://aio/inlinecmd?command={quote(cmd)}&reply={str(reply).lower()}&enter={str(enter).lower()})"
class RenderData(LiteModel):
label: str
visited_label: str
style: int
class Button(LiteModel):
id: int
render_data: RenderData

View File

@ -64,28 +64,6 @@ class MarkdownMessage:
message_type = event.message_type
session_id = event.user_id if event.message_type == "private" else event.group_id
try:
# 构建Markdown消息并获取转发消息ID
# forward_id = await bot.call_api(
# api="send_forward_msg",
# messages=[
# v11.MessageSegment(
# type="node",
# data={
# "name" : "Liteyuki.OneBot",
# "uin" : bot.self_id,
# "content": [
# {
# "type": "markdown",
# "data": {
# "content": '{"content":"%s"}' % formatted_md
# }
# },
# ]
# },
# )
# ]
# )
# 发送Markdown longmsg并获取相应数据
data = await bot.send_msg(
user_id=session_id,
group_id=session_id,
@ -94,27 +72,39 @@ class MarkdownMessage:
{
"type": "markdown",
"data": {
"content": "{\"content\":\"%s\"}" % formatted_md
"content": "{\"content\":\"%s\"}" % formatted_md,
}
}
],
# messages=[
# v11.MessageSegment(
# type="node",
# data={
# "name": "Liteyuki.OneBot",
# "uin": bot.self_id,
# "content": [
},
# {
# "type": "markdown",
# "type": "keyboard",
# "data": {
# "content": '{"content":"%s"}' % formatted_md
# "content": {
# "rows": [
# {
# "buttons": [
# {
# "render_data": {
# "label" : "NPM",
# "visited_label": "NPM已点击",
# "style" : 1
# },
# "action" : {
# "type" : 2,
# "enter" : True,
# "permission": {
# "type": 2
# },
# "data" : "npm"
#
# }
# }
# ]
# }
# ),
# ],
# ]
# }
# }
# }
],
**kwargs
)
except BaseException as e: