nonebot-plugin-parser 2.0.4__tar.gz → 2.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/PKG-INFO +2 -1
  2. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/README.md +1 -0
  3. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/pyproject.toml +2 -2
  4. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/config.py +7 -0
  5. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/base.py +2 -3
  6. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/data.py +30 -10
  7. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/youtube.py +2 -2
  8. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/__init__.py +2 -3
  9. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/base.py +5 -0
  10. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/common.py +39 -43
  11. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/__init__.py +0 -0
  12. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/constants.py +0 -0
  13. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/download/__init__.py +0 -0
  14. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/download/task.py +0 -0
  15. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
  16. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/exception.py +0 -0
  17. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/helper.py +0 -0
  18. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/matchers/__init__.py +0 -0
  19. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
  20. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/matchers/preprocess.py +0 -0
  21. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
  22. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/acfun.py +0 -0
  23. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +0 -0
  24. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
  25. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
  26. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
  27. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -0
  28. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
  29. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
  30. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/kuaishou.py +0 -0
  31. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
  32. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
  33. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/twitter.py +0 -0
  34. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/weibo.py +0 -0
  35. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +0 -0
  36. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/default.py +0 -0
  37. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/fonts/HYSongYunLangHeiW-1.ttf +0 -0
  38. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
  39. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/weibo.py +0 -0
  40. {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nonebot-plugin-parser
3
- Version: 2.0.4
3
+ Version: 2.0.5
4
4
  Summary: NoneBot2 链接分享解析器自动解析, BV号/链接/小程序/卡片 | B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun
5
5
  Keywords: nonebot,nonebot2,video,bilibili,youtube,tiktok,twitter,kuaishou,acfun,weibo,xiaohongshu,nga,douyin
6
6
  Author: fllesser
@@ -153,6 +153,7 @@ Windows 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
153
153
  | parser_disabled_platforms | 否 | [] | 全局禁止的解析,示例 parser_disabled_platforms=["bilibili", "douyin"] 表示禁止了哔哩哔哩和抖, 请根据自己需求填写["bilibili", "douyin", "kuaishou", "twitter", "youtube", "acfun", "tiktok", "weibo", "xiaohongshu"] |
154
154
  | parser_render_type | 否 | "common" | 渲染器类型,可选 "default"(无图片渲染), "common"(PIL 通用图片渲染), "htmlkit"(htmlkit) |
155
155
  | parser_append_url | 否 | False | 是否在解析结果中附加原始URL |
156
+ | parser_custom_font | 否 | None | 自定义字体,如未指定则使用内置字体,需将字体文件放置于localstore 生成的插件 data 目录下,parser_custom_font 为字体文件名 |
156
157
 
157
158
  ## 🎉 使用
158
159
  ### 指令表
@@ -123,6 +123,7 @@ Windows 参考(原项目推荐): https://www.jianshu.com/p/5015a477de3c
123
123
  | parser_disabled_platforms | 否 | [] | 全局禁止的解析,示例 parser_disabled_platforms=["bilibili", "douyin"] 表示禁止了哔哩哔哩和抖, 请根据自己需求填写["bilibili", "douyin", "kuaishou", "twitter", "youtube", "acfun", "tiktok", "weibo", "xiaohongshu"] |
124
124
  | parser_render_type | 否 | "common" | 渲染器类型,可选 "default"(无图片渲染), "common"(PIL 通用图片渲染), "htmlkit"(htmlkit) |
125
125
  | parser_append_url | 否 | False | 是否在解析结果中附加原始URL |
126
+ | parser_custom_font | 否 | None | 自定义字体,如未指定则使用内置字体,需将字体文件放置于localstore 生成的插件 data 目录下,parser_custom_font 为字体文件名 |
126
127
 
127
128
  ## 🎉 使用
128
129
  ### 指令表
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-parser"
3
- version = "2.0.4"
3
+ version = "2.0.5"
4
4
  description = "NoneBot2 链接分享解析器自动解析, BV号/链接/小程序/卡片 | B站/抖音/快手/微博/小红书/youtube/tiktok/twitter/acfun"
5
5
  authors = [{ "name" = "fllesser", "email" = "fllessive@gmail.com" }]
6
6
  readme = "README.md"
@@ -185,7 +185,7 @@ build-backend = "uv_build"
185
185
 
186
186
 
187
187
  [tool.bumpversion]
188
- current_version = "2.0.4"
188
+ current_version = "2.0.5"
189
189
  commit = true
190
190
  message = "🔖 release: bump vesion from {current_version} to {new_version}"
191
191
  tag = true
@@ -51,6 +51,8 @@ class Config(BaseModel):
51
51
  """B站视频编码"""
52
52
  parser_render_type: RenderType = RenderType.common
53
53
  """Renderer 类型"""
54
+ parser_custom_font: str | None = None
55
+ """自定义字体"""
54
56
 
55
57
  @property
56
58
  def nickname(self) -> str:
@@ -127,6 +129,11 @@ class Config(BaseModel):
127
129
  """是否在解析结果中附加原始URL"""
128
130
  return self.parser_append_url
129
131
 
132
+ @property
133
+ def custom_font(self) -> Path | None:
134
+ """自定义字体"""
135
+ return (self.data_dir / self.parser_custom_font) if self.parser_custom_font else None
136
+
130
137
 
131
138
  pconfig: Config = get_plugin_config(Config)
132
139
  """配置"""
@@ -2,7 +2,6 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from asyncio import Task
5
- from collections.abc import Sequence
6
5
  from pathlib import Path
7
6
  import re
8
7
  from typing import ClassVar
@@ -116,7 +115,7 @@ class BaseParser(ABC):
116
115
  video_task = url_or_task
117
116
  return VideoContent(video_task, cover_task, duration)
118
117
 
119
- def create_image_contents(self, image_urls: Sequence[str]):
118
+ def create_image_contents(self, image_urls: list[str]):
120
119
  """创建图片内容列表"""
121
120
  from ..download import DOWNLOADER
122
121
  from .data import ImageContent
@@ -124,7 +123,7 @@ class BaseParser(ABC):
124
123
  img_tasks = [DOWNLOADER.download_img(url, ext_headers=self.headers) for url in image_urls]
125
124
  return [ImageContent(task) for task in img_tasks]
126
125
 
127
- def create_dynamic_contents(self, dynamic_urls: Sequence[str]):
126
+ def create_dynamic_contents(self, dynamic_urls: list[str]):
128
127
  """创建动态内容列表"""
129
128
  from ..download import DOWNLOADER
130
129
  from .data import DynamicContent
@@ -5,7 +5,14 @@ from pathlib import Path
5
5
  from typing import Any
6
6
 
7
7
 
8
- @dataclass
8
+ def repr_path_task(path_task: Path | Task[Path]) -> str:
9
+ if isinstance(path_task, Path):
10
+ return f"path={path_task}"
11
+ else:
12
+ return f"task={path_task.get_name()}, done={path_task.done()}"
13
+
14
+
15
+ @dataclass(repr=False)
9
16
  class MediaContent:
10
17
  path_task: Path | Task[Path]
11
18
 
@@ -15,15 +22,19 @@ class MediaContent:
15
22
  self.path_task = await self.path_task
16
23
  return self.path_task
17
24
 
25
+ def __repr__(self) -> str:
26
+ prefix = self.__class__.__name__
27
+ return f"{prefix}({repr_path_task(self.path_task)})"
18
28
 
19
- @dataclass
29
+
30
+ @dataclass(repr=False)
20
31
  class AudioContent(MediaContent):
21
32
  """音频内容"""
22
33
 
23
34
  duration: float = 0.0
24
35
 
25
36
 
26
- @dataclass
37
+ @dataclass(repr=False)
27
38
  class VideoContent(MediaContent):
28
39
  """视频内容"""
29
40
 
@@ -47,21 +58,21 @@ class VideoContent(MediaContent):
47
58
  return f"时长: {minutes}:{seconds:02d}"
48
59
 
49
60
 
50
- @dataclass
61
+ @dataclass(repr=False)
51
62
  class ImageContent(MediaContent):
52
63
  """图片内容"""
53
64
 
54
65
  pass
55
66
 
56
67
 
57
- @dataclass
68
+ @dataclass(repr=False)
58
69
  class DynamicContent(MediaContent):
59
70
  """动态内容 视频格式 后续转 gif"""
60
71
 
61
72
  gif_path: Path | None = None
62
73
 
63
74
 
64
- @dataclass
75
+ @dataclass(repr=False)
65
76
  class GraphicsContent(MediaContent):
66
77
  """图文内容"""
67
78
 
@@ -78,7 +89,7 @@ class Platform:
78
89
  """ 平台显示名称 """
79
90
 
80
91
 
81
- @dataclass
92
+ @dataclass(repr=False)
82
93
  class Author:
83
94
  """作者信息"""
84
95
 
@@ -97,6 +108,14 @@ class Author:
97
108
  self.avatar = await self.avatar
98
109
  return self.avatar
99
110
 
111
+ def __repr__(self) -> str:
112
+ repr = f"Author(name={self.name}"
113
+ if self.avatar:
114
+ repr += f", avatar_{repr_path_task(self.avatar)}"
115
+ if self.description:
116
+ repr += f", description={self.description}"
117
+ return repr + ")"
118
+
100
119
 
101
120
  @dataclass(repr=False)
102
121
  class ParseResult:
@@ -178,12 +197,13 @@ class ParseResult:
178
197
 
179
198
  def __repr__(self) -> str:
180
199
  return (
181
- f"\ntitle: {self.title}\n"
182
- f"platform: {self.platform}\n"
200
+ f"\nplatform: {self.platform}\n"
201
+ f"title: {self.title}\n"
202
+ f"timestamp: {self.timestamp}\n"
183
203
  f"author: {self.author}\n"
204
+ f"text: {self.text}\n"
184
205
  f"contents: {self.contents}\n"
185
206
  f"url: {self.url}\n"
186
- f"timestamp: {self.timestamp}\n"
187
207
  f"extra: {self.extra}\n"
188
208
  f"repost: {self.repost}\n"
189
209
  )
@@ -50,7 +50,7 @@ class YouTubeParser(BaseParser):
50
50
  video = YTDLP_DOWNLOADER.download_video(url, self.cookies_file)
51
51
  contents.append(self.create_video_content(video, video_info.thumbnail, video_info.duration))
52
52
  else:
53
- contents.append(self.create_image_contents(video_info.thumbnail))
53
+ contents.extend(self.create_image_contents([video_info.thumbnail]))
54
54
 
55
55
  return self.result(
56
56
  title=video_info.title,
@@ -73,7 +73,7 @@ class YouTubeParser(BaseParser):
73
73
  author = await self._fetch_author_info(video_info.channel_id)
74
74
 
75
75
  contents = []
76
- contents.append(self.create_image_contents(video_info.thumbnail))
76
+ contents.extend(self.create_image_contents([video_info.thumbnail]))
77
77
 
78
78
  if video_info.duration <= pconfig.duration_maximum:
79
79
  audio_task = YTDLP_DOWNLOADER.download_audio(url, self.cookies_file)
@@ -38,6 +38,5 @@ from nonebot import get_driver
38
38
 
39
39
 
40
40
  @get_driver().on_startup
41
- async def _():
42
- # CommonRenderer.load_fonts()
43
- CommonRenderer.load_custom_fonts()
41
+ async def load_font():
42
+ _COMMON_RENDERER.load_font(pconfig.custom_font)
@@ -4,6 +4,7 @@ from itertools import chain
4
4
  from pathlib import Path
5
5
  from typing import Any, ClassVar
6
6
 
7
+ from ..config import pconfig
7
8
  from ..exception import DownloadException, DownloadLimitException, ZeroSizeException
8
9
  from ..helper import ForwardNodeInner, UniHelper, UniMessage
9
10
  from ..parsers import ParseResult
@@ -74,3 +75,7 @@ class BaseRenderer(ABC):
74
75
  message = f"{failed_count} 项媒体下载失败"
75
76
  yield UniMessage(message)
76
77
  raise DownloadException(message)
78
+
79
+ @property
80
+ def append_url(self) -> bool:
81
+ return pconfig.append_url
@@ -3,15 +3,17 @@ from pathlib import Path
3
3
  from typing import Any, ClassVar
4
4
  from typing_extensions import override
5
5
 
6
+ from nonebot import logger
6
7
  from PIL import Image, ImageDraw, ImageFont
7
8
 
8
- from ..config import pconfig
9
9
  from .base import BaseRenderer, ParseResult, UniHelper, UniMessage
10
10
 
11
11
 
12
12
  class CommonRenderer(BaseRenderer):
13
13
  """统一的渲染器,将解析结果转换为消息"""
14
14
 
15
+ __slots__ = ("font_path", "fonts")
16
+
15
17
  # 卡片配置常量
16
18
  PADDING = 25
17
19
  AVATAR_SIZE = 80
@@ -70,15 +72,25 @@ class CommonRenderer(BaseRenderer):
70
72
  FONT_SIZES: ClassVar[dict[str, int]] = {"name": 28, "title": 30, "text": 24, "extra": 24}
71
73
  LINE_HEIGHTS: ClassVar[dict[str, int]] = {"name": 32, "title": 36, "text": 28, "extra": 28}
72
74
 
73
- # 预加载的字体(在类定义后立即加载)
74
- FONTS: ClassVar[dict[str, ImageFont.FreeTypeFont | ImageFont.ImageFont]]
75
+ DEFAULT_FONT_PATH: ClassVar[Path] = Path(__file__).parent / "fonts" / "HYSongYunLangHeiW-1.ttf"
76
+
77
+ def __init__(self, font_path: Path | None = None):
78
+ self.font_path: Path = self.DEFAULT_FONT_PATH
79
+
80
+ def load_font(self, font_path: Path | None = None):
81
+ if font_path is not None and font_path.exists():
82
+ self.font_path = font_path
83
+ self.fonts: dict[str, ImageFont.FreeTypeFont | ImageFont.ImageFont] = {
84
+ name: ImageFont.truetype(self.font_path, size) for name, size in self.FONT_SIZES.items()
85
+ }
86
+ logger.success(f"加载字体「{self.font_path.name}」成功")
75
87
 
76
88
  @override
77
89
  async def render_messages(self, result: ParseResult):
78
90
  # 生成图片卡片
79
91
  if image_raw := await self.draw_common_image(result):
80
92
  msg = UniMessage(UniHelper.img_seg(raw=image_raw))
81
- if pconfig.append_url:
93
+ if self.append_url:
82
94
  urls = (result.display_url, result.repost_display_url)
83
95
  msg += "\n".join(url for url in urls if url)
84
96
  yield msg
@@ -120,7 +132,6 @@ class CommonRenderer(BaseRenderer):
120
132
  PIL Image 对象,如果没有足够的内容则返回 None
121
133
  """
122
134
  # 使用预加载的字体
123
- fonts = self.FONTS
124
135
 
125
136
  # 先确定固定的卡片宽度和内容区域宽度
126
137
  card_width = max(self.DEFAULT_CARD_WIDTH, self.MIN_CARD_WIDTH)
@@ -132,14 +143,14 @@ class CommonRenderer(BaseRenderer):
132
143
  )
133
144
 
134
145
  # 计算各部分内容的高度
135
- heights = await self._calculate_sections(result, cover_img, content_width, fonts)
146
+ heights = await self._calculate_sections(result, cover_img, content_width)
136
147
 
137
148
  # 计算总高度
138
149
  card_height = sum(h for _, h, _ in heights) + self.PADDING * 2 + self.SECTION_SPACING * (len(heights) - 1)
139
150
  # 创建画布并绘制(使用指定的背景颜色,或默认背景颜色)
140
151
  background_color = bg_color if bg_color is not None else self.BG_COLOR
141
152
  image = Image.new("RGB", (card_width, card_height), background_color)
142
- self._draw_sections(image, heights, card_width, fonts)
153
+ self._draw_sections(image, heights, card_width)
143
154
 
144
155
  return image
145
156
 
@@ -234,20 +245,20 @@ class CommonRenderer(BaseRenderer):
234
245
  return None
235
246
 
236
247
  async def _calculate_sections(
237
- self, result: ParseResult, cover_img: Image.Image | None, content_width: int, fonts: dict
248
+ self, result: ParseResult, cover_img: Image.Image | None, content_width: int
238
249
  ) -> list[tuple[str, int, Any]]:
239
250
  """计算各部分内容的高度和数据"""
240
251
  heights = []
241
252
 
242
253
  # 1. Header 部分
243
254
  if result.author:
244
- header_data = await self._calculate_header_section(result, content_width, fonts)
255
+ header_data = await self._calculate_header_section(result, content_width)
245
256
  if header_data:
246
257
  heights.append(("header", header_data["height"], header_data))
247
258
 
248
259
  # 2. 标题部分
249
260
  if result.title:
250
- title_lines = self._wrap_text(result.title, content_width, fonts["title"])
261
+ title_lines = self._wrap_text(result.title, content_width, self.fonts["title"])
251
262
  title_height = len(title_lines) * self.LINE_HEIGHTS["title"]
252
263
  heights.append(("title", title_height, title_lines))
253
264
 
@@ -262,25 +273,25 @@ class CommonRenderer(BaseRenderer):
262
273
 
263
274
  # 4. 文本内容
264
275
  if result.text:
265
- text_lines = self._wrap_text(result.text, content_width, fonts["text"])
276
+ text_lines = self._wrap_text(result.text, content_width, self.fonts["text"])
266
277
  text_height = len(text_lines) * self.LINE_HEIGHTS["text"]
267
278
  heights.append(("text", text_height, text_lines))
268
279
 
269
280
  # 5. 额外信息
270
281
  if result.extra_info:
271
- extra_lines = self._wrap_text(result.extra_info, content_width, fonts["extra"])
282
+ extra_lines = self._wrap_text(result.extra_info, content_width, self.fonts["extra"])
272
283
  extra_height = len(extra_lines) * self.LINE_HEIGHTS["extra"]
273
284
  heights.append(("extra", extra_height, extra_lines))
274
285
 
275
286
  # 6. 转发内容
276
287
  if result.repost:
277
- repost_data = await self._calculate_repost_section(result.repost, content_width, fonts)
288
+ repost_data = await self._calculate_repost_section(result.repost, content_width)
278
289
  if repost_data:
279
290
  heights.append(("repost", repost_data["height"], repost_data))
280
291
 
281
292
  return heights
282
293
 
283
- async def _calculate_header_section(self, result: ParseResult, content_width: int, fonts: dict) -> dict | None:
294
+ async def _calculate_header_section(self, result: ParseResult, content_width: int) -> dict | None:
284
295
  """计算 header 部分的高度和内容"""
285
296
  if not result.author:
286
297
  return None
@@ -292,11 +303,11 @@ class CommonRenderer(BaseRenderer):
292
303
  text_area_width = content_width - (self.AVATAR_SIZE + self.AVATAR_TEXT_GAP)
293
304
 
294
305
  # 发布者名称
295
- name_lines = self._wrap_text(result.author.name, text_area_width, fonts["name"])
306
+ name_lines = self._wrap_text(result.author.name, text_area_width, self.fonts["name"])
296
307
 
297
308
  # 时间
298
309
  time_text = result.formartted_datetime
299
- time_lines = self._wrap_text(time_text, text_area_width, fonts["extra"]) if time_text else []
310
+ time_lines = self._wrap_text(time_text, text_area_width, self.fonts["extra"]) if time_text else []
300
311
 
301
312
  # 计算 header 高度(取头像和文字中较大者)
302
313
  text_height = len(name_lines) * self.LINE_HEIGHTS["name"]
@@ -312,7 +323,7 @@ class CommonRenderer(BaseRenderer):
312
323
  "text_height": text_height,
313
324
  }
314
325
 
315
- async def _calculate_repost_section(self, repost: ParseResult, content_width: int, fonts: dict) -> dict | None:
326
+ async def _calculate_repost_section(self, repost: ParseResult, content_width: int) -> dict | None:
316
327
  """计算转发内容的高度和内容(递归调用绘制方法)"""
317
328
  if not repost:
318
329
  return None
@@ -435,26 +446,24 @@ class CommonRenderer(BaseRenderer):
435
446
  bottom = top + width
436
447
  return img.crop((0, top, width, bottom))
437
448
 
438
- def _draw_sections(
439
- self, image: Image.Image, heights: list[tuple[str, int, Any]], card_width: int, fonts: dict
440
- ) -> None:
449
+ def _draw_sections(self, image: Image.Image, heights: list[tuple[str, int, Any]], card_width: int) -> None:
441
450
  """绘制所有内容到画布上"""
442
451
  draw = ImageDraw.Draw(image)
443
452
  y_pos = self.PADDING
444
453
 
445
454
  for section_type, height, content in heights:
446
455
  if section_type == "header":
447
- y_pos = self._draw_header(image, draw, content, y_pos, fonts)
456
+ y_pos = self._draw_header(image, draw, content, y_pos)
448
457
  elif section_type == "title":
449
- y_pos = self._draw_title(draw, content, y_pos, fonts["title"])
458
+ y_pos = self._draw_title(draw, content, y_pos, self.fonts["title"])
450
459
  elif section_type == "cover":
451
460
  y_pos = self._draw_cover(image, content, y_pos, card_width)
452
461
  elif section_type == "text":
453
- y_pos = self._draw_text(draw, content, y_pos, fonts["text"])
462
+ y_pos = self._draw_text(draw, content, y_pos, self.fonts["text"])
454
463
  elif section_type == "extra":
455
- y_pos = self._draw_extra(draw, content, y_pos, fonts["extra"])
464
+ y_pos = self._draw_extra(draw, content, y_pos, self.fonts["extra"])
456
465
  elif section_type == "repost":
457
- y_pos = self._draw_repost(image, draw, content, y_pos, card_width, fonts)
466
+ y_pos = self._draw_repost(image, draw, content, y_pos, card_width)
458
467
  elif section_type == "image_grid":
459
468
  y_pos = self._draw_image_grid(image, content, y_pos, card_width)
460
469
 
@@ -505,9 +514,7 @@ class CommonRenderer(BaseRenderer):
505
514
  placeholder.putalpha(mask)
506
515
  return placeholder
507
516
 
508
- def _draw_header(
509
- self, image: Image.Image, draw: ImageDraw.ImageDraw, content: dict, y_pos: int, fonts: dict
510
- ) -> int:
517
+ def _draw_header(self, image: Image.Image, draw: ImageDraw.ImageDraw, content: dict, y_pos: int) -> int:
511
518
  """绘制 header 部分"""
512
519
  x_pos = self.PADDING
513
520
 
@@ -525,14 +532,14 @@ class CommonRenderer(BaseRenderer):
525
532
 
526
533
  # 发布者名称(蓝色)
527
534
  for line in content["name_lines"]:
528
- draw.text((text_x, text_y), line, fill=self.HEADER_COLOR, font=fonts["name"])
535
+ draw.text((text_x, text_y), line, fill=self.HEADER_COLOR, font=self.fonts["name"])
529
536
  text_y += self.LINE_HEIGHTS["name"]
530
537
 
531
538
  # 时间(灰色)
532
539
  if content["time_lines"]:
533
540
  text_y += self.NAME_TIME_GAP
534
541
  for line in content["time_lines"]:
535
- draw.text((text_x, text_y), line, fill=self.EXTRA_COLOR, font=fonts["extra"])
542
+ draw.text((text_x, text_y), line, fill=self.EXTRA_COLOR, font=self.fonts["extra"])
536
543
  text_y += self.LINE_HEIGHTS["extra"]
537
544
 
538
545
  return y_pos + content["height"] + self.SECTION_SPACING
@@ -602,7 +609,7 @@ class CommonRenderer(BaseRenderer):
602
609
  return y_pos
603
610
 
604
611
  def _draw_repost(
605
- self, image: Image.Image, draw: ImageDraw.ImageDraw, content: dict, y_pos: int, card_width: int, fonts: dict
612
+ self, image: Image.Image, draw: ImageDraw.ImageDraw, content: dict, y_pos: int, card_width: int
606
613
  ) -> int:
607
614
  """绘制转发内容"""
608
615
  # 获取缩放后的转发图片
@@ -713,11 +720,7 @@ class CommonRenderer(BaseRenderer):
713
720
  text = f"+{count}"
714
721
  # 使用更大的字体
715
722
  font_size = min(img_width, img_height) // 4
716
- try:
717
- font_path = Path(__file__).parent / "fonts" / "HYSongYunLangHeiW-1.ttf"
718
- font = ImageFont.truetype(font_path, font_size)
719
- except Exception:
720
- font = ImageFont.load_default()
723
+ font = ImageFont.truetype(self.font_path, font_size)
721
724
 
722
725
  # 计算文字位置(居中)
723
726
  bbox = font.getbbox(text)
@@ -815,10 +818,3 @@ class CommonRenderer(BaseRenderer):
815
818
  lines.append(current_line)
816
819
 
817
820
  return lines if lines else [""]
818
-
819
- @classmethod
820
- def load_custom_fonts(cls):
821
- """加载字体"""
822
- font_path = Path(__file__).parent / "fonts" / "HYSongYunLangHeiW-1.ttf"
823
- # 加载字体
824
- cls.FONTS = {name: ImageFont.truetype(font_path, size) for name, size in cls.FONT_SIZES.items()}