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.
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/PKG-INFO +2 -1
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/README.md +1 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/pyproject.toml +2 -2
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/config.py +7 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/base.py +2 -3
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/data.py +30 -10
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/youtube.py +2 -2
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/__init__.py +2 -3
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/base.py +5 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/common.py +39 -43
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/__init__.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/constants.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/download/__init__.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/download/task.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/download/ytdlp.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/exception.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/helper.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/matchers/__init__.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/matchers/filter.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/matchers/preprocess.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/__init__.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/acfun.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/bilibili/__init__.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/bilibili/opus.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/bilibili/video.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/cookie.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/douyin/__init__.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/douyin/slides.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/douyin/video.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/kuaishou.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/nga.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/tiktok.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/twitter.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/weibo.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/xiaohongshu.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/default.py +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/fonts/HYSongYunLangHeiW-1.ttf +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/templates/weibo.html.jinja +0 -0
- {nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/renders/weibo.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
{nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/config.py
RENAMED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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"\
|
|
182
|
-
f"
|
|
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.
|
|
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.
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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()}
|
{nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/__init__.py
RENAMED
|
File without changes
|
{nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/exception.py
RENAMED
|
File without changes
|
{nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/parsers/nga.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_parser-2.0.4 → nonebot_plugin_parser-2.0.5}/src/nonebot_plugin_parser/utils.py
RENAMED
|
File without changes
|