nonebot-plugin-shitbot 0.1.0__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.
Files changed (46) hide show
  1. nonebot_plugin_shitbot/__init__.py +21 -0
  2. nonebot_plugin_shitbot/aux.py +71 -0
  3. nonebot_plugin_shitbot/command.py +154 -0
  4. nonebot_plugin_shitbot/commands/__init__.py +27 -0
  5. nonebot_plugin_shitbot/commands/advrandpic_cmd.py +129 -0
  6. nonebot_plugin_shitbot/commands/autoreply_cmd.py +71 -0
  7. nonebot_plugin_shitbot/commands/autoreply_main_cmd.py +67 -0
  8. nonebot_plugin_shitbot/commands/convert_cmd.py +371 -0
  9. nonebot_plugin_shitbot/commands/help_cmd.py +92 -0
  10. nonebot_plugin_shitbot/commands/md2pic_cmd.py +195 -0
  11. nonebot_plugin_shitbot/commands/perm_cmd.py +392 -0
  12. nonebot_plugin_shitbot/commands/pixiv_cmd.py +144 -0
  13. nonebot_plugin_shitbot/commands/randpic_cmd.py +75 -0
  14. nonebot_plugin_shitbot/commands/session_cmd.py +79 -0
  15. nonebot_plugin_shitbot/commands/shitpost_cmd.py +189 -0
  16. nonebot_plugin_shitbot/config.py +186 -0
  17. nonebot_plugin_shitbot/config.yaml +23 -0
  18. nonebot_plugin_shitbot/css/github-markdown-dark-dimmed.css +1220 -0
  19. nonebot_plugin_shitbot/default_config.yaml +23 -0
  20. nonebot_plugin_shitbot/docs/help/advrandpic.md +61 -0
  21. nonebot_plugin_shitbot/docs/help/convert.md +37 -0
  22. nonebot_plugin_shitbot/docs/help/help.md +17 -0
  23. nonebot_plugin_shitbot/docs/help/imgs/md2picex1.png +0 -0
  24. nonebot_plugin_shitbot/docs/help/imgs/md2picex2.png +0 -0
  25. nonebot_plugin_shitbot/docs/help/index.md +22 -0
  26. nonebot_plugin_shitbot/docs/help/md2pic.md +57 -0
  27. nonebot_plugin_shitbot/docs/help/perm.md +110 -0
  28. nonebot_plugin_shitbot/docs/help/pixiv.md +42 -0
  29. nonebot_plugin_shitbot/docs/help/randpic.md +34 -0
  30. nonebot_plugin_shitbot/docs/help/session.md +38 -0
  31. nonebot_plugin_shitbot/docs/help/shitpost.md +39 -0
  32. nonebot_plugin_shitbot/docs//346/235/203/351/231/220/347/263/273/347/273/237.md +220 -0
  33. nonebot_plugin_shitbot/handlers.py +150 -0
  34. nonebot_plugin_shitbot/msgutils.py +442 -0
  35. nonebot_plugin_shitbot/parser.py +284 -0
  36. nonebot_plugin_shitbot/permissions.py +198 -0
  37. nonebot_plugin_shitbot/scripts/p2png.sh +38 -0
  38. nonebot_plugin_shitbot/scripts/png2fr.sh +19 -0
  39. nonebot_plugin_shitbot/scripts/png2v.sh +24 -0
  40. nonebot_plugin_shitbot/session.py +76 -0
  41. nonebot_plugin_shitbot/tasks.py +25 -0
  42. nonebot_plugin_shitbot-0.1.0.dist-info/METADATA +17 -0
  43. nonebot_plugin_shitbot-0.1.0.dist-info/RECORD +46 -0
  44. nonebot_plugin_shitbot-0.1.0.dist-info/WHEEL +5 -0
  45. nonebot_plugin_shitbot-0.1.0.dist-info/licenses/LICENSE +661 -0
  46. nonebot_plugin_shitbot-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,371 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import shutil
5
+ import uuid
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import httpx
10
+ from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent, MessageSegment
11
+ from nonebot.log import logger
12
+
13
+ from ..aux import rm_cache, stuff_download
14
+ from ..command import BotCommand
15
+ from ..config import config
16
+ from ..msgutils import dump_message, get_multimedias_url
17
+ from ..parser import BotArgParser
18
+ from ..tasks import EndOfQueue, prod_cons
19
+
20
+ if TYPE_CHECKING:
21
+ from ..session import BotSession
22
+
23
+ bash = shutil.which("bash") or "/bin/bash"
24
+
25
+
26
+ class BotCommandConvert(BotCommand):
27
+ _name = "convert"
28
+
29
+ def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
30
+ super().__init__(bot, session, _pid=_pid, _internal=_internal)
31
+ self._mode = "video"
32
+ self._runlock = asyncio.Lock()
33
+ self._if_accept_pic = False
34
+ self._prod_lock = asyncio.Lock()
35
+ self._convert_lock = asyncio.Lock()
36
+ self._urls: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
37
+ self._downloads: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
38
+ self._pngs: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
39
+ self._outputs: asyncio.Queue[str | EndOfQueue] = asyncio.Queue()
40
+
41
+ @property
42
+ def outputs(self):
43
+ return self._outputs
44
+
45
+ def _init_parser(self):
46
+ parser = BotArgParser()
47
+ start = parser.add_subparser("start")
48
+ stop = parser.add_subparser("stop")
49
+ start.set_rule(max=0)
50
+ start.add_opt("-m", required=True, choice=["video", "frame"], default=["video"])
51
+ stop.set_rule(max=0)
52
+ parser.set_rule(max=0, need_subcmd=True)
53
+ return parser
54
+
55
+ async def _guard_state(self, subcmd=None):
56
+ if subcmd is None:
57
+ return False
58
+ if self._argv is not None and subcmd == "start":
59
+ if (
60
+ self._session is None
61
+ or (command := self._session.commands.get(self._pid)) is None
62
+ ):
63
+ return False
64
+ tip = "错误:会话被占用\n"
65
+ tip += f"命令 {command.name} 正在运行,进行下一步前请先终止它或等待其完成。"
66
+ await self.send_msg(tip)
67
+ return False
68
+
69
+ if self._argv is None and subcmd == "stop":
70
+ tip = "错误:会话未开始\n"
71
+ tip += "你还没有开始收集图片,请先使用 /convert start 。"
72
+ await self.send_msg(tip)
73
+ self.unlock()
74
+ return False
75
+
76
+ return True
77
+
78
+ @staticmethod
79
+ async def _url_to_download(
80
+ download_url: str, client: httpx.AsyncClient, download_dir: Path
81
+ ):
82
+ try:
83
+ filename = f"{uuid.uuid4().hex}"
84
+ save_path = download_dir / filename
85
+ await stuff_download(client, download_url, save_path)
86
+ logger.info(f"下载图片成功: {save_path}")
87
+ return str(save_path)
88
+ except Exception as e:
89
+ logger.error(f"下载图片失败 {download_url}: {e}")
90
+ return ""
91
+
92
+ @staticmethod
93
+ async def _download_to_png(download_path: str, png_dir: Path):
94
+ if download_path == "":
95
+ return ""
96
+ png_path = png_dir / f"{uuid.uuid4().hex}.png"
97
+ logger.info(
98
+ f"执行图片转png脚本: {config.script_p2png_path} {download_path} {png_path}"
99
+ )
100
+ proc = await asyncio.create_subprocess_exec(
101
+ bash,
102
+ str(config.script_p2png_path),
103
+ str(download_path),
104
+ str(png_path),
105
+ stdout=asyncio.subprocess.PIPE,
106
+ stderr=asyncio.subprocess.PIPE,
107
+ )
108
+ stdout, stderr = await proc.communicate()
109
+ if stdout:
110
+ logger.info(f"脚本 stdout:\n{stdout.decode()}")
111
+ if stderr:
112
+ logger.warning(f"脚本 stderr:\n{stderr.decode()}")
113
+ if proc.returncode != 0:
114
+ logger.error(f"转换失败: {stderr.decode()[:]}")
115
+ return ""
116
+ logger.info(f"转换图片成功: {png_path}")
117
+ return str(png_path)
118
+
119
+ @staticmethod
120
+ async def _png_to_video(png_path: str, video_dir: Path):
121
+ if png_path == "":
122
+ return ""
123
+ video_path = video_dir / f"{uuid.uuid4().hex}.mp4"
124
+ logger.info(
125
+ f"执行图片转视频脚本: {config.script_png2v_path} {png_path} {video_path}"
126
+ )
127
+ proc = await asyncio.create_subprocess_exec(
128
+ bash,
129
+ str(config.script_png2v_path),
130
+ str(png_path),
131
+ str(video_path),
132
+ stdout=asyncio.subprocess.PIPE,
133
+ stderr=asyncio.subprocess.PIPE,
134
+ )
135
+ stdout, stderr = await proc.communicate()
136
+ if stdout:
137
+ logger.info(f"脚本 stdout:\n{stdout.decode()}")
138
+ if stderr:
139
+ logger.warning(f"脚本 stderr:\n{stderr.decode()}")
140
+ if proc.returncode != 0:
141
+ logger.error(f"转换失败: {stderr.decode()[:]}")
142
+ return ""
143
+ logger.info(f"转换视频成功: {video_path}")
144
+ return str(video_path)
145
+
146
+ @staticmethod
147
+ async def _png_to_frame(png_path: str, frame_dir: Path):
148
+ if png_path == "":
149
+ return ""
150
+ frame_path = frame_dir / f"{uuid.uuid4().hex}.png"
151
+ logger.info(
152
+ f"执行图片加框脚本: {config.script_png2fr_path} {png_path} {frame_path}"
153
+ )
154
+ proc = await asyncio.create_subprocess_exec(
155
+ bash,
156
+ str(config.script_png2fr_path),
157
+ str(png_path),
158
+ str(frame_path),
159
+ stdout=asyncio.subprocess.PIPE,
160
+ stderr=asyncio.subprocess.PIPE,
161
+ )
162
+ stdout, stderr = await proc.communicate()
163
+ if stdout:
164
+ logger.info(f"脚本 stdout:\n{stdout.decode()}")
165
+ if stderr:
166
+ logger.warning(f"脚本 stderr:\n{stderr.decode()}")
167
+ if proc.returncode != 0:
168
+ logger.error(f"转换失败: {stderr.decode()[:]}")
169
+ return ""
170
+ logger.info(f"转换方形图片成功: {frame_path}")
171
+ return str(frame_path)
172
+
173
+ async def _send_outputs(self):
174
+ if not self.session:
175
+ return
176
+ outputs = []
177
+ while True:
178
+ output = await self._outputs.get()
179
+ if isinstance(output, EndOfQueue):
180
+ break
181
+ if output == "":
182
+ continue
183
+ outputs.append(output)
184
+
185
+ logger.info(f"用户 {self.session.user_id} 结束收集,共收到 {len(outputs)} 张")
186
+
187
+ if len(outputs) == 0:
188
+ await self.send_msg("没有输出被生成。")
189
+ return
190
+
191
+ await self.send_msg(f"共生成 {len(outputs)} 个输出,逐个发送…")
192
+
193
+ for i, output in enumerate(outputs):
194
+ output_file = Path(output)
195
+ if not output_file.exists():
196
+ logger.error(f"输出文件不存在: {output_file}")
197
+ continue
198
+ container_path = config.client_base / output_file.relative_to(
199
+ config.bot_base
200
+ )
201
+ try:
202
+ if self._mode == "video":
203
+ await self.send_msg(
204
+ Message(MessageSegment.video(f"file://{container_path}"))
205
+ )
206
+ elif self._mode == "frame":
207
+ await self.send_msg(
208
+ Message(MessageSegment.image(f"file://{container_path}"))
209
+ )
210
+ logger.info(f"发送第 {i + 1} 个输出成功: {output_file.name}")
211
+ except Exception as e:
212
+ logger.error(f"发送 {output_file.name} 失败: {e}")
213
+ await self.send_msg(
214
+ f"发送 {output_file.name} 失败: {e}\n尝试以文件形式发送..."
215
+ )
216
+ try:
217
+ msg = Message(
218
+ MessageSegment("file", {"file": f"file://{container_path}"})
219
+ )
220
+ await self.send_msg(msg)
221
+ logger.info(
222
+ f"以文件形式发送第 {i + 1} 个输出成功: {output_file.name}"
223
+ )
224
+ except Exception as e:
225
+ logger.error(f"发送 {output_file.name} 失败: {e}")
226
+ await self.send_msg(
227
+ f"发送第 {i + 1} 个输出 {output_file.name} 失败: {e}"
228
+ )
229
+
230
+ await asyncio.sleep(0.25)
231
+
232
+ await self.send_msg("输出发送完毕。")
233
+
234
+ async def _convert_start(self):
235
+ if not self.session:
236
+ return
237
+ downloads_dir = (
238
+ config.cache
239
+ / self.session.group_id
240
+ / self.session.user_id
241
+ / str(self._pid)
242
+ / "downloads"
243
+ )
244
+ pngs_dir = (
245
+ config.cache
246
+ / self.session.group_id
247
+ / self.session.user_id
248
+ / str(self._pid)
249
+ / "pngs"
250
+ )
251
+ outputs_dir = (
252
+ config.cache
253
+ / self.session.group_id
254
+ / self.session.user_id
255
+ / str(self._pid)
256
+ / "outputs"
257
+ )
258
+
259
+ downloads_dir.mkdir(parents=True, exist_ok=True)
260
+ pngs_dir.mkdir(parents=True, exist_ok=True)
261
+ outputs_dir.mkdir(parents=True, exist_ok=True)
262
+
263
+ async def _urls_to_downloads():
264
+ try:
265
+ async with httpx.AsyncClient() as client:
266
+ await prod_cons(
267
+ self._urls,
268
+ self._downloads,
269
+ self._url_to_download,
270
+ client,
271
+ downloads_dir,
272
+ )
273
+ except Exception as e:
274
+ logger.exception(f"urls_to_downloads 管道异常退出: {e}")
275
+
276
+ async def _downloads_to_pngs():
277
+ try:
278
+ await prod_cons(
279
+ self._downloads, self._pngs, self._download_to_png, pngs_dir
280
+ )
281
+ except Exception as e:
282
+ logger.exception(f"downloads_to_pngs 管道异常退出: {e}")
283
+
284
+ async def _pngs_to_outputs():
285
+ try:
286
+ async with self._convert_lock:
287
+ if self._mode == "video":
288
+ await prod_cons(
289
+ self._pngs, self._outputs, self._png_to_video, outputs_dir
290
+ )
291
+ elif self._mode == "frame":
292
+ await prod_cons(
293
+ self._pngs, self._outputs, self._png_to_frame, outputs_dir
294
+ )
295
+ except Exception as e:
296
+ logger.exception(f"pngs_to_outputs 管道异常退出: {e}")
297
+
298
+ asyncio.create_task(_urls_to_downloads())
299
+ asyncio.create_task(_downloads_to_pngs())
300
+ asyncio.create_task(_pngs_to_outputs())
301
+
302
+ logger.info(f"用户 {self.session.user_id} 开始了图片收集")
303
+ await self.send_msg(
304
+ "图片收集已开始, Bot 会收集本条信息后你发送的所有图片,直到你发送 /convert stop 完成收集。"
305
+ )
306
+
307
+ self._if_accept_pic = True
308
+ return
309
+
310
+ async def _convert_stop(self):
311
+ if not self.session:
312
+ return
313
+ self._if_accept_pic = False
314
+
315
+ await self.send_msg("转换图片中...")
316
+ async with self._prod_lock:
317
+ await self._urls.put(EndOfQueue())
318
+
319
+ async with self._convert_lock:
320
+ pass
321
+ await self.send_msg("转换完毕。")
322
+
323
+ await self._send_outputs()
324
+ await rm_cache(self.session.group_id, self.session.user_id, str(self._pid))
325
+ self.unlock()
326
+ return
327
+
328
+ async def roger(self, event: MessageEvent):
329
+ async with self._prod_lock:
330
+ if not self._if_accept_pic:
331
+ return
332
+ msg = await dump_message(self.bot, event.get_message())
333
+ url_list = get_multimedias_url(msg, basetypes=["image"])
334
+ for url in url_list:
335
+ await self._urls.put(url)
336
+
337
+ async def run(self, args: Message):
338
+ async with self._runlock:
339
+ if not self.session:
340
+ return
341
+
342
+ new_argv = args.extract_plain_text().strip().split()
343
+ if not await self._legal_case(new_argv):
344
+ if self._argv is None:
345
+ self.unlock()
346
+ return
347
+
348
+ self._parser.parse_argv(new_argv)
349
+ subcmd = self._parser.subcmd
350
+
351
+ if not await self._guard_state(subcmd):
352
+ return
353
+
354
+ self._argv = new_argv
355
+
356
+ if self._pid >= 0 and not self._check_perm("convert"):
357
+ await self.send_msg("权限不足")
358
+ self.unlock()
359
+ return
360
+
361
+ if subcmd == "start":
362
+ new_mode = self._parser.subparsers[subcmd].opts_value.get(
363
+ "-m", ["video"]
364
+ )[0]
365
+ self._mode = new_mode
366
+ await self._convert_start()
367
+ return
368
+
369
+ if subcmd == "stop":
370
+ await self._convert_stop()
371
+ return
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from nonebot.adapters.onebot.v11 import Bot, Message
8
+
9
+ from ..command import BotCommand
10
+ from ..parser import BotArgParser
11
+ from .md2pic_cmd import BotCommandMd2pic
12
+
13
+ if TYPE_CHECKING:
14
+ from ..session import BotSession
15
+
16
+
17
+ class BotCommandHelp(BotCommand):
18
+ _name = "help"
19
+
20
+ def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
21
+ super().__init__(bot, session, _pid=_pid, _internal=_internal)
22
+
23
+ def _init_parser(self):
24
+ parser = BotArgParser()
25
+ parser.add_subparser("help")
26
+ parser.add_subparser("session")
27
+ parser.add_subparser("perm")
28
+ parser.add_subparser("convert")
29
+ parser.add_subparser("randpic")
30
+ parser.add_subparser("shitpost")
31
+ parser.add_subparser("advrandpic")
32
+ parser.add_subparser("md2pic")
33
+ parser.add_subparser("pixiv")
34
+ return parser
35
+
36
+ async def run(self, args: Message):
37
+ if not self.session:
38
+ return
39
+ new_argv = args.extract_plain_text().strip().split()
40
+ # I know it's impossible to be illegal. Just for formalism :)
41
+ if not await self._legal_case(new_argv):
42
+ if self._argv is None:
43
+ self.unlock()
44
+ return
45
+
46
+ self._argv = new_argv
47
+ self._parser.parse_argv(self._argv)
48
+ subcmd = self._parser.subcmd
49
+
50
+ if not self._check_perm("help"):
51
+ await self.send_msg("权限不足")
52
+ self.unlock()
53
+ return
54
+
55
+ help_dir = Path(__file__).resolve().parent.parent / "docs" / "help"
56
+ tip = "错误: 帮助文档目录不存在"
57
+ if help_dir.exists():
58
+ tip = "错误: 帮助文档 index.md 不存在"
59
+ help_path = help_dir / "index.md"
60
+ if help_path.exists():
61
+ tip = help_path.read_text(encoding="utf-8")
62
+ if subcmd in (
63
+ "help",
64
+ "session",
65
+ "perm",
66
+ "convert",
67
+ "randpic",
68
+ "shitpost",
69
+ "advrandpic",
70
+ "md2pic",
71
+ "pixiv",
72
+ ):
73
+ tip = f"错误: 帮助文档 {subcmd}.md 不存在"
74
+ help_path = help_dir / f"{subcmd}.md"
75
+ if help_path.exists():
76
+ tip = help_path.read_text(encoding="utf-8")
77
+
78
+ tip = re.sub(
79
+ r"!\[([^\]]*)\]\((?!https?://|/)([^)]+)\)",
80
+ lambda m: f"![{m.group(1)}]({(help_dir / m.group(2)).resolve()})",
81
+ tip,
82
+ )
83
+
84
+ _pid = -1
85
+ pids = self.session.commands.keys()
86
+ while _pid in pids:
87
+ _pid -= 1
88
+ md2pic = BotCommandMd2pic.make(self.bot, self.session, _pid=_pid)
89
+ tip = "-c\n" + tip
90
+ msg = Message(tip)
91
+ await md2pic.run(msg) # type: ignore
92
+ self.unlock()
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from nonebot import require
8
+ from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment
9
+ from PIL import Image
10
+
11
+ require("nonebot_plugin_htmlrender")
12
+ from nonebot_plugin_htmlrender import md_to_pic
13
+
14
+ from ..command import BotCommand
15
+ from ..parser import BotArgParser
16
+
17
+ if TYPE_CHECKING:
18
+ from ..session import BotSession
19
+
20
+
21
+ class BotCommandMd2pic(BotCommand):
22
+ _name = "md2pic"
23
+
24
+ def __init__(self, bot: Bot, session: BotSession, *, _pid: int, _internal=None):
25
+ super().__init__(bot, session, _pid=_pid, _internal=_internal)
26
+ self._md = ""
27
+ self._scale = 2
28
+ self._css = ""
29
+ self._padding = 30
30
+ self._min_w = 20
31
+ self._max_w = 2000
32
+ self._min_h = 20
33
+ self._max_h = None
34
+
35
+ def _init_parser(self):
36
+ parser = BotArgParser()
37
+ parser.set_rule(min=1, max=1)
38
+ parser.add_opt("-c", necessary=True)
39
+ parser.add_opt("-s", required=True, type=float, default=[2])
40
+ parser.add_opt(
41
+ "-t",
42
+ required=True,
43
+ choice=["github-markdown-dark-dimmed"],
44
+ default=["github-markdown-dark-dimmed"],
45
+ )
46
+ parser.add_opt("--padding", required=True, type=int, default=[30])
47
+ parser.add_opt("--min_w", required=True, type=int, default=[20])
48
+ parser.add_opt("--max_w", required=True, type=int, default=[2000])
49
+ parser.add_opt("--min_h", required=True, type=int, default=[20])
50
+ parser.add_opt("--max_h", required=True, type=int, default=[])
51
+
52
+ return parser
53
+
54
+ async def _render(self) -> bytes:
55
+
56
+ min_w = int(self._scale * self._min_w)
57
+ max_w = self._max_w
58
+ min_h = int(self._scale * self._min_h)
59
+ max_h = (
60
+ int(self._scale * self._max_h) if self._max_h is not None else self._max_h
61
+ )
62
+ padding = int(self._scale * self._padding)
63
+
64
+ raw = await md_to_pic(
65
+ self._md, width=max_w, css_path=self._css, device_scale_factor=self._scale
66
+ )
67
+ img = Image.open(io.BytesIO(raw))
68
+
69
+ # Get the background color (the upper-left color)
70
+ bgpx: Any = img.getpixel((0, 0))
71
+ bgr, bgg, bgb = int(bgpx[0]), int(bgpx[1]), int(bgpx[2])
72
+
73
+ def is_bg(r: int, g: int, b: int, *, delta: int = 6) -> bool:
74
+ return (
75
+ abs(r - bgr) < delta and abs(g - bgg) < delta and abs(b - bgb) < delta
76
+ )
77
+
78
+ def is_column_has_content(x: int):
79
+ for y in range(0, h, 4):
80
+ pixel: Any = img.getpixel((x, y))
81
+ r, g, b = int(pixel[0]), int(pixel[1]), int(pixel[2])
82
+ if not is_bg(r, g, b):
83
+ return True
84
+ return False
85
+
86
+ def is_row_has_content(y: int):
87
+ for x in range(0, w, 4):
88
+ pixel: Any = img.getpixel((x, y))
89
+ r, g, b = int(pixel[0]), int(pixel[1]), int(pixel[2])
90
+ if not is_bg(r, g, b):
91
+ return True
92
+ return False
93
+
94
+ w, h = img.size
95
+ content_left, content_right = 0, w
96
+ content_top, content_bottom = 0, h
97
+
98
+ # Find out the leftmost column that has content
99
+ for x in range(w):
100
+ if is_column_has_content(x):
101
+ content_left = x
102
+ break
103
+
104
+ # Find out the rightmost column that has content
105
+ for x in range(w - 1, content_left - 1, -1):
106
+ if is_column_has_content(x):
107
+ content_right = x + 1
108
+ break
109
+
110
+ # Find out the top row that has content
111
+ for y in range(h):
112
+ if is_row_has_content(y):
113
+ content_top = y
114
+ break
115
+
116
+ # Find out the bottom row that has content
117
+ for y in range(h - 1, content_top - 1, -1):
118
+ if is_row_has_content(y):
119
+ content_bottom = y + 1
120
+ break
121
+
122
+ crop_left = max(0, content_left - padding)
123
+ crop_right = max(crop_left + min_w + 1, content_right + padding)
124
+ crop_top = max(0, content_top - padding)
125
+ crop_bottom = max(crop_top + min_h + 1, content_bottom + padding)
126
+ if max_h is not None:
127
+ crop_bottom = min(crop_bottom, crop_top + max_h)
128
+
129
+ crop_right = min(crop_right, w)
130
+ crop_bottom = min(crop_bottom, h)
131
+ img = img.crop((crop_left, crop_top, crop_right, crop_bottom))
132
+
133
+ buf = io.BytesIO()
134
+ img.save(buf, format="PNG")
135
+ return buf.getvalue()
136
+
137
+ async def run(self, args: Message):
138
+ if not self.session:
139
+ return
140
+ new_argss = args.extract_plain_text().split("\n", 1)
141
+ if len(new_argss) < 2:
142
+ await self._send_format_error()
143
+ if self._argv is None:
144
+ self.unlock()
145
+ return
146
+ new_argv = new_argss[0].strip().split()
147
+ new_argv.append(new_argss[1])
148
+ if not await self._legal_case(new_argv):
149
+ if self._argv is None:
150
+ self.unlock()
151
+ return
152
+
153
+ if not await self._guard_state():
154
+ return
155
+
156
+ self._argv = new_argv
157
+ self._parser.parse_argv(self._argv)
158
+
159
+ if self._pid >= 0 and not self._check_perm("md2pic"):
160
+ await self.send_msg("权限不足")
161
+ self.unlock()
162
+ return
163
+
164
+ self._scale = self._parser.opts_value["-s"][0]
165
+ theme = self._parser.opts_value["-t"][0]
166
+ self._css = str(Path(__file__).resolve().parent.parent / "css" / f"{theme}.css")
167
+ self._padding = self._parser.opts_value["--padding"][0]
168
+ self._min_w = self._parser.opts_value["--min_w"][0]
169
+ self._max_w = self._parser.opts_value["--max_w"][0]
170
+ self._min_h = self._parser.opts_value["--min_h"][0]
171
+ self._max_h = (
172
+ self._parser.opts_value["--max_h"][0]
173
+ if len(self._parser.opts_value["--max_h"]) > 0
174
+ else None
175
+ )
176
+ self._md = self._parser.value[0]
177
+
178
+ self._padding = max(0, self._padding)
179
+ self._padding = min(50, self._padding)
180
+ self._max_w = min(10000000, self._max_w)
181
+ self._max_w = max(0, self._max_w)
182
+ self._min_w = min(self._max_w, self._min_w)
183
+ self._min_w = max(0, self._min_w)
184
+ if self._max_h is not None:
185
+ self._max_h = min(10000000, self._max_h)
186
+ self._max_h = max(0, self._max_h)
187
+ self._min_h = min(self._max_h, self._min_h)
188
+ self._min_h = max(0, self._min_h)
189
+
190
+ img = await self._render()
191
+
192
+ msg = Message(MessageSegment.image(img))
193
+ await self.send_msg(msg)
194
+
195
+ self.unlock()