mirror of
				https://github.com/LiteyukiStudio/LiteyukiBot.git
				synced 2025-10-31 19:36:24 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			576 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			576 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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")
 |