- 增加了通过外部 HTTP API 向 QQ 群组发送消息的核心功能。 - 实现了对长文本消息的图片渲染,以避免被认定为垃圾信息。 - 支持在消息中提及特定的 QQ 用户。 - 创建了用于 API 令牌和图片渲染设置的配置选项。 - 开发了一个测试脚本以验证 API 功能。 - 对现有代码进行了重构,以提高组织性和可维护性。
208 lines
6.3 KiB
Python
208 lines
6.3 KiB
Python
"""图片生成模块 - 将文本渲染为图片"""
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import io
|
|
import base64
|
|
from typing import Tuple
|
|
|
|
|
|
class ImageRenderer:
|
|
"""图片渲染器"""
|
|
|
|
def __init__(
|
|
self,
|
|
width: int = 800,
|
|
font_size: int = 24,
|
|
padding: int = 30,
|
|
line_spacing: float = 1.4,
|
|
bg_color: Tuple[int, int, int] = (252, 252, 252),
|
|
text_color: Tuple[int, int, int] = (0, 0, 0),
|
|
font_paths: list = None
|
|
):
|
|
"""
|
|
初始化图片渲染器
|
|
|
|
Args:
|
|
width: 图片宽度
|
|
font_size: 字体大小
|
|
padding: 内边距
|
|
line_spacing: 行距倍数
|
|
bg_color: 背景颜色 (R, G, B)
|
|
text_color: 文本颜色 (R, G, B)
|
|
font_paths: 字体文件路径列表
|
|
"""
|
|
self.width = width
|
|
self.font_size = font_size
|
|
self.padding = padding
|
|
self.line_spacing = line_spacing
|
|
self.bg_color = bg_color
|
|
self.text_color = text_color
|
|
self.font_paths = font_paths or []
|
|
|
|
# 加载字体
|
|
self.font = self._load_font()
|
|
|
|
def _load_font(self) -> ImageFont.FreeTypeFont:
|
|
"""加载字体"""
|
|
return self._load_font_by_size(self.font_size)
|
|
|
|
def _load_font_by_size(self, font_size: int) -> ImageFont.FreeTypeFont:
|
|
"""
|
|
加载指定大小的字体
|
|
|
|
Args:
|
|
font_size: 字体大小
|
|
|
|
Returns:
|
|
字体对象
|
|
"""
|
|
for font_path in self.font_paths:
|
|
try:
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
return font
|
|
except Exception:
|
|
continue
|
|
|
|
# 如果都失败了,使用默认字体
|
|
return ImageFont.load_default()
|
|
|
|
def _calculate_text_dimensions(self, text: str) -> Tuple[int, int]:
|
|
"""
|
|
计算文本的尺寸
|
|
|
|
Args:
|
|
text: 文本内容
|
|
|
|
Returns:
|
|
(宽度, 高度)
|
|
"""
|
|
# 创建一个临时图片用于计算
|
|
test_img = Image.new('RGB', (1, 1), color=self.bg_color)
|
|
test_draw = ImageDraw.Draw(test_img)
|
|
|
|
bbox = test_draw.textbbox((0, 0), text, font=self.font)
|
|
width = bbox[2] - bbox[0]
|
|
height = bbox[3] - bbox[1]
|
|
|
|
return width, height
|
|
|
|
def _smart_text_wrap(self, text: str, max_width: int) -> list:
|
|
"""
|
|
智能文本换行
|
|
|
|
Args:
|
|
text: 文本内容
|
|
max_width: 最大宽度
|
|
|
|
Returns:
|
|
换行后的文本列表
|
|
"""
|
|
lines = []
|
|
current_line = ""
|
|
current_width = 0
|
|
|
|
for char in text:
|
|
char_width, _ = self._calculate_text_dimensions(char)
|
|
|
|
if current_width + char_width <= max_width:
|
|
current_line += char
|
|
current_width += char_width
|
|
else:
|
|
if current_line:
|
|
lines.append(current_line)
|
|
current_line = char
|
|
current_width = char_width
|
|
|
|
if current_line:
|
|
lines.append(current_line)
|
|
|
|
return lines
|
|
|
|
def render(self, text: str) -> bytes:
|
|
"""
|
|
将文本渲染为图片
|
|
|
|
Args:
|
|
text: 文本内容(已处理换行符)
|
|
|
|
Returns:
|
|
图片的字节数据
|
|
"""
|
|
if not text:
|
|
text = "(空消息)"
|
|
|
|
# 计算有效宽度
|
|
effective_width = self.width - (2 * self.padding)
|
|
|
|
# 按段落处理
|
|
all_lines = []
|
|
for paragraph in text.split('\n'):
|
|
if not paragraph:
|
|
all_lines.append("")
|
|
continue
|
|
|
|
wrapped_lines = self._smart_text_wrap(paragraph, effective_width)
|
|
all_lines.extend(wrapped_lines)
|
|
|
|
# 计算行高
|
|
_, line_height = self._calculate_text_dimensions("测试")
|
|
total_line_height = int(line_height * self.line_spacing)
|
|
|
|
# 标题相关
|
|
title = "蛋定助手通知您:"
|
|
title_font_size = int(self.font_size * 1.3) # 标题字体放大1.3倍
|
|
title_font = self._load_font_by_size(title_font_size)
|
|
title_height = int(line_height * 1.5) # 标题区域高度
|
|
divider_height = 2 # 分割线高度
|
|
divider_spacing = 10 # 分割线与标题的间距
|
|
|
|
# 计算总高度(包含标题区域)
|
|
content_height = (len(all_lines) * total_line_height) + (2 * self.padding)
|
|
header_height = title_height + divider_spacing + divider_height + self.padding
|
|
total_height = content_height + header_height
|
|
height = max(total_height, 200) # 最小高度
|
|
|
|
# 创建图片
|
|
image = Image.new('RGB', (self.width, height), color=self.bg_color)
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
# 绘制标题(加粗效果通过增大字体实现)
|
|
title_x = self.padding
|
|
title_y = self.padding
|
|
draw.text((title_x, title_y), title, font=title_font, fill=self.text_color)
|
|
|
|
# 绘制分割线
|
|
divider_y = title_y + title_height + 10
|
|
draw.line(
|
|
[(self.padding, divider_y), (self.width - self.padding, divider_y)],
|
|
fill=(200, 200, 200),
|
|
width=divider_height
|
|
)
|
|
|
|
# 绘制文本内容
|
|
y = divider_y + divider_spacing + self.padding
|
|
for line in all_lines:
|
|
if line: # 跳过空行
|
|
draw.text((self.padding, y), line, font=self.font, fill=self.text_color)
|
|
y += total_line_height
|
|
|
|
# 转换为字节流
|
|
img_byte_arr = io.BytesIO()
|
|
image.save(img_byte_arr, format='PNG', quality=95)
|
|
img_byte_arr.seek(0)
|
|
|
|
return img_byte_arr.getvalue()
|
|
|
|
def render_to_base64(self, text: str) -> str:
|
|
"""
|
|
将文本渲染为图片并返回 base64 编码
|
|
|
|
Args:
|
|
text: 文本内容
|
|
|
|
Returns:
|
|
base64 编码的图片数据
|
|
"""
|
|
img_bytes = self.render(text)
|
|
base64_str = base64.b64encode(img_bytes).decode('utf-8')
|
|
return f"base64://{base64_str}"
|