From e9659bf901fcdf748f759e33671b60585b5f9f43 Mon Sep 17 00:00:00 2001 From: fllesser Date: Tue, 10 Feb 2026 23:04:50 +0800 Subject: [PATCH 1/3] feat: add default avatar instead of placeholder --- src/nonebot_plugin_parser/renders/common.py | 93 +++++++++--------- .../renders/resources/avatar.png | Bin 0 -> 6767 bytes 2 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 src/nonebot_plugin_parser/renders/resources/avatar.png diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index 2eaf4ffa..770ffb85 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -127,6 +127,7 @@ class CommonRenderer(ImageRenderer): RESOURCES_DIR: ClassVar[Path] = Path(__file__).parent / "resources" DEFAULT_FONT_PATH: ClassVar[Path] = RESOURCES_DIR / "HYSongYunLangHeiW.ttf" DEFAULT_VIDEO_BUTTON_PATH: ClassVar[Path] = RESOURCES_DIR / "play.png" + DEFAULT_AVATAR_PATH: ClassVar[Path] = RESOURCES_DIR / "avatar.png" EMOJI_SOURCE: ClassVar[EmojiCDNSource] = EmojiCDNSource( base_url=pconfig.emoji_cdn, style=pconfig.emoji_style, @@ -138,8 +139,8 @@ class CommonRenderer(ImageRenderer): def load_resources(cls): """加载资源""" cls._load_fonts() - cls._load_video_button() cls._load_platform_logos() + cls._load_other_resources() @classmethod def _load_fonts(cls): @@ -147,14 +148,6 @@ def _load_fonts(cls): cls.fontset = FontSet.new(font_path) logger.success(f"加载字体「{font_path.name}」成功") - @classmethod - def _load_video_button(cls): - with Image.open(cls.DEFAULT_VIDEO_BUTTON_PATH) as img: - cls.video_button_image: PILImage = img.convert("RGBA").resize((100, 100)) - alpha = cls.video_button_image.split()[-1] - alpha = alpha.point(lambda x: int(x * 0.5)) - cls.video_button_image.putalpha(alpha) - @classmethod def _load_platform_logos(cls): from ..constants import PlatformEnum @@ -165,6 +158,22 @@ def _load_platform_logos(cls): if logo_path.exists(): with Image.open(logo_path) as img: cls.platform_logos[str(platform_name)] = img.convert("RGBA") + logger.debug(f"加载 logo「{platform_name}」成功") + + @classmethod + def _load_other_resources(cls): + # avatar + with Image.open(cls.DEFAULT_AVATAR_PATH) as img: + cls.avatar_image: PILImage = img.convert("RGBA").resize((cls.AVATAR_SIZE, cls.AVATAR_SIZE)) + logger.debug(f"加载头像「{cls.DEFAULT_AVATAR_PATH.name}」成功") + + # video button + with Image.open(cls.DEFAULT_VIDEO_BUTTON_PATH) as img: + cls.video_button_image: PILImage = img.convert("RGBA").resize((100, 100)) + alpha = cls.video_button_image.split()[-1] + alpha = alpha.point(lambda x: int(x * 0.5)) + cls.video_button_image.putalpha(alpha) + logger.debug(f"加载视频按钮「{cls.DEFAULT_VIDEO_BUTTON_PATH.name}」成功") @override async def render_image(self, result: ParseResult) -> bytes: @@ -317,15 +326,18 @@ async def _render_header(self, ctx: RenderContext) -> None: def _load_avatar(self, avatar_path: Path | None) -> PILImage: """加载头像(带圆形裁剪)""" - if avatar_path and avatar_path.exists(): - try: - with Image.open(avatar_path) as img: - avatar = img.convert("RGBA") - avatar = avatar.resize((self.AVATAR_SIZE, self.AVATAR_SIZE), Image.Resampling.LANCZOS) - except Exception: - avatar = self._create_avatar_placeholder() - else: - avatar = self._create_avatar_placeholder() + if avatar_path is None or not avatar_path.exists(): + return self.avatar_image + + try: + with Image.open(avatar_path) as img: + avatar = img.convert("RGBA") + avatar = avatar.resize( + (self.AVATAR_SIZE, self.AVATAR_SIZE), + Image.Resampling.LANCZOS, + ) + except Exception: + return self.avatar_image # 圆形遮罩 mask = Image.new("L", (self.AVATAR_SIZE, self.AVATAR_SIZE), 0) @@ -333,25 +345,6 @@ def _load_avatar(self, avatar_path: Path | None) -> PILImage: avatar.putalpha(mask) return avatar - def _create_avatar_placeholder(self) -> PILImage: - """创建默认头像""" - size = self.AVATAR_SIZE - placeholder = Image.new("RGBA", (size, size), (230, 230, 230, 255)) - draw = ImageDraw.Draw(placeholder) - - # 简单的人形图标 - center = size // 2 - head_r = size // 6 - draw.ellipse( - (center - head_r, size * 35 // 100 - head_r, center + head_r, size * 35 // 100 + head_r), - fill=(200, 200, 200, 255), - ) - draw.ellipse( - (center - size * 27 // 100, size * 55 // 100, center + size * 27 // 100, size * 115 // 100), - fill=(200, 200, 200, 255), - ) - return placeholder - async def _render_title(self, ctx: RenderContext) -> None: """渲染标题""" if not ctx.result.title: @@ -361,13 +354,11 @@ async def _render_title(self, ctx: RenderContext) -> None: ctx.y_pos += await self._draw_text_lines(ctx, lines, self.fontset.title) ctx.y_pos += self.SECTION_SPACING - async def _render_cover_or_images(self, ctx: RenderContext) -> None: + async def _render_cover_or_images(self, ctx: RenderContext): """渲染封面或图片网格""" - cover_path = await ctx.result.cover_path - if cover_path and cover_path.exists(): - cover = self._load_cover(cover_path, ctx.content_width) - if cover: + if (cover_path := await ctx.result.cover_path) and cover_path.exists(): + if cover := self._load_cover(cover_path, ctx.content_width): x_pos = self.PADDING ctx.image.paste(cover, (x_pos, ctx.y_pos)) # 视频按钮 @@ -456,7 +447,14 @@ async def _render_image_grid(self, ctx: RenderContext) -> None: # +N 指示器 if has_more and row == rows - 1 and i == len(row_imgs) - 1: remaining = total - self.MAX_IMAGES_DISPLAY - self._draw_more_indicator(ctx.image, img_x, current_y + spacing, img.width, img.height, remaining) + self._draw_more_indicator( + ctx.image, + img_x, + current_y + spacing, + img.width, + img.height, + remaining, + ) current_y += spacing + max_h @@ -602,7 +600,14 @@ async def _draw_text_lines(self, ctx: RenderContext, lines: list[str], font: Fon xy = (self.PADDING, ctx.y_pos) if emosvg is not None: - emosvg.text(ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height) + emosvg.text( + ctx.image, + xy, + lines, + font.font, + fill=font.fill, + line_height=font.line_height, + ) else: await Apilmoji.text( ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height, source=self.EMOJI_SOURCE diff --git a/src/nonebot_plugin_parser/renders/resources/avatar.png b/src/nonebot_plugin_parser/renders/resources/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..7387667bb15d5647b0d21b42d065c622e92eae3e GIT binary patch literal 6767 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 Date: Tue, 10 Feb 2026 23:26:44 +0800 Subject: [PATCH 2/3] tweak --- src/nonebot_plugin_parser/renders/common.py | 28 ++++++++------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index 770ffb85..23f362b0 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -351,7 +351,7 @@ async def _render_title(self, ctx: RenderContext) -> None: return lines = self._wrap_text(ctx.result.title, ctx.content_width, self.fontset.title) - ctx.y_pos += await self._draw_text_lines(ctx, lines, self.fontset.title) + ctx.y_pos += await self._draw_text(ctx, lines, self.fontset.title) ctx.y_pos += self.SECTION_SPACING async def _render_cover_or_images(self, ctx: RenderContext): @@ -369,12 +369,12 @@ async def _render_cover_or_images(self, ctx: RenderContext): ctx.y_pos += cover.height + self.SECTION_SPACING return - # 尝试图片网格 + # 图片网格 if ctx.result.img_contents: await self._render_image_grid(ctx) return - # 尝试图文内容 + # 图文内容 if ctx.result.graphics_contents: for gc in ctx.result.graphics_contents: await self._render_graphics(ctx, gc) @@ -526,7 +526,7 @@ async def _render_graphics(self, ctx: RenderContext, gc: GraphicsContent) -> Non # 文本 if gc.text: lines = self._wrap_text(gc.text, ctx.content_width, self.fontset.text) - ctx.y_pos += await self._draw_text_lines(ctx, lines, self.fontset.text) + ctx.y_pos += await self._draw_text(ctx, lines, self.fontset.text) ctx.y_pos += self.SECTION_SPACING # 图片 @@ -552,7 +552,7 @@ async def _render_text(self, ctx: RenderContext) -> None: return lines = self._wrap_text(ctx.result.text, ctx.content_width, self.fontset.text) - ctx.y_pos += await self._draw_text_lines(ctx, lines, self.fontset.text) + ctx.y_pos += await self._draw_text(ctx, lines, self.fontset.text) ctx.y_pos += self.SECTION_SPACING async def _render_extra(self, ctx: RenderContext) -> None: @@ -561,7 +561,7 @@ async def _render_extra(self, ctx: RenderContext) -> None: return lines = self._wrap_text(ctx.result.extra_info, ctx.content_width, self.fontset.extra) - ctx.y_pos += await self._draw_text_lines(ctx, lines, self.fontset.extra) + ctx.y_pos += await self._draw_text(ctx, lines, self.fontset.extra) async def _render_repost(self, ctx: RenderContext) -> None: """渲染转发内容""" @@ -593,21 +593,14 @@ async def _render_repost(self, ctx: RenderContext) -> None: ctx.y_pos += container_h + self.SECTION_SPACING - async def _draw_text_lines(self, ctx: RenderContext, lines: list[str], font: FontInfo) -> int: + async def _draw_text(self, ctx: RenderContext, lines: list[str], font: FontInfo) -> int: """绘制多行文本""" if not lines: return 0 xy = (self.PADDING, ctx.y_pos) if emosvg is not None: - emosvg.text( - ctx.image, - xy, - lines, - font.font, - fill=font.fill, - line_height=font.line_height, - ) + emosvg.text(ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height) else: await Apilmoji.text( ctx.image, xy, lines, font.font, fill=font.fill, line_height=font.line_height, source=self.EMOJI_SOURCE @@ -619,7 +612,8 @@ def _wrap_text(self, text: str, max_width: int, font: FontInfo) -> list[str]: if not text: return [] - def is_punctuation(c: str) -> bool: + # 行尾标点符号 + def is_trailing_punctuation(c: str) -> bool: return c in ",。!?;:、)】》〉」』〕〗〙〛…—·,.;:!?)]}" lines: list[str] = [] @@ -652,7 +646,7 @@ def is_punctuation(c: str) -> bool: current_width = char_width continue - if len(char) == 1 and is_punctuation(char): + if len(char) == 1 and is_trailing_punctuation(char): current_line += char current_width += char_width continue From c64b41ecbe899787315ce5c4d2182e7ec784214b Mon Sep 17 00:00:00 2001 From: fllesser Date: Tue, 10 Feb 2026 23:31:18 +0800 Subject: [PATCH 3/3] tweak --- src/nonebot_plugin_parser/renders/common.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nonebot_plugin_parser/renders/common.py b/src/nonebot_plugin_parser/renders/common.py index 23f362b0..9b1e31ec 100644 --- a/src/nonebot_plugin_parser/renders/common.py +++ b/src/nonebot_plugin_parser/renders/common.py @@ -126,8 +126,8 @@ class CommonRenderer(ImageRenderer): # 资源路径 RESOURCES_DIR: ClassVar[Path] = Path(__file__).parent / "resources" DEFAULT_FONT_PATH: ClassVar[Path] = RESOURCES_DIR / "HYSongYunLangHeiW.ttf" - DEFAULT_VIDEO_BUTTON_PATH: ClassVar[Path] = RESOURCES_DIR / "play.png" DEFAULT_AVATAR_PATH: ClassVar[Path] = RESOURCES_DIR / "avatar.png" + DEFAULT_VIDEO_BUTTON_PATH: ClassVar[Path] = RESOURCES_DIR / "play.png" EMOJI_SOURCE: ClassVar[EmojiCDNSource] = EmojiCDNSource( base_url=pconfig.emoji_cdn, style=pconfig.emoji_style, @@ -215,7 +215,7 @@ async def _create_card_image(self, result: ParseResult, not_repost: bool = True) logger.debug(f"估算高度: {estimated_height}, 画布高度: {ctx.image.height}, 最终高度: {final_height}") return ctx.image.crop((0, 0, card_width, final_height)) - def _ensure_canvas_height(self, ctx: RenderContext, needed_height: int) -> None: + def _ensure_height_enough(self, ctx: RenderContext, needed_height: int) -> None: """确保画布有足够高度,不够则扩展""" if ctx.y_pos + needed_height + self.PADDING > ctx.image.height: # 扩展画布(每次扩展 1.6 倍或至少满足需求) @@ -521,7 +521,7 @@ async def _render_graphics(self, ctx: RenderContext, gc: GraphicsContent) -> Non text_height = len(lines) * self.fontset.text.line_height + self.SECTION_SPACING alt_height = self.fontset.extra.line_height + self.SECTION_SPACING if gc.alt else 0 needed = text_height + img.height + alt_height + self.SECTION_SPACING - self._ensure_canvas_height(ctx, needed) + self._ensure_height_enough(ctx, needed) # 文本 if gc.text: