"""小番茄图片混淆 —— 编排(Service 层)。无 nonebot 依赖:下载 / 解码 / 变换 / 编码。""" from __future__ import annotations import asyncio import io import httpx from PIL import Image from ..utils.obfuscation import obfuscate, to_jpeg_bytes async def download_image(url: str) -> bytes | None: """下载图片;失败(超时/404/断链)返回 None,由调用方决定提示与重试。""" try: async with httpx.AsyncClient(timeout=30) as client: resp = await client.get(url) resp.raise_for_status() return resp.content except Exception: # noqa: BLE001 - 网络失败一律走 None,交给上层提示 return None async def run_pipeline( image_bytes: bytes, *, decrypt: bool, max_pixels: int, quality: int, ) -> tuple[bytes | None, str | None, list[str]]: """PIL 解码 → 混淆/解混淆(线程池) → JPEG 编码。 max_pixels 语义随方向不同: - 加密:超过则等比缩小(与网页版一致,自洽即可); - 解密:不能缩放(缩了就永远解不出),超过直接拒绝。 Returns: (jpeg_bytes, err, notes) —— err 非空时 bytes 为 None; notes 是给用户的附加提示(已压缩 / 动图仅首帧)。 """ try: img = Image.open(io.BytesIO(image_bytes)) img.load() # 提前解码,截断/坏文件在这里就暴露 except Exception: # noqa: BLE001 return None, "打不开这张图,可能不是有效的图片文件", [] width, height = img.size if decrypt and width * height > max_pixels: return ( None, f"图片太大({width}x{height}),解混淆不能缩放" f"(上限 {max_pixels / 1_000_000:.0f}MP),先裁剪或压小再试", [], ) try: out, scaled, extra_frames = await asyncio.to_thread( obfuscate, img, decrypt=decrypt, max_pixels=max_pixels ) data = await asyncio.to_thread(to_jpeg_bytes, out, quality) except Exception: # noqa: BLE001 return None, "处理这张图失败了,换一张再试试", [] notes: list[str] = [] if scaled: notes.append(f"图片超过 {max_pixels / 1_000_000:.0f}MP,已等比缩小后再混淆") if extra_frames: notes.append("这是动图,只处理了第一帧") return data, None, notes