From 1980f8a8958f88be3475af41a87c6ef3bdd8adec Mon Sep 17 00:00:00 2001 From: pi-agent Date: Sat, 14 Mar 2026 00:52:34 +0800 Subject: [PATCH] feat: add jpeg damage filter to fx --- konabot/docs/user/fx.txt | 2 ++ konabot/plugins/fx_process/fx_handle.py | 24 +++++++++++++++ konabot/plugins/fx_process/fx_manager.py | 1 + tests/test_fx_process.py | 37 ++++++++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 tests/test_fx_process.py diff --git a/konabot/docs/user/fx.txt b/konabot/docs/user/fx.txt index 254ca59..c7ea93d 100644 --- a/konabot/docs/user/fx.txt +++ b/konabot/docs/user/fx.txt @@ -76,6 +76,8 @@ fx [滤镜名称] <参数1> <参数2> ... * ```fx 设置遮罩``` * ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>``` * ```fx 晃动 <最大偏移量=5> <运动模糊=False>``` +* ```fx JPEG损坏 <质量=10>``` + * 质量范围建议为 1~95,数值越低,压缩痕迹越重、效果越搞笑。 * ```fx 动图 <帧率=10>``` ### 多图像处理器 diff --git a/konabot/plugins/fx_process/fx_handle.py b/konabot/plugins/fx_process/fx_handle.py index acc8165..f2a2048 100644 --- a/konabot/plugins/fx_process/fx_handle.py +++ b/konabot/plugins/fx_process/fx_handle.py @@ -1,4 +1,5 @@ import random +from io import BytesIO from PIL import Image, ImageFilter, ImageDraw, ImageStat, ImageFont from PIL import ImageEnhance from PIL import ImageChops @@ -167,6 +168,29 @@ class ImageFilterImplement: return Image.fromarray(result, 'RGBA') + # JPEG 损坏感压缩 + @staticmethod + def apply_jpeg_damage(image: Image.Image, quality: int = 10) -> Image.Image: + quality = max(1, min(95, int(quality))) + + alpha = None + if image.mode in ('RGBA', 'LA') or (image.mode == 'P' and 'transparency' in image.info): + rgba_image = image.convert('RGBA') + alpha = rgba_image.getchannel('A') + rgb_image = Image.new('RGB', rgba_image.size, (255, 255, 255)) + rgb_image.paste(rgba_image, mask=alpha) + else: + rgb_image = image.convert('RGB') + + output = BytesIO() + rgb_image.save(output, format='JPEG', quality=quality, optimize=False) + output.seek(0) + damaged = Image.open(output).convert('RGB') + + if alpha is not None: + return Image.merge('RGBA', (*damaged.split(), alpha)) + return damaged.convert('RGBA') + # 缩放 @staticmethod def apply_resize(image: Image.Image, scale: float = 1.5, scale_y = None) -> Image.Image: diff --git a/konabot/plugins/fx_process/fx_manager.py b/konabot/plugins/fx_process/fx_manager.py index af174e7..2b8fedd 100644 --- a/konabot/plugins/fx_process/fx_manager.py +++ b/konabot/plugins/fx_process/fx_manager.py @@ -50,6 +50,7 @@ class ImageFilterManager: "描边": ImageFilterImplement.apply_stroke, "形状描边": ImageFilterImplement.apply_shape_stroke, "半调": ImageFilterImplement.apply_halftone, + "JPEG损坏": ImageFilterImplement.apply_jpeg_damage, "设置通道": ImageFilterImplement.apply_set_channel, "设置遮罩": ImageFilterImplement.apply_set_mask, # 图像处理 diff --git a/tests/test_fx_process.py b/tests/test_fx_process.py new file mode 100644 index 0000000..98171f7 --- /dev/null +++ b/tests/test_fx_process.py @@ -0,0 +1,37 @@ +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +import nonebot +from PIL import Image + + +nonebot.init() + +MODULE_PATH = Path(__file__).resolve().parents[1] / "konabot/plugins/fx_process/fx_handle.py" +SPEC = spec_from_file_location("test_fx_handle_module", MODULE_PATH) +assert SPEC is not None and SPEC.loader is not None +fx_handle = module_from_spec(SPEC) +SPEC.loader.exec_module(fx_handle) +ImageFilterImplement = fx_handle.ImageFilterImplement + + +def test_apply_jpeg_damage_keeps_size_and_rgba_mode(): + image = Image.new("RGBA", (32, 24), (255, 0, 0, 128)) + + result = ImageFilterImplement.apply_jpeg_damage(image, 5) + + assert result.size == image.size + assert result.mode == "RGBA" + assert result.getchannel("A").getextrema() == (128, 128) + + +def test_apply_jpeg_damage_clamps_quality_range(): + image = Image.new("RGB", (16, 16), (123, 222, 111)) + + low = ImageFilterImplement.apply_jpeg_damage(image, -10) + high = ImageFilterImplement.apply_jpeg_damage(image, 999) + + assert low.size == image.size + assert high.size == image.size + assert low.mode == "RGBA" + assert high.mode == "RGBA" -- 2.49.0