Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 274ca0fa9a | |||
| c72cdd6a6b | |||
|
16b0451133
|
|||
|
cb34813c4b
|
|||
| 2de3be271e | |||
| f7d2168dac | |||
|
26e10be4ec
|
@ -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>```
|
||||
|
||||
## 颜色名称支持
|
||||
- **基本颜色**:红、绿、蓝、黄、紫、黑、白、橙、粉、灰、青、靛、棕
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
|
||||
341
konabot/plugins/fx_process/gradient.py
Normal file
341
konabot/plugins/fx_process/gradient.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
|
||||
# 保持不变
|
||||
|
||||
26
konabot/plugins/notice_ui/__init__.py
Normal file
26
konabot/plugins/notice_ui/__init__.py
Normal 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())
|
||||
53
konabot/plugins/notice_ui/notice.py
Normal file
53
konabot/plugins/notice_ui/notice.py
Normal 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()
|
||||
@ -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:
|
||||
|
||||
Reference in New Issue
Block a user