From d096f43d38f9e89e9cc873eb708cf25483a4a6b5 Mon Sep 17 00:00:00 2001 From: passthem Date: Sun, 12 Oct 2025 12:40:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20giftool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/plugins/image_process/__init__.py | 138 ++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/konabot/plugins/image_process/__init__.py b/konabot/plugins/image_process/__init__.py index 0d679bd..2993a73 100644 --- a/konabot/plugins/image_process/__init__.py +++ b/konabot/plugins/image_process/__init__.py @@ -1,6 +1,12 @@ +import re +from io import BytesIO + from nonebot import on_message from nonebot.adapters import Bot +from nonebot_plugin_alconna import (Alconna, Args, Image, Option, UniMessage, + on_alconna) +from konabot.common.nb.exc import BotExceptionMessage from konabot.common.nb.extract_image import PIL_Image from konabot.common.nb.match_keyword import match_keyword from konabot.common.nb.reply_image import reply_image @@ -11,3 +17,135 @@ cmd_black_white = on_message(rule=match_keyword("黑白")) @cmd_black_white.handle() async def _(img: PIL_Image, bot: Bot): await reply_image(cmd_black_white, bot, img.convert("LA")) + + +def parse_timestamp(tx: str) -> float | None: + res = 0.0 + for component in tx.split(":"): + res *= 60 + if not re.match(r"^\d+(\.\d+)?$", component): + return + res += float(component) + return res + + +cmd_giftool = on_alconna(Alconna( + "giftool", + Args["img", Image | None], + Option("--ss", Args["start_point", str]), + Option("--frames:v", Args["frame_count", int]), + Option("-t", Args["length", str]), + Option("-to", Args["end_point", str]), +)) + + +@cmd_giftool.handle() +async def _( + image: PIL_Image, + start_point: str | None = None, + frame_count: int | None = None, + length: str | None = None, + end_point: str | None = None, +): + ss: None | float = None + if start_point: + ss = parse_timestamp(start_point) + if ss is None: + raise BotExceptionMessage("--ss 的格式不满足条件") + + t: None | float = None + if length: + t = parse_timestamp(length) + if t is None: + raise BotExceptionMessage("-t 的格式不满足条件") + + to: None | float = None + if end_point: + to = parse_timestamp(end_point) + if to is None: + raise BotExceptionMessage("-to 的格式不满足条件") + + if to is not None and ss is not None and to <= ss: + raise BotExceptionMessage("错误:出点时间小于入点") + if frame_count is not None and frame_count <= 0: + raise BotExceptionMessage("错误:帧数量应该大于 0") + + if not getattr(image, "is_animated", False): + raise BotExceptionMessage("错误:输入的不是动图(GIF)") + frames = [] + durations = [] + total_duration = 0.0 + + try: + for i in range(getattr(image, "n_frames")): + image.seek(i) + frames.append(image.copy()) + duration = image.info.get("duration", 100) # 单位:毫秒 + durations.append(duration) + total_duration += duration / 1000.0 # 转为秒 + except EOFError: + pass + + if not frames: + raise BotExceptionMessage("错误:读取 GIF 帧失败") + + def time_to_frame_index(target_time: float) -> int: + if target_time <= 0: + return 0 + cum = 0.0 + for idx, dur in enumerate(durations): + cum += dur / 1000.0 + if cum >= target_time: + return min(idx, len(frames) - 1) + return len(frames) - 1 + start_frame = 0 + end_frame = len(frames) - 1 + if ss is not None: + start_frame = time_to_frame_index(ss) + if to is not None: + end_frame = time_to_frame_index(to) + if end_frame < start_frame: + end_frame = start_frame + elif t is not None: + end_time = (ss or 0.0) + t + end_frame = time_to_frame_index(end_time) + if end_frame < start_frame: + end_frame = start_frame + + start_frame = max(0, start_frame) + end_frame = min(len(frames) - 1, end_frame) + selected_frames = frames[start_frame : end_frame + 1] + selected_durations = durations[start_frame : end_frame + 1] + + if frame_count is not None and frame_count > 0: + if frame_count >= len(selected_frames): + pass + else: + step = len(selected_frames) / frame_count + sampled_frames = [] + sampled_durations = [] + for i in range(frame_count): + idx = int(i * step) + sampled_frames.append(selected_frames[idx]) + sampled_durations.append( + sum(selected_durations) // len(selected_durations) + ) + selected_frames = sampled_frames + selected_durations = sampled_durations + + output_img = BytesIO() + + if selected_frames: + selected_frames[0].save( + output_img, + format="GIF", + save_all=True, + append_images=selected_frames[1:], + duration=selected_durations, + loop=0, + ) + else: + raise BotExceptionMessage("错误:没有可输出的帧") + output_img.seek(0) + + await cmd_giftool.send(await UniMessage().image(raw=output_img).export())