From 4f89cbf1500b82907838604c7a19b7ac98598ca8 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Sun, 27 Jul 2025 16:24:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E6=9C=AA=E6=8C=87=E5=AE=9A=E5=AD=97=E4=BD=93=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vibrant_template.cpython-312.pyc | Bin 24057 -> 26017 bytes poster/templates/vibrant_template.py | 161 ++++++++++++++---- 2 files changed, 130 insertions(+), 31 deletions(-) diff --git a/poster/templates/__pycache__/vibrant_template.cpython-312.pyc b/poster/templates/__pycache__/vibrant_template.cpython-312.pyc index 9071ead1c81d3fa4d3d35dd1669ad57a10d6ce9d..2d6aab3e725fcc43d2bd4e531831b7640f513e20 100644 GIT binary patch delta 8510 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 delta 6421 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 diff --git a/poster/templates/vibrant_template.py b/poster/templates/vibrant_template.py index 06af8e3..ba4c550 100644 --- a/poster/templates/vibrant_template.py +++ b/poster/templates/vibrant_template.py @@ -10,7 +10,7 @@ import math from typing import Dict, Any, Optional, Tuple import numpy as np -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw, ImageFont, ImageFilter from .base_template import BaseTemplate from ..utils import ColorExtractor @@ -206,7 +206,7 @@ class VibrantTemplate(BaseTemplate): color_tuple = tuple(int(c) for c in color) + (alpha,) draw.line([(0, y), (self.width, y)], fill=color_tuple) - return self.image_processor.apply_blur(overlay, radius=enhanced_blur) + return overlay.filter(ImageFilter.GaussianBlur(radius=enhanced_blur)) def _extract_glass_colors_from_image(self, image: Image.Image, gradient_start: int) -> tuple: """从图片中提取用于毛玻璃背景的颜色""" @@ -231,6 +231,84 @@ class VibrantTemplate(BaseTemplate): return top_color, bottom_color + def _calculate_optimal_font_size_enhanced(self, text: str, target_width: int, + max_size: int = 120, min_size: int = 10) -> Tuple[int, int]: + """ + 计算文本的最佳字体大小,使其宽度接近目标宽度(增强版本,与demo一致) + + 返回: + (字体大小, 实际文本宽度) + """ + # 二分查找最佳字体大小 + low = min_size + high = max_size + best_size = min_size + best_width = 0 + tolerance = 0.08 # 容差值,使文本宽度更接近目标值 + + # 首先尝试最大字体大小 + try: + font = self.text_renderer._load_default_font(max_size) + max_width, _ = self.text_renderer.get_text_size(text, font) + except: + max_width = target_width * 2 # 如果出错,设置一个大值 + + # 如果最大字体大小下的宽度仍小于目标宽度的108%,直接使用最大字体 + if max_width < target_width * (1 + tolerance): + best_size = max_size + best_width = max_width + else: + # 记录最接近目标宽度的字体大小 + closest_size = min_size + closest_diff = target_width + + while low <= high: + mid = (low + high) // 2 + try: + font = self.text_renderer._load_default_font(mid) + width, _ = self.text_renderer.get_text_size(text, font) + except: + width = target_width * 2 # 如果出错,设置一个大值 + + # 计算与目标宽度的差距 + diff = abs(width - target_width) + + # 更新最接近的字体大小 + if diff < closest_diff: + closest_diff = diff + closest_size = mid + + # 如果宽度在目标宽度的允许范围内,认为找到了最佳匹配 + if target_width * (1 - tolerance) <= width <= target_width * (1 + tolerance): + best_size = mid + best_width = width + break + + # 如果当前宽度小于目标宽度,尝试更大的字体 + if width < target_width: + if width > best_width: + best_width = width + best_size = mid + low = mid + 1 + else: + # 如果当前宽度大于目标宽度,尝试更小的字体 + high = mid - 1 + + # 如果没有找到在容差范围内的字体大小,使用最接近的字体大小 + if best_width == 0: + best_size = closest_size + + # 确保返回的宽度是使用最终字体计算的实际宽度 + try: + best_font = self.text_renderer._load_default_font(best_size) + final_width, _ = self.text_renderer.get_text_size(text, best_font) + except: + final_width = best_width + + logger.info(f"文本'{text[:min(10, len(text))]}...'的最佳字体大小: {best_size},目标宽度: {target_width},实际宽度: {final_width},差距: {abs(final_width-target_width)}") + + return best_size, final_width + def _render_texts(self, canvas: Image.Image, content: Dict[str, Any], gradient_start: int) -> Image.Image: """渲染所有文本元素,采用双栏布局""" draw = ImageDraw.Draw(canvas) @@ -256,27 +334,41 @@ class VibrantTemplate(BaseTemplate): return canvas def _calculate_content_margins(self, content: Dict[str, Any], width: int, center_x: int) -> Tuple[int, int]: - """计算内容区域的左右边距""" + """计算内容区域的左右边距(增强版本,与demo一致)""" + # 计算标题位置 title_text = content.get("title", "") - title_size=self.text_renderer.calculate_optimal_font_size(title_text,int(width * 0.95),max_size=130) - title_width,title_height=self.text_renderer.get_text_size(title_text,self.text_renderer._load_default_font(title_size)) + title_target_width = int(width * 0.95) + title_size, title_width = self._calculate_optimal_font_size_enhanced( + title_text, title_target_width, min_size=40, max_size=130 + ) title_x = center_x - title_width // 2 - + + # 计算副标题位置 slogan_text = content.get("slogan", "") - subtitle_size=self.text_renderer.calculate_optimal_font_size(slogan_text,int(width * 0.9),max_size=50) - subtitle_width,subtitle_height=self.text_renderer.get_text_size(slogan_text,self.text_renderer._load_default_font(subtitle_size)) + subtitle_target_width = int(width * 0.9) + subtitle_size, subtitle_width = self._calculate_optimal_font_size_enhanced( + slogan_text, subtitle_target_width, max_size=50, min_size=20 + ) subtitle_x = center_x - subtitle_width // 2 - - padding = 20 - left_margin = max(40, min(title_x, subtitle_x) - padding) - right_margin = min(width - 40, max(title_x + title_width, subtitle_x + subtitle_width) + padding) - - if (right_margin - left_margin) < (min_width := int(width * 0.75)): - extra = min_width - (right_margin - left_margin) - left_margin = max(30, left_margin - extra // 2) - right_margin = min(width - 30, right_margin + extra // 2) - - return left_margin, right_margin + + # 计算内容区域边距 - 减小额外的边距,让内容区域更宽 + padding = 20 # 从30减小到20 + content_left_margin = min(title_x, subtitle_x) - padding + content_right_margin = max(title_x + title_width, subtitle_x + subtitle_width) + padding + + # 确保边距不超出合理范围,但允许更宽的内容区域 + content_left_margin = max(40, content_left_margin) + content_right_margin = min(width - 40, content_right_margin) + + # 如果内容区域太窄,强制使用更宽的区域 + min_content_width = int(width * 0.75) # 至少使用75%的宽度 + current_width = content_right_margin - content_left_margin + if current_width < min_content_width: + extra_width = min_content_width - current_width + content_left_margin = max(30, content_left_margin - extra_width // 2) + content_right_margin = min(width - 30, content_right_margin + extra_width // 2) + + return content_left_margin, content_right_margin def _render_footer(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, left: int, right: int): """渲染页脚文本""" @@ -288,11 +380,13 @@ class VibrantTemplate(BaseTemplate): draw.text((right - width, y), pagination, font=font, fill=(255, 255, 255)) def _render_title_subtitle(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, center_x: int, left: int, right: int) -> int: - """渲染标题和副标题""" + """渲染标题和副标题(增强版本,与demo一致)""" # 标题 title_text = content.get("title", "默认标题") title_target_width = int((right - left) * 0.98) - title_size=self.text_renderer.calculate_optimal_font_size(title_text,title_target_width,max_size=140,min_size=40) + title_size, title_actual_width = self._calculate_optimal_font_size_enhanced( + title_text, title_target_width, max_size=140, min_size=40 + ) title_font = self.text_renderer._load_default_font(title_size) text_w, text_h = self.text_renderer.get_text_size(title_text, title_font) @@ -303,7 +397,9 @@ class VibrantTemplate(BaseTemplate): # 副标题 (slogan) subtitle_text = content.get("slogan", "") subtitle_target_width = int((right - left) * 0.95) - subtitle_size=self.text_renderer.calculate_optimal_font_size(subtitle_text,subtitle_target_width,max_size=75,min_size=20) + subtitle_size, subtitle_actual_width = self._calculate_optimal_font_size_enhanced( + subtitle_text, subtitle_target_width, max_size=75, min_size=20 + ) subtitle_font = self.text_renderer._load_default_font(subtitle_size) sub_text_w, sub_text_h = self.text_renderer.get_text_size(subtitle_text, subtitle_font) @@ -348,31 +444,34 @@ class VibrantTemplate(BaseTemplate): draw.text((x, item_y), " " + item, font=font, fill=(255, 255, 255)) def _render_right_column(self, draw: ImageDraw.Draw, content: Dict[str, Any], y: int, x: int, right_margin: int): - """渲染右栏内容:价格、票种和备注""" + """渲染右栏内容:价格、票种和备注(增强版本,与demo一致)""" price_text = content.get('price', '') - price_size=self.text_renderer.calculate_optimal_font_size(price_text,int((right_margin - x) * 0.7),max_size=120,min_size=40) - price_width,_=self.text_renderer.get_text_size(price_text,self.text_renderer._load_default_font(price_size)) + price_target_width = int((right_margin - x) * 0.7) + price_size, price_actual_width = self._calculate_optimal_font_size_enhanced( + price_text, price_target_width, max_size=120, min_size=40 + ) price_font = self.text_renderer._load_default_font(price_size) - suffix_font = self.text_renderer._load_default_font(int(price_size * 0.3)) _, price_height = self.text_renderer.get_text_size(price_text, price_font) suffix_width, suffix_height = self.text_renderer.get_text_size("CNY起", suffix_font) - price_x = right_margin - price_width - suffix_width + price_x = right_margin - price_actual_width - suffix_width self.text_renderer.draw_text_with_shadow(draw, (price_x, y), price_text, price_font) suffix_y = y + price_height - suffix_height - draw.text((price_x + price_width, suffix_y), "CNY起", font=suffix_font, fill=(255, 255, 255)) + draw.text((price_x + price_actual_width, suffix_y), "CNY起", font=suffix_font, fill=(255, 255, 255)) underline_y = y + price_height + 18 draw.line([(price_x - 10, underline_y), (right_margin, underline_y)], fill=(255, 255, 255, 80), width=2) ticket_text = content.get("ticket_type", "") - ticket_size=self.text_renderer.calculate_optimal_font_size(ticket_text,int((right_margin - x) * 0.7),max_size=60,min_size=30) - ticket_width,_=self.text_renderer.get_text_size(ticket_text,self.text_renderer._load_default_font(ticket_size)) + ticket_target_width = int((right_margin - x) * 0.7) + ticket_size, ticket_actual_width = self._calculate_optimal_font_size_enhanced( + ticket_text, ticket_target_width, max_size=60, min_size=30 + ) ticket_font = self.text_renderer._load_default_font(ticket_size) - ticket_x = right_margin - ticket_width + ticket_x = right_margin - ticket_actual_width ticket_y = y + price_height + 35 self.text_renderer.draw_text_with_shadow(draw, (ticket_x, ticket_y), ticket_text, ticket_font) _, ticket_height = self.text_renderer.get_text_size(ticket_text, ticket_font)