208 lines
6.2 KiB
Python
208 lines
6.2 KiB
Python
import re
|
|
from io import BytesIO
|
|
from typing import Any
|
|
|
|
import PIL
|
|
import PIL.Image
|
|
import cv2
|
|
import imageio.v3 as iio
|
|
from nonebot import on_message
|
|
from nonebot.adapters import Bot
|
|
from nonebot_plugin_alconna import Alconna, Args, Image, Option, UniMessage, on_alconna
|
|
import numpy
|
|
|
|
from konabot.common.nb.exc import BotExceptionMessage
|
|
from konabot.common.nb.extract_image import DepImageBytes, DepPILImage
|
|
from konabot.common.nb.match_keyword import match_keyword
|
|
from konabot.common.nb.reply_image import reply_image
|
|
|
|
# 保持不变
|
|
cmd_black_white = on_message(rule=match_keyword("黑白"))
|
|
|
|
|
|
@cmd_black_white.handle()
|
|
async def _(img: DepPILImage, 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]),
|
|
Option("--speed", Args["speed_factor", float], default=1.0, alias=["-s"]),
|
|
)
|
|
)
|
|
|
|
|
|
@cmd_giftool.handle()
|
|
async def _(
|
|
image: DepImageBytes,
|
|
start_point: str | None = None,
|
|
frame_count: int | None = None,
|
|
length: str | None = None,
|
|
speed_factor: float = 1.0,
|
|
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 speed_factor == 0:
|
|
raise BotExceptionMessage("错误:速度不能为 0")
|
|
|
|
is_rev = speed_factor < 0
|
|
speed_factor = abs(speed_factor)
|
|
|
|
try:
|
|
reader = iio.imread(BytesIO(image), extension=".gif", index=None)
|
|
np_frames = list(reader)
|
|
|
|
_pil = PIL.Image.open(BytesIO(image))
|
|
durations: list[float] = []
|
|
while True:
|
|
try:
|
|
duration = _pil.info.get('duration', 20)
|
|
durations.append(max(duration, 20) / 1000)
|
|
_pil.seek(_pil.tell() + 1)
|
|
except EOFError:
|
|
break
|
|
except Exception:
|
|
raise BotExceptionMessage("错误:读取 GIF 帧失败")
|
|
|
|
##
|
|
# 根据开始、结束时间或者帧数量来裁取 GIF 图
|
|
begin_time = ss or 0
|
|
end_time = sum(durations)
|
|
end_time = min(begin_time + (t or end_time), to or end_time, end_time)
|
|
|
|
accumulated = 0.0
|
|
status = 0
|
|
|
|
sel_np_frames: list[numpy.ndarray[Any, Any]] = []
|
|
sel_durations: list[float] = []
|
|
|
|
for i in range(len(np_frames)):
|
|
frame = np_frames[i]
|
|
duration = durations[i]
|
|
|
|
if status == 0:
|
|
if accumulated + duration > begin_time:
|
|
status = 1
|
|
sel_np_frames.append(frame)
|
|
sel_durations.append(accumulated + duration - begin_time)
|
|
elif accumulated + duration == begin_time:
|
|
status = 1
|
|
elif status == 1:
|
|
if accumulated + duration >= end_time:
|
|
included_duration = end_time - accumulated
|
|
if included_duration > 0:
|
|
sel_np_frames.append(frame)
|
|
sel_durations.append(included_duration)
|
|
break
|
|
sel_np_frames.append(frame)
|
|
sel_durations.append(duration)
|
|
|
|
accumulated += duration
|
|
|
|
if not sel_np_frames:
|
|
raise BotExceptionMessage("错误:裁取 GIF 帧失败(可能时间设置错误)")
|
|
|
|
rdur_ms_unprocessed = [dur / speed_factor * 1000 for dur in sel_durations]
|
|
rframes: list[numpy.ndarray] = []
|
|
rdur_ms: list[int] = []
|
|
|
|
acc_mod_20 = 0
|
|
|
|
for i in range(len(sel_np_frames)):
|
|
fr = sel_np_frames[i]
|
|
du = rdur_ms_unprocessed[i]
|
|
|
|
if du >= 20:
|
|
rframes.append(fr)
|
|
rdur_ms.append(int(round(du)))
|
|
acc_mod_20 = 0
|
|
else:
|
|
if acc_mod_20 == 0:
|
|
rframes.append(fr)
|
|
rdur_ms.append(20)
|
|
acc_mod_20 += du
|
|
else:
|
|
acc_mod_20 += du
|
|
if acc_mod_20 >= 20:
|
|
acc_mod_20 = 0
|
|
|
|
if len(rframes) == 1 and len(sel_np_frames) > 1:
|
|
middle_index = max(2, len(sel_np_frames) // 2)
|
|
rframes.append(sel_np_frames[middle_index])
|
|
rdur_ms.append(20)
|
|
|
|
if is_rev:
|
|
rframes = rframes[::-1]
|
|
rdur_ms = rdur_ms[::-1]
|
|
|
|
output_img = BytesIO()
|
|
|
|
if rframes:
|
|
do_transparent = any((f.shape[2] == 4 for f in rframes))
|
|
if do_transparent:
|
|
rframes = [(
|
|
f
|
|
if f.shape[2] == 4
|
|
else cv2.cvtColor(f, cv2.COLOR_RGB2RGBA)
|
|
) for f in rframes]
|
|
kwargs = { "transparency": 0, "disposal": 2, "mode": "RGBA" }
|
|
else:
|
|
kwargs = {}
|
|
try:
|
|
iio.imwrite(
|
|
output_img,
|
|
rframes,
|
|
extension=".gif",
|
|
duration=rdur_ms,
|
|
loop=0,
|
|
optimize=True,
|
|
plugin="pillow",
|
|
**kwargs,
|
|
)
|
|
except Exception as e:
|
|
raise BotExceptionMessage(f"错误:写入 GIF 失败: {e}")
|
|
else:
|
|
raise BotExceptionMessage("错误:没有可输出的帧")
|
|
output_img.seek(0)
|
|
await cmd_giftool.send(await UniMessage().image(raw=output_img).export())
|