nonebot-plugin-kawaii-logos 0.0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ # coding=utf-8
2
+ import html
3
+ from PIL import Image
4
+ from nonebot import logger, require, on_command
5
+ from nonebot.plugin import PluginMetadata, inherit_supported_adapters
6
+ from nonebot.adapters import Event
7
+
8
+ from .tools import save_image
9
+ from .config import Config, menu_data
10
+ from .command import command_kawaii_logos
11
+
12
+ require("nonebot_plugin_saa")
13
+ from nonebot_plugin_saa import Image as saaImage, MessageFactory
14
+ from nonebot_plugin_saa import Text as saaText
15
+
16
+ __plugin_meta__ = PluginMetadata(
17
+ name="kawaii_logos",
18
+ description="logo生成器,可以生成一些logo",
19
+ usage="/kwlogo",
20
+ type="application",
21
+ # 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。
22
+ homepage="https://github.com/SuperGuGuGu/nonebot_plugin_kawaii_logos",
23
+ # 发布必填。
24
+ config=Config,
25
+ # 插件配置项类,如无需配置可不填写。
26
+ supported_adapters=inherit_supported_adapters(
27
+ "nonebot_plugin_saa",
28
+ ),
29
+ # 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。
30
+ # 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。
31
+ extra={'menu_data': menu_data},
32
+ )
33
+
34
+
35
+ kawaii_logos_cmd = on_command("kwlogo", priority=10, block=False)
36
+
37
+
38
+ @kawaii_logos_cmd.handle()
39
+ async def download_msg(event: Event):
40
+ if not event.get_type().startswith("message"):
41
+ await kawaii_logos_cmd.finish()
42
+ msg: str = str(event.get_message().copy())
43
+ if msg == "":
44
+ await kawaii_logos_cmd.finish()
45
+
46
+ command_prefix = f"{msg.split('kwlogo')[0]}kwlogo"
47
+ args = msg.removeprefix(command_prefix).removeprefix(" ")
48
+ args = html.unescape(args) # 反转义文字
49
+
50
+ msg = await command_kawaii_logos(args=args)
51
+
52
+ await send(msg)
53
+ await kawaii_logos_cmd.finish()
54
+
55
+
56
+ async def send(msg):
57
+ if msg is None:
58
+ return
59
+
60
+ if type(msg) is not list:
61
+ msg = [msg]
62
+
63
+ saa_msg = []
64
+ for m in msg:
65
+ if type(m) is Image.Image:
66
+ saa_msg.append(saaImage(save_image(m, to_bytes=True)))
67
+ elif type(m) is bytes:
68
+ saa_msg.append(saaImage(m))
69
+ else:
70
+ saa_msg.append(saaText(m))
71
+
72
+ if not saa_msg:
73
+ return
74
+
75
+ msg_builder = MessageFactory(saa_msg)
76
+ await msg_builder.send()
@@ -0,0 +1,43 @@
1
+ # coding=utf-8
2
+ from nonebot import logger
3
+ from .draw import template_1
4
+
5
+
6
+ async def command_kawaii_logos(args: str):
7
+ # 解析
8
+ args = args.split(" ")
9
+ if len(args) == 0:
10
+ return "请添加要生成logo的文字"
11
+ elif len(args) == 1:
12
+ logo_data: dict = {"标题": args[0], "副标题": None, "下": None,"上": None}
13
+ elif len(args) == 2:
14
+ logo_data: dict = {"标题": args[0], "副标题": args[1], "下": None,"上": None}
15
+ elif len(args) == 3:
16
+ logo_data: dict = {"标题": args[0], "副标题": args[1], "下": args[2],"上": None}
17
+ elif len(args) == 4:
18
+ logo_data: dict = {"标题": args[0], "副标题": args[1], "下": args[2],"上": args[3]}
19
+ else:
20
+ return "文字参数过多,最多包含4段文字"
21
+
22
+ # 检查内容长度
23
+ if not 3 <= len(logo_data["标题"]):
24
+ return "第1段文字过短"
25
+ if not len(logo_data["标题"]) <= 10:
26
+ return "第1段文字过长"
27
+ if not 3 <= len(logo_data["副标题"]):
28
+ return "第2段文字过短"
29
+ if not len(logo_data["副标题"]) <= 10:
30
+ return "第2段文字过长"
31
+ if not 3 <= len(logo_data["下"]):
32
+ return "第3段文字过短"
33
+ if not len(logo_data["下"]) <= 15:
34
+ return "第3段文字过长"
35
+ if not 3 <= len(logo_data["上"]):
36
+ return "第4段文字过短"
37
+ if not len(logo_data["上"]) <= 15:
38
+ return "第4段文字过长"
39
+
40
+ image = await template_1(logo_data=logo_data)
41
+
42
+ return image
43
+
@@ -0,0 +1,37 @@
1
+ # coding=utf-8
2
+ from nonebot import get_plugin_config, logger
3
+ from pydantic import BaseModel
4
+ from pathlib import Path
5
+ from nonebot import require
6
+
7
+ try:
8
+ require("nonebot_plugin_localstore")
9
+ import nonebot_plugin_localstore as store
10
+
11
+ plugin_cache_dir: Path = store.get_plugin_cache_dir()
12
+ # plugin_config_dir: Path = store.get_plugin_config_dir()
13
+ plugin_data_dir: Path = store.get_plugin_data_dir()
14
+ except Exception as e:
15
+ plugin_cache_dir: Path = Path("./nonebot_plugin_kawaii_logos/cache")
16
+ plugin_data_dir: Path = Path("./nonebot_plugin_kawaii_logos/data")
17
+
18
+
19
+ class Config(BaseModel):
20
+ ...
21
+
22
+
23
+ try:
24
+ plugin_config = get_plugin_config(Config)
25
+ # qb_url = plugin_config.qbm_url
26
+ except Exception as e:
27
+ pass
28
+
29
+
30
+ menu_data = [
31
+ {
32
+ "trigger_method": "qb帮助",
33
+ "func": "列出命令列表",
34
+ "trigger_condition": " ",
35
+ "brief_des": "qb帮助",
36
+ }
37
+ ]
@@ -0,0 +1,155 @@
1
+ # coding=utf-8
2
+ import os
3
+ import random
4
+ import cv2
5
+ import numpy as np
6
+ from PIL import Image
7
+ from nonebot import logger
8
+
9
+ from .config import plugin_cache_dir
10
+ from .tools import circle_corner, text_to_b64, load_image, draw_text, save_image
11
+
12
+
13
+ text_full = (
14
+ "あアいイうウえエおオかカきキくクけケこコさサしシすスせセそソたタちチつツてテとトなナにニぬヌねネのノはハひヒふフへヘほホまマみミむムめメもモや"
15
+ "ヤゆユよヨらラりリるルれレろロわワをヲんンがガぎギぐグげゲごゴざザじジずズぜゼぞゾだダぢヂづヅでデどドばバびビぶブべベぼボぱパぴピぷプぺペぽポ"
16
+ )
17
+ text_half = "ゃャゅュょョ"
18
+
19
+
20
+ async def template_1(logo_data: dict[str, str]) -> Image.Image:
21
+ """
22
+ 模板1
23
+ :param logo_data:
24
+ :return:
25
+ """
26
+ image_size = (1000, 500)
27
+ image = Image.new("RGBA", image_size, "#FFFFFF00")
28
+
29
+ size_list = [random.randint(110, 190) for _ in range(len(logo_data["标题"]))]
30
+ rotate_list = [random.randint(-20, 20) for _ in range(len(logo_data["标题"]))]
31
+ y_list = [random.randint(-20, 20) for _ in range(len(logo_data["标题"]))]
32
+
33
+ image_size_list = [2, 1, 0, -1]
34
+ image_color_list = ["#7c71ec", "#fff", "#7c71ec", "#fff"]
35
+ for i, image_size in enumerate(image_size_list):
36
+ color = image_color_list[i]
37
+ x, y = 0, 100
38
+ for i_t, text in enumerate(logo_data["标题"]):
39
+ rotate = rotate_list[i_t]
40
+ size = size_list[i_t]
41
+
42
+ paste_alpha = await kawaii_text_to_image(text, image_size)
43
+ paste_alpha = paste_alpha.resize((size, size))
44
+ paste_alpha = paste_alpha.rotate(rotate)
45
+ image_color = Image.new("RGBA", paste_alpha.size, color)
46
+ image.paste(image_color, (x, y + y_list[i_t]), mask=paste_alpha)
47
+
48
+ x += int(size * 0.7)
49
+
50
+ return image
51
+
52
+
53
+ async def kawaii_text_to_image(text: str, size=0) -> Image.Image:
54
+ """
55
+ 获取字符图片
56
+ 高光仅适用于日文
57
+ :param text:要获取的文字
58
+ :param size: -1:高光, 0:本体, 1:描边一, 2:描边二
59
+ :return:PLI.Image.Image
60
+ """
61
+ text_image_path = plugin_cache_dir / "text"
62
+ text_image_path.mkdir(exist_ok=True)
63
+ text_image_path = text_image_path / f"{text_to_b64(text, replace=True)}_{size}.png"
64
+ if os.path.exists(text_image_path):
65
+ image = await load_image(text_image_path)
66
+ return image
67
+
68
+ logger.debug(f"未绘制‘{text}’,进行绘制")
69
+ if size == -1:
70
+ if text not in text_full + text_half:
71
+ return Image.new("RGBA", (200, 200), (0, 0, 0, 0))
72
+ for i, t in enumerate(text_full + text_half):
73
+ if t == text:
74
+ text = i + 1
75
+ url = f"https://cdn.kanon.ink/api/image?imageid=knapi-kawaii_logos-{text}_{size}.png"
76
+ try:
77
+ image = await load_image(url, cache_image=False)
78
+ save_image(image, text_image_path.parent, text_image_path.name, mode="png")
79
+ except Exception as e:
80
+ logger.error("请求高光图片失败")
81
+ logger.error(e)
82
+ image = Image.new("RGBA", (200, 200), (0, 0, 0, 0))
83
+ return image
84
+
85
+ image_0_path = text_image_path.parent / f"{text_to_b64(text, replace=True)}_0.png"
86
+ if os.path.exists(image_0_path):
87
+ image_0 = await load_image(image_0_path)
88
+ else:
89
+ paste_image = await draw_text(
90
+ text,
91
+ size=170,
92
+ textlen=50,
93
+ fontfile="胖胖猪肉体_猫啃网.otf",
94
+ text_color="#fff",
95
+ calculate=False
96
+ )
97
+ image_0 = Image.new("RGBA", (200, 200), (0, 0, 0, 0))
98
+ image_0.alpha_composite(paste_image, (
99
+ int((image_0.size[0] - paste_image.size[0]) / 2),
100
+ int((image_0.size[1] - paste_image.size[1]) / 2)
101
+ ))
102
+ save_image(image_0, image_0_path.parent, f"{text_to_b64(text, replace=True)}_0.png", mode="png")
103
+ if size == 0:
104
+ return image_0
105
+
106
+ if size == 1:
107
+ image_1 = draw_out_line(image_0, 7, (255, 255, 255, 255))
108
+ image_1 = image_1.convert("RGBA")
109
+ save_image(image_1, text_image_path.parent, text_image_path.name, mode="png")
110
+ return image_1
111
+
112
+ if size == 2:
113
+ image_2 = draw_out_line(image_0, 11, (255, 255, 255, 255))
114
+ image_2 = image_2.convert("RGBA")
115
+ save_image(image_2, text_image_path.parent, text_image_path.name, mode="png")
116
+ return image_2
117
+
118
+
119
+ def draw_out_line(image, size=5, color=None):
120
+ if color is None:
121
+ logger.warning("color is None")
122
+ color = [100, 100, 100, 255]
123
+ image_np = np.array(image)
124
+ image_gray = cv2.cvtColor(image_np, cv2.COLOR_RGB2GRAY)
125
+
126
+ # 使用形态学操作生成描边
127
+ kernel = np.ones((3, 3), np.uint8)
128
+ dilated = cv2.dilate(image_gray, kernel, iterations=size)
129
+
130
+ edges = dilated - image_gray
131
+
132
+ # 将描边区域叠加到原图上
133
+ image_np[edges > 0] = color # 描边颜色
134
+
135
+ # 将结果转换回 PIL 图像
136
+ result_image = Image.fromarray(image_np)
137
+
138
+ image = Image.new("RGBA", result_image.size, (0, 0, 0, 0))
139
+ image.alpha_composite(result_image)
140
+
141
+ pixels = image.load()
142
+ for i in range(image.width):
143
+ for j in range(image.height):
144
+ if pixels[i, j] == (0, 0, 0, 255):
145
+ pass
146
+ # pixels[i, j] = (0, 0, 0, 0)
147
+ elif pixels[i, j] == (0, 0, 0, 0):
148
+ pass
149
+ else:
150
+ pixels[i, j] = (color[0], color[1], color[2], 255)
151
+
152
+ return image
153
+
154
+
155
+
@@ -0,0 +1,535 @@
1
+ # coding=utf-8
2
+ import base64
3
+ import io
4
+ import json
5
+ import re
6
+ import shutil
7
+ import httpx
8
+ from PIL.Image import Image as PIL_Image
9
+ from PIL import Image, ImageDraw, ImageFont
10
+ import random
11
+ import os
12
+ import time
13
+ import matplotlib.font_manager as fm
14
+ from nonebot import logger
15
+ from pathlib import Path
16
+ from .config import plugin_cache_dir
17
+
18
+ kwlogo_cache = {
19
+ "font_path": {}
20
+ }
21
+ system_font_list = fm.findSystemFonts(fontpaths=None, fontext='ttf')
22
+ for font_path in system_font_list:
23
+ font_path = font_path.replace("\\", "/")
24
+ kwlogo_cache["font_path"][font_path.split("/")[-1]] = font_path
25
+
26
+
27
+ def save_image(
28
+ image,
29
+ image_path: str | Path = None,
30
+ image_name: int | str = None,
31
+ to_bytes: bool = False,
32
+ mode: str = "jpg"):
33
+ """
34
+ 保存图片文件到缓存文件夹
35
+ :param image:要保存的图片
36
+ :param image_path: 指定的图片所在文件夹路径,默认为缓存
37
+ :param image_name:图片名称,不填为随机数字
38
+ :param to_bytes: 是否转为bytes
39
+ :param mode: 保存的图片格式
40
+ :return:保存的路径
41
+ """
42
+ if mode == "jpg":
43
+ image = image.convert("RGB")
44
+ if to_bytes is True and type(image) is PIL_Image:
45
+ # 将Pillow图像数据保存到内存中
46
+ image_stream = io.BytesIO()
47
+ image.save(image_stream, format='JPEG')
48
+ image_stream.seek(0)
49
+ return image_stream.read()
50
+
51
+ d_y, d_m, d_d = time.strftime("%Y/%m/%d", time.localtime()).split("/")
52
+ time_now = int(time.time())
53
+
54
+ if image_path is None:
55
+ image_path = plugin_cache_dir / "cache" / d_y / d_m / d_d
56
+ os.makedirs(image_path, exist_ok=True)
57
+
58
+ if image_name is None:
59
+ image_name = f"{time_now}_{random.randint(1000, 9999)}"
60
+ num = 50
61
+ while True and num > 0:
62
+ num -= 1
63
+ random_num = str(random.randint(1000, 9999))
64
+ if os.path.exists(image_path / f"{image_name}_{random_num}.{mode}"):
65
+ continue
66
+ image_name = f"{image_name}_{random_num}.{mode}"
67
+ break
68
+
69
+ logger.debug(f"保存图片文件:{image_path}/{image_name}")
70
+ image.save(image_path / image_name)
71
+
72
+ if to_bytes is True:
73
+ image_file = open(image_path / image_name, "rb")
74
+ image = image_file.read()
75
+ image_file.close()
76
+ return image
77
+ return image_path / image_name
78
+
79
+
80
+ def circle_corner(img, radii: int):
81
+ """
82
+ 圆角处理
83
+ :param img: 源图象。
84
+ :param radii: 半径,如:30。
85
+ :return: 返回一个圆角处理后的图象。
86
+ """
87
+
88
+ # 画圆(用于分离4个角)
89
+ circle = Image.new('L', (radii * 2, radii * 2), 0) # 创建一个黑色背景的画布
90
+ draw = ImageDraw.Draw(circle)
91
+ draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 画白色圆形
92
+
93
+ # 原图
94
+ img = img.convert("RGBA")
95
+ w, h = img.size
96
+
97
+ # 画4个角(将整圆分离为4个部分)
98
+ alpha = Image.new('L', img.size, 255)
99
+ alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角
100
+ alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) # 右上角
101
+ alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), (w - radii, h - radii)) # 右下角
102
+ alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) # 左下角
103
+ # alpha.show()
104
+
105
+ img.putalpha(alpha) # 白色区域透明可见,黑色区域不可见
106
+ return img
107
+
108
+
109
+ def image_resize2(image, size: [int, int], overturn=False):
110
+ """
111
+ 重缩放图像
112
+ :param image: 要缩放的图像
113
+ :param size: 缩放后的大小
114
+ :param overturn: 是否放大到全屏
115
+ :return: 缩放后的图像
116
+ """
117
+ x, y = image.size
118
+ if size[0] is None:
119
+ size = (int(size[1] * x / y), size[1])
120
+ if size[1] is None:
121
+ size = (size[0], int(size[0] * y / x))
122
+
123
+ image_background = Image.new("RGBA", size=size, color=(0, 0, 0, 0))
124
+ image = image.convert("RGBA")
125
+ w, h = image_background.size
126
+ x, y = image.size
127
+ if overturn:
128
+ if w / h >= x / y:
129
+ rex = w
130
+ rey = int(rex * y / x)
131
+ paste_image = image.resize((rex, rey))
132
+ image_background.alpha_composite(paste_image, (0, 0))
133
+ else:
134
+ rey = h
135
+ rex = int(rey * x / y)
136
+ paste_image = image.resize((rex, rey))
137
+ x = int((w - rex) / 2)
138
+ image_background.alpha_composite(paste_image, (x, 0))
139
+ else:
140
+ if w / h >= x / y:
141
+ rey = h
142
+ rex = int(rey * x / y)
143
+ paste_image = image.resize((rex, rey))
144
+ x = int((w - rex) / 2)
145
+ y = 0
146
+ image_background.alpha_composite(paste_image, (x, y))
147
+ else:
148
+ rex = w
149
+ rey = int(rex * y / x)
150
+ paste_image = image.resize((rex, rey))
151
+ x = 0
152
+ y = int((h - rey) / 2)
153
+ image_background.alpha_composite(paste_image, (x, y))
154
+
155
+ return image_background
156
+
157
+
158
+ async def draw_text(
159
+ texts: str,
160
+ size: int,
161
+ textlen: int = 20,
162
+ fontfile: str = "",
163
+ text_color=None,
164
+ calculate=False
165
+ ):
166
+ """
167
+ - 文字转图片
168
+ :param texts: 输入的字符串
169
+ :param size: 文字尺寸
170
+ :param textlen: 一行的文字数量
171
+ :param fontfile: 字体文字
172
+ :param text_color: 字体颜色,例:"#FFFFFF"、(10, 10, 10)
173
+ :param calculate: 计算长度。True时只返回空白图,不用粘贴文字,加快速度。
174
+
175
+ :return: 图片文件(RGBA)
176
+ """
177
+ if texts is None:
178
+ texts = "None"
179
+ if text_color is None:
180
+ text_color = "#000000"
181
+
182
+ def get_font_render_w(text):
183
+ if text == " ":
184
+ return 20
185
+ none = ["\n", ""]
186
+ if text in none:
187
+ return 1
188
+ canvas = Image.new('RGB', (500, 500))
189
+ draw = ImageDraw.Draw(canvas)
190
+ draw.text((0, 0), text, font=font, fill=(255, 255, 255))
191
+ bbox = canvas.getbbox()
192
+ # 宽高
193
+ # size = (bbox[2] - bbox[0], bbox[3] - bbox[1])
194
+ if bbox is None:
195
+ return 0
196
+ return bbox[2]
197
+
198
+ def sequence_generator(sequence):
199
+ for value in sequence:
200
+ yield value
201
+
202
+ default_font = ["msyh.ttc", "DejaVuSans.ttf", "msjh.ttc", "msjhl.ttc", "msjhb.ttc", "YuGothR.ttc"]
203
+ if fontfile is None or fontfile == "":
204
+ fontfile = "msyh.ttc"
205
+ if not fontfile.startswith("/") or ":/" in fontfile:
206
+ # 获取字体绝对路径
207
+
208
+ font_list = [fontfile] + default_font + ["no_font"]
209
+ for font in font_list:
210
+ if font == "no_font":
211
+ logger.warning(f"{fontfile}字体加载失败,将使用默认字体")
212
+ # raise f"字体加载失败,请安装字体"
213
+ fontfile = font_list[0]
214
+ break
215
+
216
+ if font in kwlogo_cache["font_path"].keys():
217
+ fontfile = kwlogo_cache["font_path"][font]
218
+ break
219
+
220
+ if os.path.exists(fontfile):
221
+ break
222
+
223
+ font = ImageFont.truetype(font=fontfile, size=size)
224
+
225
+ # 计算图片尺寸
226
+ print_x = 0
227
+ print_y = 0
228
+ jump_num = 0
229
+ text_num = -1
230
+ max_text_y = 0
231
+ max_text_y_list = []
232
+ texts_len = len(texts)
233
+ for text in texts:
234
+ text_num += 1
235
+ if jump_num > 0:
236
+ jump_num -= 1
237
+ else:
238
+ if (textlen * size) < print_x or text == "\n":
239
+ print_x = 0
240
+ print_y += 1.3 * max_text_y
241
+ max_text_y_list.append(max_text_y)
242
+ max_text_y = 0
243
+ if text == "\n":
244
+ continue
245
+ if text == " ":
246
+ print_x += get_font_render_w(text) + 2
247
+ if size > max_text_y:
248
+ max_text_y = size
249
+ continue
250
+ if text == "<":
251
+ while text_num + jump_num < texts_len and texts[text_num + jump_num] != ">":
252
+ jump_num += 1
253
+ jump_num += 0
254
+
255
+ text = texts[text_num:text_num + jump_num]
256
+ pattern = r'src="([^"]+)"'
257
+ urls = re.findall(pattern, text)
258
+ if urls:
259
+ pattern = r'width="(\d+)"'
260
+ image_size_x = re.findall(pattern, text)
261
+
262
+ paste_image = await load_image(urls[0])
263
+ if image_size_x:
264
+ paste_image = image_resize2(paste_image, (int(image_size_x[0]), None))
265
+ print_x += paste_image.size[0] + 2
266
+ if paste_image.size[1] > max_text_y:
267
+ max_text_y = paste_image.size[1]
268
+ continue
269
+ print_x += get_font_render_w(text) + 2
270
+ if size > max_text_y:
271
+ max_text_y = size
272
+ max_text_y_list.append(max_text_y)
273
+ text_y_list = sequence_generator(max_text_y_list)
274
+
275
+ x = int((textlen + 1.5) * size)
276
+ y = int(print_y + 1.2 * size)
277
+
278
+ image = Image.new("RGBA", size=(x, y), color=(0, 0, 0, 0)) # 生成透明图片
279
+ draw_image = ImageDraw.Draw(image)
280
+
281
+ # 绘制文字
282
+ if calculate is False:
283
+ print_x = 0
284
+ print_y = 0
285
+ jump_num = 0
286
+ text_num = -1
287
+ draw_max_text_y = next(text_y_list)
288
+ for text in texts:
289
+ text_num += 1
290
+ if jump_num > 0:
291
+ jump_num -= 1
292
+ else:
293
+ if (textlen * size) < print_x or text == "\n":
294
+ print_x = 0
295
+ print_y += draw_max_text_y
296
+ draw_max_text_y = next(text_y_list, None)
297
+ if text == "\n":
298
+ continue
299
+ if text in ["\n", " "]:
300
+ if text == " ":
301
+ print_x += get_font_render_w(text) + 2
302
+ continue
303
+ if text == "<":
304
+ while text_num + jump_num < texts_len and texts[text_num + jump_num] != ">":
305
+ jump_num += 1
306
+ jump_num += 0
307
+
308
+ text = texts[text_num:text_num + jump_num]
309
+ pattern = r'src="([^"]+)"'
310
+ urls = re.findall(pattern, text)
311
+ if urls:
312
+ pattern = r'width="(\d+)"'
313
+ image_size_x = re.findall(pattern, text)
314
+
315
+ paste_image = await load_image(urls[0])
316
+ if image_size_x:
317
+ paste_image = image_resize2(paste_image, (int(image_size_x[0]), None))
318
+ image.alpha_composite(paste_image, (int(print_x), int(print_y)))
319
+ print_x += paste_image.size[0] + 2
320
+ continue
321
+
322
+ draw_image.text(xy=(int(print_x), int(print_y)),
323
+ text=text,
324
+ fill=text_color,
325
+ font=font)
326
+ print_x += get_font_render_w(text) + 2
327
+ # 把输出的图片裁剪为只有内容的部分
328
+ bbox = image.getbbox()
329
+ if bbox is None:
330
+ box_image = Image.new("RGBA", (2, size), (0, 0, 0, 0))
331
+ else:
332
+ box_image = Image.new("RGBA", (bbox[2] - bbox[0], bbox[3] - bbox[1]), (0, 0, 0, 0))
333
+ box_image.paste(image, (0 - int(bbox[0]), 0 - int(bbox[1])), mask=image)
334
+ image = box_image
335
+ return image
336
+
337
+
338
+ async def load_image(path: str | Image.Image | Path, size=None, mode=None, cache_image=True):
339
+ """
340
+ 读取图片或请求网络图片
341
+ :param path: 图片路径/图片url
342
+ :param size: 出错时候返回的图片尺寸
343
+ :param mode: 图片读取模式
344
+ :param cache_image: 焕缓存获取的图片
345
+ :return:image
346
+ """
347
+ if type(path) is Image.Image:
348
+ return path
349
+ if mode is None:
350
+ mode = "r"
351
+ try:
352
+ if type(path) is str and path.startswith("http"):
353
+ if cache_image is False:
354
+ image = await connect_api("image", path)
355
+ else:
356
+ cache_path = Path(path.removeprefix("http://").removeprefix("https://").split("?")[0])
357
+
358
+ if os.path.exists(plugin_cache_dir / "web_cache" / cache_path):
359
+ return Image.open(plugin_cache_dir / "web_cache" / cache_path)
360
+ file_name = cache_path.name
361
+ file_path = plugin_cache_dir / "web_cache" / cache_path.parent
362
+ os.makedirs(file_path, exist_ok=True)
363
+ image = await connect_api("image", path)
364
+ image.save(file_path / file_name)
365
+
366
+ return image
367
+ else:
368
+ if type(path) is str and path.startswith("{cache_dir}"):
369
+ image_path = plugin_cache_dir / Path(path.removeprefix("{cache_dir}"))
370
+ if not os.path.exists(image_path):
371
+ raise "图片不存在"
372
+ image = Image.open(image_path, mode)
373
+ if mode == "rb":
374
+ return save_image(image, to_bytes=True)
375
+ return image
376
+ return Image.open(path, mode)
377
+ except Exception as e:
378
+ logger.error(f"读取图片错误:{path}")
379
+ logger.error(e)
380
+ if size is not None:
381
+ return Image.new("RGBA", size, (0, 0, 0, 0))
382
+ raise "图片读取错误"
383
+
384
+
385
+ def draw_gradient_color(
386
+ color_a: tuple | str,
387
+ color_b: tuple | str,
388
+ size: tuple[int, int] | list[int, int],
389
+ ):
390
+ """
391
+ 绘制一张从左到右的渐变
392
+ :param size: 图片的尺寸
393
+ :param color_a: 图片读取模式
394
+ :param color_b: 图片读取模式
395
+ :return:image
396
+ """
397
+
398
+ def covert_color(c: tuple | str) -> tuple:
399
+ """
400
+ 转换str颜色到tuple颜色
401
+ """
402
+ if type(c) is str:
403
+ c = (
404
+ int(c[1:3], 16),
405
+ int(c[3:5], 16),
406
+ int(c[5:7], 16),
407
+ 255 if len(c) == 7 else int(c[7:9], 16)
408
+ )
409
+ return c
410
+
411
+ color_a = covert_color(color_a)
412
+ color_b = covert_color(color_b)
413
+
414
+ image = Image.new("RGBA", (size[0], 1), (0, 0, 0, 0))
415
+ img_array = image.load()
416
+ for i in range(size[0]):
417
+ color = (
418
+ int(color_a + ((color_b[0] - color_a[0]) / size[0] * i)),
419
+ int(color_a + ((color_b[1] - color_a[1]) / size[0] * i)),
420
+ int(color_a + ((color_b[2] - color_a[2]) / size[0] * i)),
421
+ int(color_a + ((color_b[3] - color_a[3]) / size[0] * i)),
422
+ )
423
+ img_array[i, 0] = color
424
+ image = image.resize(size)
425
+ return image
426
+
427
+
428
+ async def connect_api(
429
+ connect_type: str,
430
+ url: str,
431
+ post_json=None,
432
+ file_path: str = None,
433
+ timeout: int = 10
434
+ ):
435
+ """
436
+ 请求网络资源
437
+ :param connect_type: json, image, file
438
+ :param url: url
439
+ :param post_json: 要post的内容
440
+ :param file_path: 文件的保存路径
441
+ :param timeout: 超时时间
442
+ :return:
443
+ """
444
+ logger.debug(f"connect_api请求URL:{url}")
445
+ h = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
446
+ "Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.76"}
447
+ if connect_type == "json":
448
+ if post_json is None:
449
+ async with httpx.AsyncClient() as client:
450
+ data = await client.get(url, headers=h, timeout=timeout)
451
+ return json.loads(data.text)
452
+ else:
453
+ async with httpx.AsyncClient() as client:
454
+ data = await client.post(url, json=post_json, headers=h, timeout=timeout)
455
+ return json.loads(data.text)
456
+ elif connect_type == "image":
457
+ if url is None or url in ["none", "None", "", " "]:
458
+ image = await draw_text("获取图片出错", 50, 10)
459
+ else:
460
+ try:
461
+ async with httpx.AsyncClient() as client:
462
+ data = await client.get(url, timeout=timeout)
463
+ image = Image.open(io.BytesIO(data.content))
464
+ except Exception as e:
465
+ logger.error(e)
466
+ logger.error(url)
467
+ raise "获取图片出错"
468
+ return image
469
+ elif connect_type == "file":
470
+ cache_file_path = file_path + "cache"
471
+ f = open(cache_file_path, "wb")
472
+ try:
473
+ res = httpx.get(url, headers=h, timeout=timeout).content
474
+ f.write(res)
475
+ logger.debug(f"下载完成-{file_path}")
476
+ except Exception as e:
477
+ logger.error(e)
478
+ raise Exception
479
+ finally:
480
+ f.close()
481
+ shutil.copyfile(cache_file_path, file_path)
482
+ os.remove(cache_file_path)
483
+ return
484
+
485
+
486
+ async def mix_image(image_1, image_2, mix_type=1):
487
+ """
488
+ 将两张图合并为1张
489
+ :param image_1: 要合并的图像1
490
+ :param image_2: 要合并的图像2
491
+ :param mix_type: 合成方式。1:竖向
492
+ :return:
493
+ """
494
+ if type(image_1) is str:
495
+ image_1 = await load_image(image_1)
496
+ if type(image_2) is str:
497
+ image_2 = await load_image(image_2)
498
+ if mix_type == 1:
499
+ x1, y1 = image_1.size
500
+ x2, y2 = image_2.size
501
+ if image_1.mode == "RGB":
502
+ image_1 = image_1.convert("RGBA")
503
+ if image_2.mode == "RGB":
504
+ image_2 = image_2.convert("RGBA")
505
+
506
+ if x1 > x2:
507
+ x2_m = x1
508
+ y2_m = int(x2_m / x2 * y2)
509
+ images = Image.new("RGBA", (x2_m, y2_m + y1), (0, 0, 0, 0))
510
+ image_2_m = image_2.resize((x2_m, y2_m))
511
+ images.alpha_composite(image_1, (0, 0))
512
+ images.alpha_composite(image_2_m, (0, y1))
513
+ return images
514
+ else: # x1 < x2
515
+ x1_m = x2
516
+ y1_m = int(x1_m / x1 * y1)
517
+ images = Image.new("RGBA", (x1_m, y1_m + y2), (0, 0, 0, 0))
518
+ image_1_m = image_1.resize((x1_m, y1_m))
519
+ images.alpha_composite(image_1_m, (0, 0))
520
+ images.alpha_composite(image_2, (0, y1_m))
521
+ return images
522
+ raise "未知的合并图像方式"
523
+
524
+
525
+ def text_to_b64(text: str, replace=False) -> str:
526
+ b64 = str(base64.b64encode(text.encode('UTF-8')), encoding='UTF-8')
527
+ if replace is True:
528
+ return b64.replace("+", "-").replace("/", "_")
529
+ return b64
530
+
531
+
532
+ def b64_to_text(b64_text: str, replace=False) -> str:
533
+ if replace is True:
534
+ b64_text = b64_text.replace("-", "+").replace("_", "/")
535
+ return base64.b64decode(b64_text).decode('UTF-8')
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 SuperGuGuGu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.3
2
+ Name: nonebot-plugin-kawaii-logos
3
+ Version: 0.0.0.1
4
+ Summary: logo生成器
5
+ License: MIT
6
+ Keywords: nonebot2,qbittorrent
7
+ Author: SuperGuGuGu
8
+ Author-email: from89917812@163.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: httpx (>=0.23)
17
+ Requires-Dist: matplotlib (>=3.9.0)
18
+ Requires-Dist: nonebot-plugin-localstore (>=0.6.0)
19
+ Requires-Dist: nonebot-plugin-send-anything-anywhere (>=0.7.0)
20
+ Requires-Dist: nonebot2 (>=2.2.0)
21
+ Requires-Dist: opencv-python (>=4.9.0)
22
+ Requires-Dist: pathlib (>=1.0)
23
+ Requires-Dist: pillow (>=11.0.0)
24
+ Project-URL: Repository, https://github.com/SuperGuGuGu/nonebot-plugin-kawaii-logos
25
+ Description-Content-Type: text/markdown
26
+
27
+ <div align="center">
28
+ <a href="https://v2.nonebot.dev/store"><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/nbp_logo.png" width="180" height="180" alt="NoneBotPluginLogo"></a>
29
+ <br>
30
+ <p><img src="https://github.com/A-kirami/nonebot-plugin-template/blob/resources/NoneBotPlugin.svg" width="240" alt="NoneBotPluginText"></p>
31
+ </div>
32
+
33
+ <div align="center">
34
+
35
+ # nonebot-plugin-kawaii-logos
36
+
37
+ _✨ kawaii_logo生成器 ✨_
38
+
39
+
40
+ <a href="./LICENSE">
41
+ <img src="https://img.shields.io/github/license/SuperGuGuGu/nonebot_plugin_kawaii_logos.svg" alt="license">
42
+ </a>
43
+ <a href="https://pypi.python.org/pypi/nonebot-plugin-kawaii-logos">
44
+ <img src="https://img.shields.io/pypi/v/nonebot-plugin-kawaii-logos.svg" alt="pypi">
45
+ </a>
46
+ <img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
47
+
48
+ </div>
49
+
50
+ ## 📖 介绍
51
+
52
+ logo生成器,可以生成一些logo
53
+
54
+ 跨平台
55
+
56
+ ## 💿 安装
57
+
58
+ <details open>
59
+ <summary>使用 nb-cli 安装</summary>
60
+ 在 nonebot2 项目的根目录下打开命令行, 输入以下指令即可安装
61
+
62
+ nb plugin install nonebot-plugin-kawaii-logos
63
+
64
+ </details>
65
+
66
+ <details>
67
+ <summary>使用包管理器安装</summary>
68
+ 在 nonebot2 项目的插件目录下, 打开命令行, 根据你使用的包管理器, 输入相应的安装命令
69
+
70
+ <details>
71
+ <summary>pip</summary>
72
+
73
+ pip install nonebot-plugin-kawaii-logos
74
+
75
+ </details>
76
+ <details>
77
+ <summary>pdm</summary>
78
+
79
+ pdm add nonebot-plugin-kawaii-logos
80
+
81
+ </details>
82
+ <details>
83
+ <summary>poetry</summary>
84
+
85
+ poetry add nonebot-plugin-kawaii-logos
86
+
87
+ </details>
88
+ <details>
89
+ <summary>conda</summary>
90
+
91
+ conda install nonebot-plugin-kawaii-logos
92
+
93
+ </details>
94
+
95
+ 打开 nonebot2 项目根目录下的 `pyproject.toml` 文件, 在 `[tool.nonebot]` 部分追加写入
96
+
97
+ plugins = ["nonebot_plugin_kawaii_logos"]
98
+
99
+ </details>
100
+
101
+ ## ⚙️ 配置
102
+
103
+ 在 nonebot2 项目的`.env`文件中添加下表中的必填配置
104
+
105
+ | 配置项 | 必填 | 默认值 | 说明 | 示例 |
106
+ |:-------------:|:--:|:---:|:--:|:--:|
107
+ | config_config | 否 | 示例 | 示例 | 示例 |
108
+
109
+ 本插件使用了nonebot-plugin-localstore存储文件。
110
+
111
+ 如有需要修改存储位置,请参考 [localstore文档](https://github.com/nonebot/plugin-localstore)
112
+
113
+ ## 🎉 使用
114
+
115
+ ### 指令表
116
+
117
+ - ✅: 支持
118
+ - 🚧: 部分支持或正在完善
119
+ - 🗓️️: 计划中
120
+ - ✖️: 不支持/无计划
121
+
122
+ | 指令 | 说明 | 需要at | 功能实现 | 图形界面 |
123
+ |:------:|:--:|:----:|:----:|:----:|
124
+ | kwlogo | 示例 | 否 | 🗓️ | 🗓️ |
125
+
126
+ ### 效果图
127
+
128
+ [假装有图片.jpg]
129
+
130
+ ## ⭐
131
+
132
+ <p><img src="https://api.star-history.com/svg?repos=SuperGuGuGu/nonebot_plugin_kawaii_logos&type=Date" width="480" alt="NoneBotPluginText"></p>
133
+
134
+
@@ -0,0 +1,9 @@
1
+ nonebot_plugin_kawaii_logos/__init__.py,sha256=ko_KKoxZ_OGRokYPYJU0_-lbpytXg7AHmmXCqSXFpCY,2428
2
+ nonebot_plugin_kawaii_logos/command.py,sha256=Xs0wrj6THHtIgY2qdKYi1gaYyfBsRYZkRg9U4dESWXs,1503
3
+ nonebot_plugin_kawaii_logos/config.py,sha256=eJEWSIKW6CXYCPrg2hkow-SaxExbA2qfE8niSanWWtw,912
4
+ nonebot_plugin_kawaii_logos/draw.py,sha256=Sst9kU300GMDwCx_cj70da5j8qzIzeyEnZbH5KXdfZo,5558
5
+ nonebot_plugin_kawaii_logos/tools.py,sha256=NkXf8xJbh0nkk5dHl2GkvXRHDl2M0t_Zg2hcziL_OOY,18379
6
+ nonebot_plugin_kawaii_logos-0.0.0.1.dist-info/LICENSE,sha256=4QwosCUBWhxPzvBqBKkegYlm-APuqbMaNBTOAYjrt7E,1068
7
+ nonebot_plugin_kawaii_logos-0.0.0.1.dist-info/METADATA,sha256=HpmgfLYyfBPALwy_kvMVxVtAtnX6zbPjtgX44vW8tow,3679
8
+ nonebot_plugin_kawaii_logos-0.0.0.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
9
+ nonebot_plugin_kawaii_logos-0.0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.0.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any