Compare commits

...

7 Commits

Author SHA1 Message Date
274ca0fa9a 初步尝试UI化 2025-12-03 22:00:44 +08:00
c72cdd6a6b 新滤镜,新修复
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 17:20:46 +08:00
16b0451133 删掉黑白
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 13:32:11 +08:00
cb34813c4b Merge branch 'master' of ssh://gitea.service.jazzwhom.top:2221/mttu-developers/konabot 2025-12-03 13:22:09 +08:00
2de3be271e 最新最热模糊
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 13:10:16 +08:00
f7d2168dac 最新最热 2025-12-03 12:25:39 +08:00
26e10be4ec 修复相关文档 2025-11-28 17:10:18 +08:00
10 changed files with 1599 additions and 102 deletions

View File

@ -19,14 +19,26 @@ fx [滤镜名称] <参数1> <参数2> ...
## 可用滤镜列表
### 基础滤镜
* ```fx 模糊 <半径=10>```
* ```fx 马赛克 <像素大小=10>```
* ```fx 轮廓```
* ```fx 锐化```
* ```fx 边缘增强```
* ```fx 浮雕```
* ```fx 查找边缘```
* ```fx 平滑```
* ```fx 暗角 <半径=1.5>```
* ```fx 发光 <强度=0.5> <模糊半径=15>```
* ```fx 噪点 <数量=0.05>```
* ```fx 素描```
* ```fx 阴影 <x偏移量=10> <y偏移量=10> <模糊量=10> <不透明度=0.5> <阴影颜色=black>```
### 模糊滤镜
* ```fx 模糊 <半径=10>```
* ```fx 马赛克 <像素大小=10>```
* ```fx 径向模糊 <强度=3.0> <采样量=6>```
* ```fx 旋转模糊 <强度=30.0> <采样量=6>```
* ```fx 方向模糊 <角度=0.0> <距离=20> <采样量=6>```
* ```fx 缩放模糊 <强度=0.1> <采样量=6>```
* ```fx 边缘模糊 <半径=10.0>```
### 色彩处理滤镜
* ```fx 反色```
@ -36,13 +48,29 @@ fx [滤镜名称] <参数1> <参数2> ...
* ```fx 亮度 <因子=1.5>```
* ```fx 色彩 <因子=1.5>```
* ```fx 色调 <颜色="rgb(255,0,0)">```
* ```fx RGB分离 <偏移量=5>```
* ```fx 叠加颜色 <颜色列表=[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]> <叠加模式=overlay>```
* ```fx 像素抖动 <最大偏移量=2>```
### 几何变换滤镜
* ```fx 缩放 <比例=1.5>```
* ```fx 平移 <x偏移量=10> <y偏移量=10>```
* ```fx 缩放 <比例(X)=1.5> <比例Y=None>```
* ```fx 旋转 <角度=45>```
* ```fx 透视变换 <变换矩阵>```
* ```fx 裁剪 <左=0> <上=0> <右=100> <下=100>(百分比)```
* ```fx 拓展边缘 <拓展量=10>```
* ```fx 波纹 <振幅=5> <波长=20>```
* ```fx 光学补偿 <数量=100> <反转=false>```
* ```fx 球面化 <强度=0.5>```
* ```fx 镜像 <角度=90>```
* ```fx 水平翻转```
* ```fx 垂直翻转```
* ```fx 复制 <目标位置=(100,100)> <缩放=1.0> <源区域=(0,0,100,100)>(百分比)```
### 特殊效果滤镜
* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>```
* ```fx 晃动 <最大偏移量=5> <运动模糊=False>```
* ```fx 动图 <帧率=10>```
## 颜色名称支持
- **基本颜色**:红、绿、蓝、黄、紫、黑、白、橙、粉、灰、青、靛、棕

View File

@ -9,7 +9,7 @@ John: 11-28 16:50:37
谁来总结一下今天的工作?
Jack: 11-28 16:50:55
[引用John的消息] tqszm
[引用John的消息] @此方Bot tqszm
此方Bot: 11-28 16:50:56
slzjyxjtdgz
@ -18,7 +18,7 @@ slzjyxjtdgz
或者,你也可以直接以正常指令的方式调用:
```
提取首字母 中山大学
@此方Bot 提取首字母 中山大学
> zsdx
```

View File

@ -1,10 +1,12 @@
import asyncio as asynkio
from dataclasses import dataclass
from io import BytesIO
from inspect import signature
from konabot.common.nb.extract_image import DepPILImage
from konabot.common.nb.extract_image import DepImageBytes, DepPILImage
from nonebot.adapters import Event as BaseEvent
from nonebot import on_message
from nonebot import on_message, logger
from nonebot_plugin_alconna import (
UniMessage,
@ -13,46 +15,149 @@ from nonebot_plugin_alconna import (
from konabot.plugins.fx_process.fx_manager import ImageFilterManager
from PIL import Image, ImageSequence
@dataclass
class FilterItem:
name: str
filter: callable
args: list
def prase_input_args(input_str: str) -> list[FilterItem]:
# 按分号或换行符分割参数
args = []
for part in input_str.replace('\n', ';').split(';'):
part = part.strip()
if not part:
continue
split_part = part.split()
filter_name = split_part[0]
if not ImageFilterManager.has_filter(filter_name):
continue
filter_func = ImageFilterManager.get_filter(filter_name)
input_filter_args = split_part[1:]
# 获取函数最大参数数量
sig = signature(filter_func)
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
# 从 args 提取参数,并转换为适当类型
func_args = []
for i in range(0, min(len(input_filter_args), max_params)):
# 尝试将参数转换为函数签名中对应的类型
param = list(sig.parameters.values())[i + 1]
param_type = param.annotation
arg_value = input_filter_args[i]
try:
if param_type is float:
converted_value = float(arg_value)
elif param_type is int:
converted_value = int(arg_value)
else:
converted_value = arg_value
except Exception:
converted_value = arg_value
func_args.append(converted_value)
args.append(FilterItem(name=filter_name,filter=filter_func, args=func_args))
return args
def apply_filters_to_image(img: Image, filters: list[FilterItem]) -> Image:
for filter_item in filters:
filter_func = filter_item.filter
func_args = filter_item.args
img = filter_func(img, *func_args)
return img
async def apply_filters_to_bytes(image_bytes: bytes, filters: list[FilterItem]) -> BytesIO:
# 检测是否需要将静态图视作动图处理
frozen_to_move = any(
filter_item.name == "动图"
for filter_item in filters
)
static_fps = 10
# 找到动图参数 fps
if frozen_to_move:
for filter_item in filters:
if filter_item.name == "动图" and filter_item.args:
try:
static_fps = int(filter_item.args[0])
except Exception:
static_fps = 10
break
# 如果 image 是动图,则逐帧处理
img = Image.open(BytesIO(image_bytes))
logger.debug("开始图像处理")
output = BytesIO()
if getattr(img, "is_animated", False) or frozen_to_move:
frames = []
all_frames = []
if getattr(img, "is_animated", False):
logger.debug("处理动图帧")
for frame in ImageSequence.Iterator(img):
frame_copy = frame.copy()
all_frames.append(frame_copy)
else:
# 将静态图视作单帧动图处理,拷贝多份
logger.debug("处理静态图为多帧动图")
for _ in range(10): # 默认复制10帧
all_frames.append(img.copy())
img.info['duration'] = int(1000 / static_fps)
async def process_single_frame(frame: Image.Image, frame_idx: int) -> Image.Image:
"""处理单帧的异步函数"""
logger.debug(f"开始处理帧 {frame_idx}")
result = await asynkio.to_thread(apply_filters_to_image, frame, filters)
logger.debug(f"完成处理帧 {frame_idx}")
return result
# 并发处理所有帧
tasks = []
for i, frame in enumerate(all_frames):
task = process_single_frame(frame, i)
tasks.append(task)
frames = await asynkio.gather(*tasks, return_exceptions=True)
# 检查是否有处理失败的帧
for i, result in enumerate(frames):
if isinstance(result, Exception):
logger.error(f"{i} 处理失败: {result}")
# 使用原始帧作为回退
frames[i] = all_frames[i]
logger.debug("保存动图")
frames[0].save(
output,
format="GIF",
save_all=True,
append_images=frames[1:],
loop=img.info.get("loop", 0),
disposal=img.info.get("disposal", 2),
duration=img.info.get("duration", 100),
)
logger.debug("Animated image saved")
else:
img = apply_filters_to_image(img, filters)
img.save(output, format="PNG")
logger.debug("Image processing completed")
output.seek(0)
return output
def is_fx_mentioned(evt: BaseEvent, msg: UniMsg) -> bool:
txt = msg.extract_plain_text()
if "fx" not in txt[:3]:
if "fx" not in txt[:3].lower():
return False
return True
fx_on = on_message(rule=is_fx_mentioned)
@fx_on.handle()
async def _(msg: UniMsg, event: BaseEvent, img: DepPILImage):
async def _(msg: UniMsg, event: BaseEvent, img: DepImageBytes):
args = msg.extract_plain_text().split()
if len(args) < 2:
return
filter_name = args[1]
filter_func = ImageFilterManager.get_filter(filter_name)
if not filter_func:
filters = prase_input_args(msg.extract_plain_text()[2:])
if not filters:
return
# 获取函数最大参数数量
sig = signature(filter_func)
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
# 从 args 提取参数,并转换为适当类型
func_args = []
for i in range(2, min(len(args), max_params + 2)):
# 尝试将参数转换为函数签名中对应的类型
param = list(sig.parameters.values())[i - 1]
param_type = param.annotation
arg_value = args[i]
try:
if param_type is float:
converted_value = float(arg_value)
elif param_type is int:
converted_value = int(arg_value)
else:
converted_value = arg_value
except Exception:
converted_value = arg_value
func_args.append(converted_value)
# 应用滤镜
out_img = filter_func(img, *func_args)
output = BytesIO()
out_img.save(output, format="PNG")
output = await apply_filters_to_bytes(img, filters)
logger.debug("FX processing completed, sending result.")
await fx_on.send(await UniMessage().image(raw=output).export())

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
from typing import Optional
from konabot.plugins.fx_process.fx_handle import ImageFilterImplement
from konabot.plugins.fx_process.fx_handle import ImageFilterEmpty, ImageFilterImplement
class ImageFilterManager:
filter_map = {
@ -21,8 +21,40 @@ class ImageFilterManager:
"缩放": ImageFilterImplement.apply_resize,
"波纹": ImageFilterImplement.apply_wave,
"色键": ImageFilterImplement.apply_color_key,
"暗角": ImageFilterImplement.apply_vignette,
"发光": ImageFilterImplement.apply_glow,
"RGB分离": ImageFilterImplement.apply_rgb_split,
"光学补偿": ImageFilterImplement.apply_optical_compensation,
"球面化": ImageFilterImplement.apply_spherize,
"旋转": ImageFilterImplement.apply_rotate,
"透视变换": ImageFilterImplement.apply_perspective_transform,
"裁剪": ImageFilterImplement.apply_crop,
"噪点": ImageFilterImplement.apply_noise,
"平移": ImageFilterImplement.apply_translate,
"拓展边缘": ImageFilterImplement.apply_expand_edges,
"素描": ImageFilterImplement.apply_sketch,
"叠加颜色": ImageFilterImplement.apply_gradient_overlay,
"阴影": ImageFilterImplement.apply_shadow,
"径向模糊": ImageFilterImplement.apply_radial_blur,
"旋转模糊": ImageFilterImplement.apply_spin_blur,
"方向模糊": ImageFilterImplement.apply_directional_blur,
"边缘模糊": ImageFilterImplement.apply_focus_blur,
"缩放模糊": ImageFilterImplement.apply_zoom_blur,
"镜像": ImageFilterImplement.apply_mirror_half,
"水平翻转": ImageFilterImplement.apply_flip_horizontal,
"垂直翻转": ImageFilterImplement.apply_flip_vertical,
"复制": ImageFilterImplement.copy_area,
"晃动": ImageFilterImplement.apply_random_wiggle,
"动图": ImageFilterEmpty.empty_filter_param,
"像素抖动": ImageFilterImplement.apply_pixel_jitter,
}
@classmethod
def get_filter(cls, name: str) -> Optional[callable]:
return cls.filter_map.get(name)
return cls.filter_map.get(name)
@classmethod
def has_filter(cls, name: str) -> bool:
return name in cls.filter_map

View File

@ -0,0 +1,341 @@
import re
from konabot.plugins.fx_process.color_handle import ColorHandle
import numpy as np
from PIL import Image, ImageDraw
from typing import List, Tuple, Dict, Optional
class GradientGenerator:
"""渐变生成器类"""
def __init__(self):
self.has_numpy = hasattr(np, '__version__')
def parse_color_list(self, color_list_str: str) -> List[Dict]:
"""解析渐变颜色列表字符串
Args:
color_list_str: 格式如 '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]'
Returns:
list: 包含颜色和位置信息的字典列表
"""
color_nodes = []
color_list_str = color_list_str.strip('[] ').strip()
pattern = r'([^|]+)\|\(([^)]+)\)'
matches = re.findall(pattern, color_list_str)
for color_str, pos_str in matches:
color = ColorHandle.parse_color(color_str.strip())
try:
x_str, y_str = pos_str.split(',')
x_percent = float(x_str.strip().replace('%', ''))
y_percent = float(y_str.strip().replace('%', ''))
x_percent = max(0, min(100, x_percent))
y_percent = max(0, min(100, y_percent))
except:
x_percent = 0
y_percent = 0
color_nodes.append({
'color': color,
'position': (x_percent / 100.0, y_percent / 100.0)
})
if not color_nodes:
color_nodes = [
{'color': (255, 0, 0), 'position': (0, 0)},
{'color': (0, 0, 255), 'position': (1, 1)}
]
return color_nodes
def create_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
"""创建渐变图像
Args:
width: 图像宽度
height: 图像高度
color_nodes: 颜色节点列表
Returns:
Image.Image: 渐变图像
"""
if len(color_nodes) == 1:
return Image.new('RGB', (width, height), color_nodes[0]['color'])
elif len(color_nodes) == 2:
return self._create_linear_gradient(width, height, color_nodes)
else:
return self._create_radial_gradient(width, height, color_nodes)
def _create_linear_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
"""创建线性渐变"""
color1 = color_nodes[0]['color']
color2 = color_nodes[1]['color']
pos1 = color_nodes[0]['position']
pos2 = color_nodes[1]['position']
if self.has_numpy:
return self._create_linear_gradient_numpy(width, height, color1, color2, pos1, pos2)
else:
return self._create_linear_gradient_pil(width, height, color1, color2, pos1, pos2)
def _create_linear_gradient_numpy(self, width: int, height: int,
color1: Tuple, color2: Tuple,
pos1: Tuple, pos2: Tuple) -> Image.Image:
"""使用numpy创建线性渐变"""
# 创建坐标网格
x = np.linspace(0, 1, width)
y = np.linspace(0, 1, height)
xx, yy = np.meshgrid(x, y)
# 计算渐变方向
dx = pos2[0] - pos1[0]
dy = pos2[1] - pos1[1]
length_sq = dx * dx + dy * dy
if length_sq > 0:
# 计算投影参数
t = ((xx - pos1[0]) * dx + (yy - pos1[1]) * dy) / length_sq
t = np.clip(t, 0, 1)
else:
t = np.zeros_like(xx)
# 插值颜色
r = color1[0] + (color2[0] - color1[0]) * t
g = color1[1] + (color2[1] - color1[1]) * t
b = color1[2] + (color2[2] - color1[2]) * t
# 创建图像
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
return Image.fromarray(gradient_array)
def _create_linear_gradient_pil(self, width: int, height: int,
color1: Tuple, color2: Tuple,
pos1: Tuple, pos2: Tuple) -> Image.Image:
"""使用PIL创建线性渐变没有numpy时使用"""
gradient = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(gradient)
# 判断渐变方向
if abs(pos1[0] - pos2[0]) < 0.01: # 垂直渐变
y1 = int(pos1[1] * (height - 1))
y2 = int(pos2[1] * (height - 1))
if y2 < y1:
y1, y2 = y2, y1
color1, color2 = color2, color1
if y2 > y1:
for y in range(height):
if y <= y1:
fill_color = color1
elif y >= y2:
fill_color = color2
else:
ratio = (y - y1) / (y2 - y1)
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
fill_color = (r, g, b)
draw.line([(0, y), (width, y)], fill=fill_color)
else:
draw.rectangle([0, 0, width, height], fill=color1)
elif abs(pos1[1] - pos2[1]) < 0.01: # 水平渐变
x1 = int(pos1[0] * (width - 1))
x2 = int(pos2[0] * (width - 1))
if x2 < x1:
x1, x2 = x2, x1
color1, color2 = color2, color1
if x2 > x1:
for x in range(width):
if x <= x1:
fill_color = color1
elif x >= x2:
fill_color = color2
else:
ratio = (x - x1) / (x2 - x1)
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
fill_color = (r, g, b)
draw.line([(x, 0), (x, height)], fill=fill_color)
else:
draw.rectangle([0, 0, width, height], fill=color1)
else: # 对角渐变(简化处理为左上到右下)
for y in range(height):
for x in range(width):
distance = (x/width + y/height) / 2
r = int(color1[0] + (color2[0] - color1[0]) * distance)
g = int(color1[1] + (color2[1] - color1[1]) * distance)
b = int(color1[2] + (color2[2] - color1[2]) * distance)
draw.point((x, y), fill=(r, g, b))
return gradient
def _create_radial_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
"""创建径向渐变"""
if self.has_numpy and len(color_nodes) > 2:
return self._create_radial_gradient_numpy(width, height, color_nodes)
else:
return self._create_simple_gradient(width, height, color_nodes)
def _create_radial_gradient_numpy(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
"""使用numpy创建径向渐变多色"""
# 创建坐标网格
x = np.linspace(0, 1, width)
y = np.linspace(0, 1, height)
xx, yy = np.meshgrid(x, y)
# 提取颜色和位置
positions = np.array([node['position'] for node in color_nodes])
colors = np.array([node['color'] for node in color_nodes])
# 计算每个点到所有节点的距离
distances = np.sqrt((xx[:, :, np.newaxis] - positions[np.newaxis, np.newaxis, :, 0]) ** 2 +
(yy[:, :, np.newaxis] - positions[np.newaxis, np.newaxis, :, 1]) ** 2)
# 找到最近的两个节点
sorted_indices = np.argsort(distances, axis=2)
nearest_idx = sorted_indices[:, :, 0]
second_idx = sorted_indices[:, :, 1]
# 获取对应的颜色
nearest_colors = colors[nearest_idx]
second_colors = colors[second_idx]
# 获取距离并计算权重
nearest_dist = np.take_along_axis(distances, np.expand_dims(nearest_idx, axis=2), axis=2)[:, :, 0]
second_dist = np.take_along_axis(distances, np.expand_dims(second_idx, axis=2), axis=2)[:, :, 0]
total_dist = nearest_dist + second_dist
mask = total_dist > 0
weight1 = np.zeros_like(nearest_dist)
weight1[mask] = second_dist[mask] / total_dist[mask]
weight2 = 1 - weight1
# 插值颜色
r = nearest_colors[:, :, 0] * weight1 + second_colors[:, :, 0] * weight2
g = nearest_colors[:, :, 1] * weight1 + second_colors[:, :, 1] * weight2
b = nearest_colors[:, :, 2] * weight1 + second_colors[:, :, 2] * weight2
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
return Image.fromarray(gradient_array)
def _create_simple_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
"""创建简化渐变没有numpy或多色时使用"""
gradient = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(gradient)
if len(color_nodes) >= 2:
# 使用第一个和最后一个颜色创建简单渐变
color1 = color_nodes[0]['color']
color2 = color_nodes[-1]['color']
# 判断节点分布
x_positions = [node['position'][0] for node in color_nodes]
y_positions = [node['position'][1] for node in color_nodes]
if all(abs(x - x_positions[0]) < 0.01 for x in x_positions):
# 垂直渐变
for y in range(height):
ratio = y / (height - 1) if height > 1 else 0
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
draw.line([(0, y), (width, y)], fill=(r, g, b))
else:
# 水平渐变
for x in range(width):
ratio = x / (width - 1) if width > 1 else 0
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
draw.line([(x, 0), (x, height)], fill=(r, g, b))
else:
# 单色
draw.rectangle([0, 0, width, height], fill=color_nodes[0]['color'])
return gradient
def create_simple_gradient(self, width: int, height: int,
start_color: Tuple, end_color: Tuple,
direction: str = 'vertical') -> Image.Image:
"""创建简单双色渐变
Args:
width: 图像宽度
height: 图像高度
start_color: 起始颜色
end_color: 结束颜色
direction: 渐变方向 'vertical', 'horizontal', 'diagonal'
Returns:
Image.Image: 渐变图像
"""
if direction == 'vertical':
return self._create_vertical_gradient(width, height, start_color, end_color)
elif direction == 'horizontal':
return self._create_horizontal_gradient(width, height, start_color, end_color)
else: # diagonal
return self._create_diagonal_gradient(width, height, start_color, end_color)
def _create_vertical_gradient(self, width: int, height: int,
color1: Tuple, color2: Tuple) -> Image.Image:
"""创建垂直渐变"""
gradient = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(gradient)
for y in range(height):
ratio = y / (height - 1) if height > 1 else 0
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
draw.line([(0, y), (width, y)], fill=(r, g, b))
return gradient
def _create_horizontal_gradient(self, width: int, height: int,
color1: Tuple, color2: Tuple) -> Image.Image:
"""创建水平渐变"""
gradient = Image.new('RGB', (width, height))
draw = ImageDraw.Draw(gradient)
for x in range(width):
ratio = x / (width - 1) if width > 1 else 0
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
draw.line([(x, 0), (x, height)], fill=(r, g, b))
return gradient
def _create_diagonal_gradient(self, width: int, height: int,
color1: Tuple, color2: Tuple) -> Image.Image:
"""创建对角渐变"""
if self.has_numpy:
return self._create_diagonal_gradient_numpy(width, height, color1, color2)
else:
return self._create_horizontal_gradient(width, height, color1, color2) # 降级为水平渐变
def _create_diagonal_gradient_numpy(self, width: int, height: int,
color1: Tuple, color2: Tuple) -> Image.Image:
"""使用numpy创建对角渐变"""
x = np.linspace(0, 1, width)
y = np.linspace(0, 1, height)
xx, yy = np.meshgrid(x, y)
distance = (xx + yy) / 2.0
r = color1[0] + (color2[0] - color1[0]) * distance
g = color1[1] + (color2[1] - color1[1]) * distance
b = color1[2] + (color2[2] - color1[2]) * distance
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
return Image.fromarray(gradient_array)

View File

@ -6,24 +6,11 @@ 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"))
from konabot.common.nb.extract_image import DepImageBytes
# 保持不变

View File

@ -0,0 +1,26 @@
from loguru import logger
import nonebot
from nonebot.adapters import Event as BaseEvent
from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent
from nonebot_plugin_alconna import (
UniMessage,
UniMsg
)
from konabot.plugins.notice_ui.notice import NoticeUI
from nonebot_plugin_alconna import on_alconna, Alconna, Args
evt = on_alconna(Alconna(
"notice",
Args["title", str],
Args["message", str]
),
use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True
)
@evt.handle()
async def _(title: str, message: str, msg: UniMsg, event: BaseEvent):
logger.debug(f"Received notice command with title: {title}, message: {message}")
out = await NoticeUI.render_notice(title, message)
await evt.send(await UniMessage().image(raw=out).export())

View File

@ -0,0 +1,53 @@
from io import BytesIO
from PIL import Image
from konabot.common.web_render import konaweb
from konabot.common.web_render.core import WebRenderer
from playwright.async_api import Page
class NoticeUI:
@staticmethod
async def render_notice(title: str, message: str) -> bytes:
"""
渲染一个通知图片,包含标题和消息内容。
"""
async def page_function(page: Page):
# 直到 setMaskMode 函数加载完成
await page.wait_for_function("typeof setMaskMode === 'function'", timeout=1000)
await page.evaluate('setMaskMode(false)')
# 直到 setContent 函数加载完成
await page.wait_for_function("typeof setContent === 'function'", timeout=1000)
# 设置标题和消息内容
await page.evaluate(f'setContent("{title}", "{message}")')
async def mask_function(page: Page):
# 直到 setContent 函数加载完成
await page.wait_for_function("typeof setContent === 'function'", timeout=1000)
# 设置标题和消息内容
await page.evaluate(f'setContent("{title}", "{message}")')
# 直到 setMaskMode 函数加载完成
await page.wait_for_function("typeof setMaskMode === 'function'", timeout=1000)
await page.evaluate('setMaskMode(true)')
image_bytes = await WebRenderer.render_with_persistent_page(
"notice_renderer",
konaweb('notice'),
target='#main',
other_function=page_function,
)
mask_bytes = await WebRenderer.render_with_persistent_page(
"notice_renderer",
konaweb('notice'),
target='#main',
other_function=mask_function)
image = Image.open(BytesIO(image_bytes)).convert("RGBA")
mask = Image.open(BytesIO(mask_bytes)).convert("L")
# 应用遮罩
image.putalpha(mask)
output_buffer = BytesIO()
image.save(output_buffer, format="GIF", disposal=2, loop=0)
output_buffer.seek(0)
return output_buffer.getvalue()

View File

@ -6,6 +6,7 @@ from pathlib import Path
from typing import Any
import nanoid
from konabot.plugins.notice_ui.notice import NoticeUI
import nonebot
from loguru import logger
from nonebot import get_plugin_config, on_message
@ -107,6 +108,10 @@ async def _(task: LongTask):
await task.target.send_message(
UniMessage().text(f"代办提醒:{message}")
)
notice_bytes = NoticeUI.render_notice("代办提醒", message)
await task.target.send_message(
await UniMessage().image(raw=notice_bytes).export()
)
async with DATA_FILE_LOCK:
data = load_notify_config()
if (chan := data.notify_channels.get(task.target.target_id)) is not None: