"""GIF 倒放 —— 业务逻辑(Service 层)。""" from __future__ import annotations from io import BytesIO import httpx from PIL import Image, ImageSequence from nonebot.log import logger async def reverse_gif_bytes(image_urls: list[str]) -> tuple[bytes | None, str | None]: """下载并翻转 GIF。 Returns: (bytes, None) 成功; (None, msg) 失败原因(无 GIF / 帧数过多 / 处理失败)。 """ gif_img = [] async with httpx.AsyncClient(timeout=30) as client: for image_url in image_urls: try: resp = await client.get(image_url) resp.raise_for_status() image = Image.open(BytesIO(resp.content)) if getattr(image, "n_frames", 1) == 1: continue gif_img.append(image) except Exception as e: # noqa: BLE001 logger.warning(f"下载或解析图片失败: {e}") if not gif_img: return None, "没有发现GIF图片" suit_gifs = [gif for gif in gif_img if gif.n_frames <= 400] if not suit_gifs: return None, "GIF帧数太多了,懒得倒放😅" suit_image = suit_gifs[0] sequence = [f.copy() for f in ImageSequence.Iterator(suit_image)] logger.info(f"GIF帧数: {len(sequence)}") if len(sequence) <= 2: return None, "写入文件时发生未知错误" sequence.reverse() bio = BytesIO() sequence[0].save( bio, format="GIF", save_all=True, append_images=sequence[1:], disposal=1, loop=0, ) bio.seek(0) return bio.getvalue(), None def image_to_bytes(image: Image.Image, input_format: str) -> bytes: with BytesIO() as bio: image.save(bio, format=input_format) return bio.getvalue()