diff --git a/poster/templates/__pycache__/vibrant_template.cpython-312.pyc b/poster/templates/__pycache__/vibrant_template.cpython-312.pyc index 9071ead..2d6aab3 100644 Binary files a/poster/templates/__pycache__/vibrant_template.cpython-312.pyc and b/poster/templates/__pycache__/vibrant_template.cpython-312.pyc differ 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)