2024-08-30 23:50:45 +08:00
|
|
|
|
import base64
|
|
|
|
|
|
import contextlib
|
2024-02-04 04:18:54 +08:00
|
|
|
|
from io import BytesIO
|
2024-12-10 19:49:11 +08:00
|
|
|
|
import itertools
|
|
|
|
|
|
import math
|
2024-02-04 04:18:54 +08:00
|
|
|
|
from pathlib import Path
|
2024-08-30 23:50:45 +08:00
|
|
|
|
from typing import Literal, TypeAlias, overload
|
2024-12-10 19:49:11 +08:00
|
|
|
|
from typing_extensions import Self
|
|
|
|
|
|
import uuid
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
from nonebot.utils import run_sync
|
2024-12-10 19:49:11 +08:00
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
2024-02-04 04:18:54 +08:00
|
|
|
|
from PIL.Image import Image as tImage
|
2025-01-07 14:36:22 +08:00
|
|
|
|
from PIL.Image import Resampling, Transpose
|
2024-02-04 04:18:54 +08:00
|
|
|
|
from PIL.ImageFont import FreeTypeFont
|
|
|
|
|
|
|
|
|
|
|
|
from zhenxun.configs.path_config import FONT_PATH
|
|
|
|
|
|
|
|
|
|
|
|
ModeType = Literal[
|
|
|
|
|
|
"1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"
|
|
|
|
|
|
]
|
|
|
|
|
|
"""图片类型"""
|
|
|
|
|
|
|
2024-08-30 23:50:45 +08:00
|
|
|
|
ColorAlias: TypeAlias = str | tuple[int, int, int] | tuple[int, int, int, int] | None
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
CenterType = Literal["center", "height", "width"]
|
|
|
|
|
|
"""
|
|
|
|
|
|
粘贴居中
|
|
|
|
|
|
|
|
|
|
|
|
center: 水平垂直居中
|
|
|
|
|
|
|
|
|
|
|
|
height: 垂直居中
|
|
|
|
|
|
|
|
|
|
|
|
width: 水平居中
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BuildImage:
|
|
|
|
|
|
"""
|
|
|
|
|
|
快捷生成图片与操作图片的工具类
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
width: int = 0,
|
|
|
|
|
|
height: int = 0,
|
2024-02-25 03:18:34 +08:00
|
|
|
|
color: ColorAlias = (255, 255, 255),
|
2024-02-04 04:18:54 +08:00
|
|
|
|
mode: ModeType = "RGBA",
|
|
|
|
|
|
font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf",
|
|
|
|
|
|
font_size: int = 20,
|
2024-08-24 12:30:49 +08:00
|
|
|
|
background: str | BytesIO | Path | bytes | None = None,
|
2024-02-04 04:18:54 +08:00
|
|
|
|
) -> None:
|
2024-02-25 03:18:34 +08:00
|
|
|
|
self.uid = uuid.uuid1()
|
2024-02-04 04:18:54 +08:00
|
|
|
|
self.width = width
|
|
|
|
|
|
self.height = height
|
|
|
|
|
|
self.color = color
|
|
|
|
|
|
self.font = (
|
2024-08-30 23:50:45 +08:00
|
|
|
|
font if isinstance(font, FreeTypeFont) else self.load_font(font, font_size)
|
2024-02-04 04:18:54 +08:00
|
|
|
|
)
|
|
|
|
|
|
if background:
|
2024-08-24 12:30:49 +08:00
|
|
|
|
if isinstance(background, bytes):
|
|
|
|
|
|
self.markImg = Image.open(BytesIO(background))
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.markImg = Image.open(background)
|
2024-02-04 04:18:54 +08:00
|
|
|
|
if width and height:
|
2025-01-07 14:36:22 +08:00
|
|
|
|
self.markImg = self.markImg.resize((width, height), Resampling.LANCZOS)
|
2024-05-16 00:29:52 +08:00
|
|
|
|
else:
|
|
|
|
|
|
self.width = self.markImg.width
|
|
|
|
|
|
self.height = self.markImg.height
|
2024-08-30 23:50:45 +08:00
|
|
|
|
elif width and height:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
self.markImg = Image.new(mode, (width, height), color) # type: ignore
|
2024-08-30 23:50:45 +08:00
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError("长度和宽度不能为空...")
|
2024-02-04 04:18:54 +08:00
|
|
|
|
self.draw = ImageDraw.Draw(self.markImg)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
2024-08-30 23:50:45 +08:00
|
|
|
|
def size(self) -> tuple[int, int]:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
return self.markImg.size
|
|
|
|
|
|
|
2024-08-02 20:46:51 +08:00
|
|
|
|
@classmethod
|
2024-08-24 12:30:49 +08:00
|
|
|
|
def open(cls, path: str | Path | bytes) -> Self:
|
2024-08-02 20:46:51 +08:00
|
|
|
|
"""打开图片
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
path: 图片路径
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
Self: BuildImage
|
|
|
|
|
|
"""
|
|
|
|
|
|
return cls(background=path)
|
|
|
|
|
|
|
2024-02-04 04:18:54 +08:00
|
|
|
|
@classmethod
|
|
|
|
|
|
async def build_text_image(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
text: str,
|
2024-02-25 03:18:34 +08:00
|
|
|
|
font: str | FreeTypeFont | Path = "HYWenHei-85W.ttf",
|
2024-02-04 04:18:54 +08:00
|
|
|
|
size: int = 10,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
font_color: str | tuple[int, int, int] = (0, 0, 0),
|
2024-02-04 04:18:54 +08:00
|
|
|
|
color: ColorAlias = None,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
padding: int | tuple[int, int, int, int] | None = None,
|
2024-02-04 04:18:54 +08:00
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""构建文本图片
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
text: 文本
|
|
|
|
|
|
font: 字体路径
|
|
|
|
|
|
size: 字体大小
|
|
|
|
|
|
font_color: 字体颜色.
|
|
|
|
|
|
color: 背景颜色
|
|
|
|
|
|
padding: 外边距
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
Self: Self
|
|
|
|
|
|
"""
|
2024-08-11 15:57:33 +08:00
|
|
|
|
if not text.strip():
|
|
|
|
|
|
return cls(1, 1)
|
2024-02-25 03:18:34 +08:00
|
|
|
|
_font = None
|
|
|
|
|
|
if isinstance(font, FreeTypeFont):
|
|
|
|
|
|
_font = font
|
2024-08-30 23:50:45 +08:00
|
|
|
|
elif isinstance(font, str | Path):
|
2024-02-25 03:18:34 +08:00
|
|
|
|
_font = cls.load_font(font, size)
|
2024-08-11 15:57:33 +08:00
|
|
|
|
width, height = cls.get_text_size(text, _font)
|
2024-02-25 03:18:34 +08:00
|
|
|
|
if isinstance(padding, int):
|
2024-02-04 04:18:54 +08:00
|
|
|
|
width += padding * 2
|
|
|
|
|
|
height += padding * 2
|
2024-02-25 03:18:34 +08:00
|
|
|
|
elif isinstance(padding, tuple):
|
2024-02-04 04:18:54 +08:00
|
|
|
|
width += padding[1] + padding[3]
|
|
|
|
|
|
height += padding[0] + padding[2]
|
2024-08-11 15:57:33 +08:00
|
|
|
|
markImg = cls(width, height, color, font=_font)
|
2024-02-04 04:18:54 +08:00
|
|
|
|
await markImg.text(
|
|
|
|
|
|
(0, 0), text, fill=font_color, font=_font, center_type="center"
|
|
|
|
|
|
)
|
|
|
|
|
|
return markImg
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def auto_paste(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
img_list: list[Self | tImage],
|
|
|
|
|
|
row: int,
|
|
|
|
|
|
space: int = 10,
|
|
|
|
|
|
padding: int = 50,
|
2024-02-25 03:18:34 +08:00
|
|
|
|
color: ColorAlias = (255, 255, 255),
|
2024-02-04 04:18:54 +08:00
|
|
|
|
background: str | BytesIO | Path | None = None,
|
2024-02-25 03:18:34 +08:00
|
|
|
|
) -> Self:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""自动贴图
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
img_list: 图片列表
|
|
|
|
|
|
row: 一行图片的数量
|
|
|
|
|
|
space: 图片之间的间距.
|
|
|
|
|
|
padding: 外边距.
|
|
|
|
|
|
color: 图片背景颜色.
|
|
|
|
|
|
background: 图片背景图片.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
Self: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not img_list:
|
2024-02-25 03:18:34 +08:00
|
|
|
|
raise ValueError("贴图类别为空...")
|
2024-11-21 15:10:07 +08:00
|
|
|
|
width = max(img.size[0] for img in img_list)
|
|
|
|
|
|
height = max(img.size[1] for img in img_list)
|
2024-02-04 04:18:54 +08:00
|
|
|
|
background_width = width * row + space * (row - 1) + padding * 2
|
2024-02-25 03:18:34 +08:00
|
|
|
|
row_count = math.ceil(len(img_list) / row)
|
|
|
|
|
|
if row_count == 1:
|
|
|
|
|
|
background_width = (
|
2024-08-30 23:50:45 +08:00
|
|
|
|
sum(img.width for img in img_list) + space * (row - 1) + padding * 2
|
2024-02-25 03:18:34 +08:00
|
|
|
|
)
|
|
|
|
|
|
background_height = height * row_count + space * (row_count - 1) + padding * 2
|
2024-02-04 04:18:54 +08:00
|
|
|
|
background_image = cls(
|
|
|
|
|
|
background_width, background_height, color=color, background=background
|
|
|
|
|
|
)
|
|
|
|
|
|
_cur_width, _cur_height = padding, padding
|
2024-11-21 15:10:07 +08:00
|
|
|
|
row_num = 0
|
|
|
|
|
|
for i in range(len(img_list)):
|
|
|
|
|
|
row_num += 1
|
|
|
|
|
|
img: Self | tImage = img_list[i]
|
2024-02-04 04:18:54 +08:00
|
|
|
|
await background_image.paste(img, (_cur_width, _cur_height))
|
|
|
|
|
|
_cur_width += space + img.width
|
2024-11-21 15:10:07 +08:00
|
|
|
|
next_image_width = 0
|
|
|
|
|
|
if i != len(img_list) - 1:
|
|
|
|
|
|
next_image_width = img_list[i + 1].width
|
|
|
|
|
|
if (
|
|
|
|
|
|
row_num == row
|
|
|
|
|
|
or _cur_width + padding + next_image_width >= background_image.width + 1
|
|
|
|
|
|
):
|
2024-02-25 03:18:34 +08:00
|
|
|
|
_cur_height += space + img.height
|
2024-02-04 04:18:54 +08:00
|
|
|
|
_cur_width = padding
|
2024-11-21 15:10:07 +08:00
|
|
|
|
row_num = 0
|
2024-02-04 04:18:54 +08:00
|
|
|
|
return background_image
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2024-02-25 03:18:34 +08:00
|
|
|
|
def load_font(
|
|
|
|
|
|
cls, font: str | Path = "HYWenHei-85W.ttf", font_size: int = 10
|
|
|
|
|
|
) -> FreeTypeFont:
|
|
|
|
|
|
"""加载字体
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
font: 字体名称
|
|
|
|
|
|
font_size: 字体大小
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
FreeTypeFont: 字体
|
|
|
|
|
|
"""
|
2024-08-30 23:50:45 +08:00
|
|
|
|
path = FONT_PATH / font if type(font) is str else font
|
2024-02-04 04:18:54 +08:00
|
|
|
|
return ImageFont.truetype(str(path), font_size)
|
|
|
|
|
|
|
|
|
|
|
|
@overload
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_text_size(
|
|
|
|
|
|
cls, text: str, font: FreeTypeFont | None = None
|
2024-08-30 23:50:45 +08:00
|
|
|
|
) -> tuple[int, int]: ...
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
@overload
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_text_size(
|
|
|
|
|
|
cls, text: str, font: str | None = None, font_size: int = 10
|
2024-08-30 23:50:45 +08:00
|
|
|
|
) -> tuple[int, int]: ...
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def get_text_size(
|
2024-02-25 03:18:34 +08:00
|
|
|
|
cls,
|
|
|
|
|
|
text: str,
|
|
|
|
|
|
font: str | FreeTypeFont | None = "HYWenHei-85W.ttf",
|
|
|
|
|
|
font_size: int = 10,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
) -> tuple[int, int]: # sourcery skip: remove-unnecessary-cast
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""获取该字体下文本需要的长宽
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
text: 文本内容
|
|
|
|
|
|
font: 字体名称或FreeTypeFont
|
|
|
|
|
|
font_size: 字体大小
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
2024-08-30 23:50:45 +08:00
|
|
|
|
tuple[int, int]: 长宽
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
_font = font
|
2024-08-30 23:50:45 +08:00
|
|
|
|
if font and type(font) is str:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
_font = cls.load_font(font, font_size)
|
2024-08-10 17:56:49 +08:00
|
|
|
|
temp_image = Image.new("RGB", (1, 1), (255, 255, 255))
|
|
|
|
|
|
draw = ImageDraw.Draw(temp_image)
|
|
|
|
|
|
text_box = draw.textbbox((0, 0), str(text), font=_font) # type: ignore
|
|
|
|
|
|
text_width = text_box[2] - text_box[0]
|
|
|
|
|
|
text_height = text_box[3] - text_box[1]
|
|
|
|
|
|
return text_width, text_height + 10
|
|
|
|
|
|
# return _font.getsize(str(text)) # type: ignore
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
2024-08-30 23:50:45 +08:00
|
|
|
|
def getsize(self, msg: str) -> tuple[int, int]:
|
|
|
|
|
|
# sourcery skip: remove-unnecessary-cast
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取文字在该图片 font_size 下所需要的空间
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
msg: 文本
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
2024-08-30 23:50:45 +08:00
|
|
|
|
tuple[int, int]: 长宽
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
2024-08-10 17:56:49 +08:00
|
|
|
|
temp_image = Image.new("RGB", (1, 1), (255, 255, 255))
|
|
|
|
|
|
draw = ImageDraw.Draw(temp_image)
|
|
|
|
|
|
text_box = draw.textbbox((0, 0), str(msg), font=self.font)
|
|
|
|
|
|
text_width = text_box[2] - text_box[0]
|
|
|
|
|
|
text_height = text_box[3] - text_box[1]
|
|
|
|
|
|
return text_width, text_height + 10
|
|
|
|
|
|
# return self.font.getsize(msg) # type: ignore
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
def __center_xy(
|
|
|
|
|
|
self,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
pos: tuple[int, int],
|
2024-02-04 04:18:54 +08:00
|
|
|
|
width: int,
|
|
|
|
|
|
height: int,
|
|
|
|
|
|
center_type: CenterType | None,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
) -> tuple[int, int]:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
根据居中类型定位xy
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
pos: 定位
|
|
|
|
|
|
image: image
|
|
|
|
|
|
center_type: 居中类型
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
2024-08-30 23:50:45 +08:00
|
|
|
|
tuple[int, int]: 定位
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
# _width, _height = pos
|
|
|
|
|
|
if self.width and self.height:
|
|
|
|
|
|
if center_type == "center":
|
|
|
|
|
|
width = int((self.width - width) / 2)
|
|
|
|
|
|
height = int((self.height - height) / 2)
|
|
|
|
|
|
elif center_type == "width":
|
|
|
|
|
|
width = int((self.width - width) / 2)
|
|
|
|
|
|
height = pos[1]
|
|
|
|
|
|
elif center_type == "height":
|
|
|
|
|
|
width = pos[0]
|
|
|
|
|
|
height = int((self.height - height) / 2)
|
|
|
|
|
|
return width, height
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def paste(
|
|
|
|
|
|
self,
|
|
|
|
|
|
image: Self | tImage,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
pos: tuple[int, int] = (0, 0),
|
2024-02-04 04:18:54 +08:00
|
|
|
|
center_type: CenterType | None = None,
|
|
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""贴图
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
image: BuildImage 或 Image
|
|
|
|
|
|
pos: 定位.
|
|
|
|
|
|
center_type: 居中.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
ValueError: 居中类型错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
if center_type and center_type not in ["center", "height", "width"]:
|
|
|
|
|
|
raise ValueError("center_type must be 'center', 'width' or 'height'")
|
|
|
|
|
|
_image = image
|
|
|
|
|
|
if isinstance(image, BuildImage):
|
|
|
|
|
|
_image = image.markImg
|
|
|
|
|
|
if _image.width and _image.height and center_type:
|
|
|
|
|
|
pos = self.__center_xy(pos, _image.width, _image.height, center_type)
|
2024-02-25 03:18:34 +08:00
|
|
|
|
try:
|
|
|
|
|
|
self.markImg.paste(_image, pos, _image) # type: ignore
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
self.markImg.paste(_image, pos) # type: ignore
|
2024-02-04 04:18:54 +08:00
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def point(
|
2024-08-30 23:50:45 +08:00
|
|
|
|
self, pos: tuple[int, int], fill: tuple[int, int, int] | None = None
|
2024-02-04 04:18:54 +08:00
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
绘制多个或单独的像素
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
pos: 坐标
|
|
|
|
|
|
fill: 填充颜色.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.draw.point(pos, fill=fill)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def ellipse(
|
|
|
|
|
|
self,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
pos: tuple[int, int, int, int],
|
|
|
|
|
|
fill: tuple[int, int, int] | None = None,
|
|
|
|
|
|
outline: tuple[int, int, int] | None = None,
|
2024-02-04 04:18:54 +08:00
|
|
|
|
width: int = 1,
|
|
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
绘制圆
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
pos: 坐标范围
|
|
|
|
|
|
fill: 填充颜色.
|
|
|
|
|
|
outline: 描线颜色.
|
|
|
|
|
|
width: 描线宽度.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.draw.ellipse(pos, fill, outline, width)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def text(
|
|
|
|
|
|
self,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
pos: tuple[int, int],
|
2024-02-04 04:18:54 +08:00
|
|
|
|
text: str,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
fill: str | tuple[int, int, int] = (0, 0, 0),
|
2024-02-04 04:18:54 +08:00
|
|
|
|
center_type: CenterType | None = None,
|
|
|
|
|
|
font: FreeTypeFont | str | Path | None = None,
|
|
|
|
|
|
font_size: int = 10,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
) -> Self: # sourcery skip: remove-unnecessary-cast
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
在图片上添加文字
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
pos: 文字位置
|
|
|
|
|
|
text: 文字内容
|
|
|
|
|
|
fill: 文字颜色.
|
|
|
|
|
|
center_type: 居中类型.
|
|
|
|
|
|
font: 字体.
|
|
|
|
|
|
font_size: 字体大小.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
ValueError: 居中类型错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
if center_type and center_type not in ["center", "height", "width"]:
|
|
|
|
|
|
raise ValueError("center_type must be 'center', 'width' or 'height'")
|
|
|
|
|
|
max_length_text = ""
|
2024-08-30 23:50:45 +08:00
|
|
|
|
sentence = str(text).split("\n")
|
2024-02-04 04:18:54 +08:00
|
|
|
|
for x in sentence:
|
|
|
|
|
|
max_length_text = x if len(x) > len(max_length_text) else max_length_text
|
|
|
|
|
|
if font:
|
|
|
|
|
|
if not isinstance(font, FreeTypeFont):
|
|
|
|
|
|
font = self.load_font(font, font_size)
|
|
|
|
|
|
else:
|
|
|
|
|
|
font = self.font
|
|
|
|
|
|
if center_type:
|
2024-08-10 17:56:49 +08:00
|
|
|
|
ttf_w, ttf_h = self.getsize(max_length_text) # type: ignore
|
2024-08-11 15:57:33 +08:00
|
|
|
|
# ttf_h = ttf_h * len(sentence)
|
2024-02-04 04:18:54 +08:00
|
|
|
|
pos = self.__center_xy(pos, ttf_w, ttf_h, center_type)
|
2024-08-30 23:50:45 +08:00
|
|
|
|
self.draw.text(pos, str(text), fill=fill, font=font)
|
2024-02-04 04:18:54 +08:00
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def save(self, path: str | Path):
|
|
|
|
|
|
"""
|
|
|
|
|
|
保存图片
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
path: 图片路径
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg.save(path) # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
def show(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
说明:
|
|
|
|
|
|
显示图片
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg.show()
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def resize(self, ratio: float = 0, width: int = 0, height: int = 0) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
压缩图片
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
ratio: 压缩倍率.
|
|
|
|
|
|
width: 压缩图片宽度至 width.
|
|
|
|
|
|
height: 压缩图片高度至 height.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
|
|
|
|
|
|
异常:
|
|
|
|
|
|
ValueError: 缺少参数
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not width and not height and not ratio:
|
|
|
|
|
|
raise ValueError("缺少参数...")
|
|
|
|
|
|
if self.width and self.height:
|
2024-08-30 23:50:45 +08:00
|
|
|
|
if not width and not height:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
width = int(self.width * ratio)
|
|
|
|
|
|
height = int(self.height * ratio)
|
|
|
|
|
|
self.markImg = self.markImg.resize((width, height), Image.LANCZOS) # type: ignore
|
|
|
|
|
|
self.width, self.height = self.markImg.size
|
|
|
|
|
|
self.draw = ImageDraw.Draw(self.markImg)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
2024-08-30 23:50:45 +08:00
|
|
|
|
def crop(self, box: tuple[int, int, int, int]) -> Self:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
裁剪图片
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
box: 左上角坐标,右下角坐标 (left, upper, right, lower)
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg = self.markImg.crop(box)
|
|
|
|
|
|
self.width, self.height = self.markImg.size
|
|
|
|
|
|
self.draw = ImageDraw.Draw(self.markImg)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def transparent(self, alpha_ratio: float = 1, n: int = 0) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
图片透明化
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
alpha_ratio: 透明化程度.
|
|
|
|
|
|
n: 透明化大小内边距.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg = self.markImg.convert("RGBA")
|
|
|
|
|
|
x, y = self.markImg.size
|
2024-08-30 23:50:45 +08:00
|
|
|
|
for i, k in itertools.product(range(n, x - n), range(n, y - n)):
|
|
|
|
|
|
color = self.markImg.getpixel((i, k))
|
2025-01-07 14:36:22 +08:00
|
|
|
|
color = color[:-1] + (int(100 * alpha_ratio),) # type: ignore
|
2024-08-30 23:50:45 +08:00
|
|
|
|
self.markImg.putpixel((i, k), color)
|
2024-02-04 04:18:54 +08:00
|
|
|
|
self.draw = ImageDraw.Draw(self.markImg)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def pic2bs4(self) -> str:
|
2024-02-26 03:04:32 +08:00
|
|
|
|
"""BuildImage 转 base64
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
str: base64
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
buf = BytesIO()
|
|
|
|
|
|
self.markImg.save(buf, format="PNG")
|
|
|
|
|
|
base64_str = base64.b64encode(buf.getvalue()).decode()
|
2024-08-30 23:50:45 +08:00
|
|
|
|
return f"base64://{base64_str}"
|
2024-02-04 04:18:54 +08:00
|
|
|
|
|
2024-02-28 00:38:54 +08:00
|
|
|
|
def pic2bytes(self) -> bytes:
|
|
|
|
|
|
"""获取bytes
|
2024-02-26 03:04:32 +08:00
|
|
|
|
|
|
|
|
|
|
返回:
|
2024-02-28 00:38:54 +08:00
|
|
|
|
bytes: bytes
|
2024-02-26 03:04:32 +08:00
|
|
|
|
"""
|
2024-02-28 00:38:54 +08:00
|
|
|
|
buf = BytesIO()
|
2024-12-11 10:40:05 +08:00
|
|
|
|
img_format = self.markImg.format.upper() if self.markImg.format else "PNG"
|
|
|
|
|
|
|
|
|
|
|
|
if img_format == "GIF":
|
|
|
|
|
|
self.markImg.save(buf, format="GIF", save_all=True, loop=0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.markImg.save(buf, format="PNG")
|
|
|
|
|
|
|
2024-02-28 00:38:54 +08:00
|
|
|
|
return buf.getvalue()
|
2024-02-26 03:04:32 +08:00
|
|
|
|
|
2024-02-04 04:18:54 +08:00
|
|
|
|
def convert(self, type_: ModeType) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
修改图片类型
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
type_: ModeType
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg = self.markImg.convert(type_)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def rectangle(
|
|
|
|
|
|
self,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
xy: tuple[int, int, int, int],
|
|
|
|
|
|
fill: tuple[int, int, int] | None = None,
|
2024-02-04 04:18:54 +08:00
|
|
|
|
outline: str | None = None,
|
|
|
|
|
|
width: int = 1,
|
|
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
画框
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
xy: 坐标
|
|
|
|
|
|
fill: 填充颜色.
|
|
|
|
|
|
outline: 轮廓颜色.
|
|
|
|
|
|
width: 线宽.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.draw.rectangle(xy, fill, outline, width)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def polygon(
|
|
|
|
|
|
self,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
xy: list[tuple[int, int]],
|
|
|
|
|
|
fill: tuple[int, int, int] = (0, 0, 0),
|
2024-02-04 04:18:54 +08:00
|
|
|
|
outline: int = 1,
|
|
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
画多边形
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
xy: 坐标
|
|
|
|
|
|
fill: 颜色.
|
|
|
|
|
|
outline: 线宽.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.draw.polygon(xy, fill, outline)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def line(
|
|
|
|
|
|
self,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
xy: tuple[int, int, int, int],
|
|
|
|
|
|
fill: tuple[int, int, int] | str = "#D8DEE4",
|
2024-02-04 04:18:54 +08:00
|
|
|
|
width: int = 1,
|
|
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
画线
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
xy: 坐标
|
|
|
|
|
|
fill: 填充.
|
|
|
|
|
|
width: 线宽.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.draw.line(xy, fill, width)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def circle(self) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
图像变圆
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg.convert("RGBA")
|
|
|
|
|
|
size = self.markImg.size
|
|
|
|
|
|
r2 = min(size[0], size[1])
|
|
|
|
|
|
if size[0] != size[1]:
|
|
|
|
|
|
self.markImg = self.markImg.resize((r2, r2), Image.LANCZOS) # type: ignore
|
|
|
|
|
|
width = 1
|
|
|
|
|
|
antialias = 4
|
|
|
|
|
|
ellipse_box = [0, 0, r2 - 2, r2 - 2]
|
|
|
|
|
|
mask = Image.new(
|
|
|
|
|
|
size=[int(dim * antialias) for dim in self.markImg.size], # type: ignore
|
|
|
|
|
|
mode="L",
|
|
|
|
|
|
color="black",
|
|
|
|
|
|
)
|
|
|
|
|
|
draw = ImageDraw.Draw(mask)
|
|
|
|
|
|
for offset, fill in (width / -2.0, "black"), (width / 2.0, "white"):
|
2024-08-30 23:50:45 +08:00
|
|
|
|
left, top = ((value + offset) * antialias for value in ellipse_box[:2])
|
|
|
|
|
|
right, bottom = ((value - offset) * antialias for value in ellipse_box[2:])
|
2024-02-04 04:18:54 +08:00
|
|
|
|
draw.ellipse([left, top, right, bottom], fill=fill)
|
2025-01-07 14:36:22 +08:00
|
|
|
|
mask = mask.resize(self.markImg.size, Resampling.LANCZOS)
|
2024-08-30 23:50:45 +08:00
|
|
|
|
with contextlib.suppress(ValueError):
|
2024-02-04 04:18:54 +08:00
|
|
|
|
self.markImg.putalpha(mask)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def circle_corner(
|
|
|
|
|
|
self,
|
|
|
|
|
|
radii: int = 30,
|
2024-08-30 23:50:45 +08:00
|
|
|
|
point_list: list[Literal["lt", "rt", "lb", "rb"]] | None = None,
|
2024-02-04 04:18:54 +08:00
|
|
|
|
) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
矩形四角变圆
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
radii: 半径.
|
|
|
|
|
|
point_list: 需要变化的角.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
2024-08-30 23:50:45 +08:00
|
|
|
|
if point_list is None:
|
|
|
|
|
|
point_list = ["lt", "rt", "lb", "rb"]
|
2024-02-04 04:18:54 +08:00
|
|
|
|
# 画圆(用于分离4个角)
|
|
|
|
|
|
img = self.markImg.convert("RGBA")
|
|
|
|
|
|
alpha = img.split()[-1]
|
|
|
|
|
|
circle = Image.new("L", (radii * 2, radii * 2), 0)
|
|
|
|
|
|
draw = ImageDraw.Draw(circle)
|
|
|
|
|
|
draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形
|
|
|
|
|
|
w, h = img.size
|
|
|
|
|
|
if "lt" in point_list:
|
|
|
|
|
|
alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0))
|
|
|
|
|
|
if "rt" in point_list:
|
|
|
|
|
|
alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0))
|
|
|
|
|
|
if "lb" in point_list:
|
|
|
|
|
|
alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii))
|
|
|
|
|
|
if "rb" in point_list:
|
|
|
|
|
|
alpha.paste(
|
|
|
|
|
|
circle.crop((radii, radii, radii * 2, radii * 2)),
|
|
|
|
|
|
(w - radii, h - radii),
|
|
|
|
|
|
)
|
|
|
|
|
|
img.putalpha(alpha)
|
|
|
|
|
|
self.markImg = img
|
|
|
|
|
|
self.draw = ImageDraw.Draw(self.markImg)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def rotate(self, angle: int, expand: bool = False) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
旋转图片
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
angle: 角度
|
|
|
|
|
|
expand: 放大图片适应角度.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg = self.markImg.rotate(angle, expand=expand)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
2025-01-07 14:36:22 +08:00
|
|
|
|
def transpose(self, angle: Transpose) -> Self:
|
2024-02-04 04:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
旋转图片(包括边框)
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
angle: 角度
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.markImg.transpose(angle)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
@run_sync
|
|
|
|
|
|
def filter(self, filter_: str, aud: int | None = None) -> Self:
|
|
|
|
|
|
"""
|
|
|
|
|
|
图片变化
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
filter_: 变化效果
|
|
|
|
|
|
aud: 利率.
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
_type = None
|
|
|
|
|
|
if filter_ == "GaussianBlur": # 高斯模糊
|
|
|
|
|
|
_type = ImageFilter.GaussianBlur
|
|
|
|
|
|
elif filter_ == "EDGE_ENHANCE": # 锐化效果
|
|
|
|
|
|
_type = ImageFilter.EDGE_ENHANCE
|
|
|
|
|
|
elif filter_ == "BLUR": # 模糊效果
|
|
|
|
|
|
_type = ImageFilter.BLUR
|
|
|
|
|
|
elif filter_ == "CONTOUR": # 铅笔滤镜
|
|
|
|
|
|
_type = ImageFilter.CONTOUR
|
|
|
|
|
|
elif filter_ == "FIND_EDGES": # 边缘检测
|
|
|
|
|
|
_type = ImageFilter.FIND_EDGES
|
|
|
|
|
|
if _type:
|
|
|
|
|
|
if aud:
|
|
|
|
|
|
self.markImg = self.markImg.filter(_type(aud)) # type: ignore
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.markImg = self.markImg.filter(_type)
|
|
|
|
|
|
self.draw = ImageDraw.Draw(self.markImg)
|
|
|
|
|
|
return self
|
2024-02-25 03:18:34 +08:00
|
|
|
|
|
|
|
|
|
|
def tobytes(self) -> bytes:
|
|
|
|
|
|
"""转换为bytes
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
bytes: bytes
|
|
|
|
|
|
"""
|
|
|
|
|
|
return self.markImg.tobytes()
|
2024-11-21 15:10:07 +08:00
|
|
|
|
|
|
|
|
|
|
def copy(self) -> "BuildImage":
|
|
|
|
|
|
"""复制
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
BuildImage: Self
|
|
|
|
|
|
"""
|
|
|
|
|
|
return BuildImage.open(self.pic2bytes())
|