Skip to content

Commit bcef83c

Browse files
committed
refactor: decouple and improve code architecture for better maintainability
1 parent 4b8ed3f commit bcef83c

File tree

7 files changed

+420
-300
lines changed

7 files changed

+420
-300
lines changed

nonebot_plugin_qqdetail/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ class RateLimitConfig(BaseModel):
5454
white_users: List[str] = Field(default_factory=list, description="用户白名单")
5555

5656

57+
class ImageStyleConfig(BaseModel):
58+
"""图片样式配置"""
59+
font_size: int = Field(default=35, description="字体大小")
60+
text_padding: int = Field(default=10, description="文本与边框的间距")
61+
avatar_size: Optional[int] = Field(default=None, description="头像大小(None 表示与文本高度一致)")
62+
border_thickness: int = Field(default=10, description="边框厚度")
63+
border_color: tuple[int, int, int] = Field(default=(38, 38, 38), description="边框颜色 RGB")
64+
corner_radius: int = Field(default=30, description="圆角大小")
65+
66+
5767
class Config(BaseModel):
5868
"""插件配置"""
5969
qqdetail_only_admin: bool = Field(default=False, description="仅管理员可用")
@@ -62,3 +72,4 @@ class Config(BaseModel):
6272
qqdetail_auto_box_config: AutoBoxConfig = Field(default_factory=AutoBoxConfig)
6373
qqdetail_rate_limit_config: RateLimitConfig = Field(default_factory=RateLimitConfig)
6474
qqdetail_display_config: DisplayConfig = Field(default_factory=DisplayConfig)
75+
qqdetail_image_style_config: ImageStyleConfig = Field(default_factory=ImageStyleConfig)

nonebot_plugin_qqdetail/draw.py

Lines changed: 64 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import io
22
import random
33
from pathlib import Path
4-
from typing import Optional
4+
from typing import Optional, Tuple
55

66
from nonebot import logger
77
from PIL import Image, ImageDraw
@@ -10,6 +10,8 @@
1010
from typing import Union
1111
from io import BytesIO
1212

13+
from .config import ImageStyleConfig
14+
1315
try:
1416
import emoji
1517
EMOJI_AVAILABLE = True
@@ -23,14 +25,6 @@
2325
FONT_PATH: Path = RESOURCE_DIR / "可爱字体.ttf"
2426
EMOJI_FONT_PATH: Path = RESOURCE_DIR / "NotoColorEmoji.ttf"
2527

26-
# 样式配置
27-
FONT_SIZE = 35 # 字体大小
28-
TEXT_PADDING = 10 # 文本与边框的间距
29-
AVATAR_SIZE = None # 头像大小(None 表示与文本高度一致)
30-
BORDER_THICKNESS = 10 # 边框厚度
31-
BORDER_COLOR = (38, 38, 38) # 边框颜色 #262626
32-
CORNER_RADIUS = 30 # 圆角大小
33-
3428
# 加载字体
3529
def load_font(font_path: Path, size: int) -> Union[ImageFont.ImageFont, FreeTypeFont]:
3630
"""加载字体,如果失败则使用默认字体"""
@@ -44,36 +38,42 @@ def load_font(font_path: Path, size: int) -> Union[ImageFont.ImageFont, FreeType
4438
RESOURCE_DIR.mkdir(parents=True, exist_ok=True)
4539

4640
# 加载主字体
47-
if FONT_PATH.exists():
48-
cute_font = load_font(FONT_PATH, FONT_SIZE)
49-
logger.debug(f"[QQDetail] 主字体加载: {FONT_PATH}")
50-
else:
51-
logger.warning(f"[QQDetail] 字体文件不存在: {FONT_PATH},使用默认字体")
52-
cute_font = ImageFont.load_default()
41+
def get_cute_font(size: int) -> Union[ImageFont.ImageFont, FreeTypeFont]:
42+
"""获取主字体"""
43+
if FONT_PATH.exists():
44+
return load_font(FONT_PATH, size)
45+
else:
46+
logger.warning(f"[QQDetail] 字体文件不存在: {FONT_PATH},使用默认字体")
47+
return ImageFont.load_default()
5348

5449
# 加载 Emoji 字体
55-
emoji_font = None
56-
if EMOJI_FONT_PATH.exists():
57-
emoji_font = load_font(EMOJI_FONT_PATH, FONT_SIZE)
58-
logger.debug(f"[QQDetail] Emoji 字体加载: {EMOJI_FONT_PATH}")
59-
else:
60-
logger.warning(f"[QQDetail] Emoji 字体文件不存在: {EMOJI_FONT_PATH}")
50+
def get_emoji_font(size: int) -> Optional[Union[ImageFont.ImageFont, FreeTypeFont]]:
51+
"""获取Emoji字体"""
52+
if EMOJI_FONT_PATH.exists():
53+
return load_font(EMOJI_FONT_PATH, size)
54+
else:
55+
logger.warning(f"[QQDetail] Emoji 字体文件不存在: {EMOJI_FONT_PATH}")
56+
return None
6157

6258

63-
def create_image(avatar: bytes, reply: list) -> bytes:
59+
def create_image(avatar: bytes, reply: list, style_config: ImageStyleConfig) -> bytes:
6460
"""创建用户资料图片"""
6561
reply_str = "\n".join(reply)
66-
62+
63+
# 获取字体
64+
cute_font = get_cute_font(style_config.font_size)
65+
emoji_font = get_emoji_font(style_config.font_size)
66+
6767
# 创建临时图片计算文本的宽高
6868
temp_img = Image.new("RGBA", (1, 1))
6969
temp_draw = ImageDraw.Draw(temp_img)
70-
70+
7171
# 将 Emoji 替换为中文字符以计算宽度
7272
if EMOJI_AVAILABLE:
7373
no_emoji_reply = "".join("一" if emoji.is_emoji(c) else c for c in reply_str) # type: ignore[union-attr]
7474
else:
7575
no_emoji_reply = reply_str
76-
76+
7777
# 计算每行文本的高度
7878
lines = no_emoji_reply.split("\n")
7979
line_heights = []
@@ -84,14 +84,14 @@ def create_image(avatar: bytes, reply: list) -> bytes:
8484
line_height = int(line_bbox[3] - line_bbox[1])
8585
line_heights.append(line_height)
8686
except:
87-
line_heights.append(FONT_SIZE + 5)
87+
line_heights.append(style_config.font_size + 5)
8888
else:
89-
line_heights.append(FONT_SIZE + 5)
90-
89+
line_heights.append(style_config.font_size + 5)
90+
9191
# 计算整体文本高度
9292
text_height = sum(line_heights) + (len(lines) - 1) * 5
93-
text_height += TEXT_PADDING # 底部额外边距
94-
93+
text_height += style_config.text_padding # 底部额外边距
94+
9595
# 计算最大文本宽度
9696
max_line_width = 0
9797
for line in lines:
@@ -101,40 +101,48 @@ def create_image(avatar: bytes, reply: list) -> bytes:
101101
line_width = int(line_bbox[2] - line_bbox[0])
102102
max_line_width = max(max_line_width, line_width + 15)
103103
except:
104-
max_line_width = max(max_line_width, len(line) * FONT_SIZE + 15)
105-
104+
max_line_width = max(max_line_width, len(line) * style_config.font_size + 15)
105+
106106
text_width = max_line_width
107-
img_height = text_height + 2 * TEXT_PADDING
108-
107+
img_height = text_height + 2 * style_config.text_padding
108+
109109
# 调整头像大小
110110
avatar_img = Image.open(BytesIO(avatar))
111-
avatar_size = AVATAR_SIZE if AVATAR_SIZE else text_height
111+
avatar_size = style_config.avatar_size if style_config.avatar_size else text_height
112112
avatar_img = avatar_img.resize((avatar_size, avatar_size))
113-
img_width = avatar_img.width + text_width + 2 * TEXT_PADDING
114-
113+
img_width = avatar_img.width + text_width + 2 * style_config.text_padding
114+
115115
# 创建主图
116116
img = Image.new("RGBA", (img_width, img_height), color=(255, 255, 255, 255))
117117
img.paste(avatar_img, (0, (img_height - avatar_size) // 2))
118-
118+
119119
# 绘制文本
120-
_draw_multi(img, reply_str, avatar_img.width + TEXT_PADDING, TEXT_PADDING)
121-
120+
_draw_multi(img, reply_str, avatar_img.width + style_config.text_padding, style_config.text_padding, cute_font, emoji_font, style_config)
121+
122122
# 添加边框
123123
border_img = Image.new(
124124
mode="RGBA",
125-
size=(img_width + BORDER_THICKNESS * 2, img_height + BORDER_THICKNESS * 2),
126-
color=BORDER_COLOR,
125+
size=(img_width + style_config.border_thickness * 2, img_height + style_config.border_thickness * 2),
126+
color=style_config.border_color,
127127
)
128-
border_img.paste(img, (BORDER_THICKNESS, BORDER_THICKNESS))
129-
128+
border_img.paste(img, (style_config.border_thickness, style_config.border_thickness))
129+
130130
# 转换为字节
131131
img_byte_arr = io.BytesIO()
132132
border_img.save(img_byte_arr, format="PNG")
133133
return img_byte_arr.getvalue()
134134

135135

136-
def _draw_multi(img: Image.Image, text: str, text_x: int = 10, text_y: int = 10):
136+
def _draw_multi(img: Image.Image, text: str, text_x: int = 10, text_y: int = 10, cute_font=None, emoji_font=None, style_config: Optional[ImageStyleConfig] = None):
137137
"""在图片上绘制多语言文本"""
138+
if cute_font is None:
139+
cute_font = get_cute_font(35) # 默认值
140+
if emoji_font is None:
141+
emoji_font = get_emoji_font(35)
142+
if style_config is None:
143+
from .config import ImageStyleConfig
144+
style_config = ImageStyleConfig()
145+
138146
lines = text.split("\n")
139147
current_y = text_y
140148
draw = ImageDraw.Draw(img)
@@ -147,10 +155,10 @@ def _draw_multi(img: Image.Image, text: str, text_x: int = 10, text_y: int = 10)
147155
random.randint(240, 255),
148156
)
149157
current_x = text_x
150-
158+
151159
# 跳过空行
152160
if not line.strip():
153-
current_y += FONT_SIZE + 5
161+
current_y += style_config.font_size + 5
154162
continue
155163

156164
# 计算当前行的实际高度
@@ -161,7 +169,7 @@ def _draw_multi(img: Image.Image, text: str, text_x: int = 10, text_y: int = 10)
161169

162170
try:
163171
bbox = cute_font.getbbox(no_emoji_line)
164-
line_height = int(bbox[3] - bbox[1]) if bbox else FONT_SIZE
172+
line_height = int(bbox[3] - bbox[1]) if bbox else style_config.font_size
165173

166174
for char in line:
167175
# 判断是否为 Emoji
@@ -175,12 +183,12 @@ def _draw_multi(img: Image.Image, text: str, text_x: int = 10, text_y: int = 10)
175183
char_bbox = cute_font.getbbox(char)
176184

177185
# 计算字符宽度
178-
char_width = char_bbox[2] - char_bbox[0] if char_bbox else FONT_SIZE // 2
179-
186+
char_width = char_bbox[2] - char_bbox[0] if char_bbox else style_config.font_size // 2
187+
180188
# 检查是否超出边界
181-
if current_x + char_width > img.width - TEXT_PADDING:
189+
if current_x + char_width > img.width - style_config.text_padding:
182190
current_x = text_x
183-
current_y += max(line_height, FONT_SIZE) + 5
191+
current_y += max(line_height, style_config.font_size) + 5
184192

185193
# 重新绘制当前字符
186194
if is_emoji_char and emoji_font:
@@ -191,13 +199,13 @@ def _draw_multi(img: Image.Image, text: str, text_x: int = 10, text_y: int = 10)
191199
current_x += char_width
192200

193201
# 移动到下一行
194-
next_y = current_y + max(line_height, FONT_SIZE) + 5
195-
if next_y > img.height - TEXT_PADDING:
196-
current_y = img.height - TEXT_PADDING - max(line_height, FONT_SIZE)
202+
next_y = current_y + max(line_height, style_config.font_size) + 5
203+
if next_y > img.height - style_config.text_padding:
204+
current_y = img.height - style_config.text_padding - max(line_height, style_config.font_size)
197205
else:
198206
current_y = next_y
199207
except Exception as e:
200208
logger.error(f"[QQDetail] 绘制文本时出错: {e}")
201-
current_y += FONT_SIZE + 5
209+
current_y += style_config.font_size + 5
202210

203211
return img

0 commit comments

Comments
 (0)