From 5efbabb8793f71908b02b0e7c81eff8242d2f58a Mon Sep 17 00:00:00 2001 From: liuwei Date: Mon, 20 Apr 2026 15:47:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8A=96=E9=9F=B3=E5=9B=BE?= =?UTF-8?q?=E6=96=87=E5=8F=91=E9=80=81=EF=BC=9A=E6=A0=87=E9=A2=98=E4=B8=8E?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=90=88=E5=B9=B6=E4=B8=BA=E5=8D=95=E6=AC=A1?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更项:\n1. 图文类型取消先发文本再发图片的流程。\n2. 新增标题渲染逻辑,将文案绘制到合并图第一页顶部后统一发送图片。\n3. 新增中文字体加载与按像素宽度自动换行能力,避免标题超宽截断。\n4. 渲染失败时回退原图,保证发送链路稳定。 --- plugins/douyin_parser/main.py | 99 ++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/plugins/douyin_parser/main.py b/plugins/douyin_parser/main.py index 2034978..fac1aab 100644 --- a/plugins/douyin_parser/main.py +++ b/plugins/douyin_parser/main.py @@ -9,7 +9,7 @@ from urllib.parse import urlparse from loguru import logger from pathlib import Path -from PIL import Image +from PIL import Image, ImageDraw, ImageFont from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.plugin_interface import PluginStatus @@ -158,8 +158,10 @@ class DouyinParserPlugin(MessagePluginInterface): if not merged_pages: return False, "图片合并失败" title = media_info.get('title') or "" + # 按你的需求,图文类型不再单独发送一条文本消息。 + # 这里把文案直接绘制到合并后第一页的顶部,让“文字 + 图片”作为同一条图片消息的一部分发送。 if len(title) > 0: - await self.bot.send_text_message((roomid if roomid else sender), title) + merged_pages[0] = self._append_title_to_image(merged_pages[0], title) for page in merged_pages: await self.bot.send_image_message((roomid if roomid else sender), page) return True, f"发送合并图片成功({len(merged_pages)}页)" @@ -483,3 +485,96 @@ class DouyinParserPlugin(MessagePluginInterface): return outputs if outputs else None except Exception: return None + + def _append_title_to_image(self, image_bytes: bytes, title: str) -> bytes: + """ + 将标题绘制到图片顶部,返回新的图片二进制数据。 + + 设计说明: + 1) 微信接口没有“单条消息同时携带纯文本+图片”的通用发送 API; + 2) 为了满足“图文合并发送”,这里把标题渲染为图片顶部文字区域; + 3) 渲染失败时直接回退原图,避免影响主流程可用性。 + """ + if not title: + return image_bytes + try: + source = Image.open(io.BytesIO(image_bytes)) + if source.mode in ("RGBA", "P"): + source = source.convert("RGB") + + width, height = source.size + # 文字区域留出左右/上下内边距,保证可读性。 + pad_x = 36 + pad_y = 26 + font = self._load_chinese_font(44) + wrapped_lines = self._wrap_text_for_image(title.strip(), font, max(100, width - pad_x * 2)) + if not wrapped_lines: + return image_bytes + + # 行高按字体大小动态计算,并增加少量行间距。 + line_height = max(44, int(font.size * 1.4)) + text_block_height = pad_y * 2 + line_height * len(wrapped_lines) + + # 新建画布:上方白底承载文案,下方保留原图内容。 + canvas = Image.new("RGB", (width, height + text_block_height), (255, 255, 255)) + canvas.paste(source, (0, text_block_height)) + + draw = ImageDraw.Draw(canvas) + y = pad_y + for line in wrapped_lines: + draw.text((pad_x, y), line, font=font, fill=(34, 34, 34)) + y += line_height + + output = io.BytesIO() + canvas.save(output, format="JPEG", quality=88) + return output.getvalue() + except Exception as e: + self.LOG.warning(f"标题绘制失败,回退原图: {e}") + return image_bytes + + def _load_chinese_font(self, size: int) -> ImageFont.FreeTypeFont: + """ + 尝试加载常见中文字体,保证标题在不同系统尽量可读。 + 如果都不可用,则回退到 Pillow 默认字体(可能不支持完整中文)。 + """ + font_candidates = [ + "C:/Windows/Fonts/msyh.ttc", + "C:/Windows/Fonts/simhei.ttf", + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", + "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", + "/System/Library/Fonts/PingFang.ttc", + ] + for font_path in font_candidates: + if os.path.exists(font_path): + try: + return ImageFont.truetype(font_path, size=size) + except Exception: + continue + return ImageFont.load_default() + + def _wrap_text_for_image(self, text: str, font: ImageFont.ImageFont, max_width: int) -> List[str]: + """ + 按像素宽度将文本自动换行,避免标题超宽被截断。 + + 实现策略: + - 逐字追加,超过最大宽度就换行; + - 保留原有换行语义(按行分段后再逐字处理)。 + """ + draw = ImageDraw.Draw(Image.new("RGB", (10, 10))) + lines: List[str] = [] + for para in text.splitlines(): + if not para: + lines.append("") + continue + current = "" + for ch in para: + test = current + ch + text_width = int(draw.textlength(test, font=font)) + if current and text_width > max_width: + lines.append(current) + current = ch + else: + current = test + if current: + lines.append(current) + return lines