diff --git a/poster/templates/__pycache__/vibrant_template.cpython-312.pyc b/poster/templates/__pycache__/vibrant_template.cpython-312.pyc index 2d6aab3..de627e7 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 ba4c550..a5ec2d0 100644 --- a/poster/templates/vibrant_template.py +++ b/poster/templates/vibrant_template.py @@ -43,6 +43,25 @@ class VibrantTemplate(BaseTemplate): 'intensity_multiplier': 1.5 }, } + # 设置中文字体路径 + self.chinese_font_path = "/root/TravelContentCreator/assets/font/兰亭粗黑简.TTF" + + # 重写text_renderer的字体加载方法以支持中文 + self._patch_text_renderer_for_chinese() + + def _patch_text_renderer_for_chinese(self): + """重写text_renderer的字体加载方法以支持中文""" + original_load_font = self.text_renderer._load_default_font + + def load_chinese_font(size: int): + try: + return ImageFont.truetype(self.chinese_font_path, size) + except: + logger.warning(f"无法加载中文字体,使用默认字体") + return original_load_font(size) + + # 替换字体加载方法 + self.text_renderer._load_default_font = load_chinese_font def generate(self, images: List, @@ -482,4 +501,451 @@ class VibrantTemplate(BaseTemplate): remarks_y = ticket_y + ticket_height + 30 for i, remark in enumerate(remarks): remark_width, _ = self.text_renderer.get_text_size(remark, remarks_font) - draw.text((right_margin - remark_width, remarks_y + i * 21), remark, font=remarks_font, fill=(255, 255, 255, 200)) \ No newline at end of file + draw.text((right_margin - remark_width, remarks_y + i * 21), remark, font=remarks_font, fill=(255, 255, 255, 200)) + + def generate_layered_psd(self, + images, + content: Optional[Dict[str, Any]] = None, + theme_color: Optional[str] = None, + glass_intensity: float = 1.5, + output_path: str = "layered_poster.psd", + **kwargs) -> str: + """ + 生成分层的PSD文件,方便后续修改 + + Args: + images: 主图 + content: 包含所有文本信息的字典 + theme_color: 预设颜色主题的名称 + glass_intensity: 毛玻璃效果强度 + output_path: PSD文件输出路径 + + Returns: + str: 生成的PSD文件路径 + """ + try: + from psd_tools import PSDImage + from psd_tools.api.layers import PixelLayer + except ImportError: + logger.error("需要安装psd-tools库: pip install psd-tools") + return None + + logger.info("开始生成分层PSD文件...") + + if content is None: + content = self._get_default_content() + + self.config['glass_effect']['intensity_multiplier'] = glass_intensity + + main_image = images + if not main_image: + logger.error("无法加载图片") + return None + + main_image = self.image_processor.resize_image(image=main_image, target_size=self.size) + estimated_height = self._estimate_content_height(content) + gradient_start = self._detect_gradient_start_position(main_image, estimated_height) + + # 创建新的PSD文档 + psd = PSDImage.new("RGB", self.size, color=(255, 255, 255)) + logger.info(f"创建PSD文档,尺寸: {self.size}") + + # 1. 添加背景图层 + background_layer = PixelLayer.frompil(main_image, psd, "Background") + psd.append(background_layer) + logger.info("✓ 添加背景图层") + + # 2. 添加毛玻璃效果层 + glass_overlay = self._create_glass_overlay_layer(main_image, gradient_start, theme_color) + if glass_overlay: + glass_layer = PixelLayer.frompil(glass_overlay, psd, "Glass Effect") + psd.append(glass_layer) + logger.info("✓ 添加毛玻璃效果层") + + # 3-7. 添加文字图层 + text_layers = self._create_text_layers(content, gradient_start) + for layer_name, layer_image in text_layers.items(): + if layer_image: + text_layer = PixelLayer.frompil(layer_image, psd, layer_name) + psd.append(text_layer) + logger.info(f"✓ Added {layer_name} layer") + + # 注意:PSD文件保持原始尺寸,最终调整在导出时进行 + + # 保存PSD文件 + psd.save(output_path) + logger.info(f"✓ PSD文件已保存: {output_path}") + + return output_path + + def _create_glass_overlay_layer(self, main_image: Image.Image, gradient_start: int, theme_color: Optional[str]) -> Optional[Image.Image]: + """创建毛玻璃效果的独立图层""" + try: + if theme_color and theme_color in self.config['colors']: + top_color, bottom_color = self.config['colors'][theme_color] + else: + top_color, bottom_color = self._extract_glass_colors_from_image(main_image, gradient_start) + + # 创建透明背景的毛玻璃层 + overlay = self._create_frosted_glass_overlay(top_color, bottom_color, gradient_start) + return overlay + except Exception as e: + logger.error(f"创建毛玻璃层失败: {e}") + return None + + def _create_text_layers(self, content: Dict[str, Any], gradient_start: int) -> Dict[str, Optional[Image.Image]]: + """创建各个文字图层""" + layers = {} + + try: + # 创建透明画布 + canvas_size = self.size + + # 计算布局参数 + width, height = canvas_size + center_x = width // 2 + left_margin, right_margin = self._calculate_content_margins(content, width, center_x) + + # 1. 标题层 + layers["Title Text"] = self._create_title_layer(content, gradient_start, center_x, left_margin, right_margin, canvas_size) + + # 2. 副标题层 + layers["Subtitle Text"] = self._create_subtitle_layer(content, gradient_start, center_x, left_margin, right_margin, canvas_size) + + # 3. 装饰线层 + layers["Decorations"] = self._create_decoration_layer(content, gradient_start, center_x, left_margin, right_margin, canvas_size) + + # 4. 左栏内容层 + title_y = gradient_start + 40 + subtitle_height = 80 + 30 # 预估副标题高度 + content_start_y = title_y + subtitle_height + 30 + content_area_width = right_margin - left_margin + left_column_width = int(content_area_width * 0.5) + + layers["Left Content"] = self._create_left_column_layer(content, content_start_y, left_margin, left_column_width, canvas_size) + + # 5. 右栏内容层 + right_column_x = left_margin + left_column_width + layers["Right Content"] = self._create_right_column_layer(content, content_start_y, right_column_x, right_margin, canvas_size) + + # 6. 页脚层 + footer_y = height - 30 + layers["Footer Info"] = self._create_footer_layer(content, footer_y, left_margin, right_margin, canvas_size) + + except Exception as e: + logger.error(f"创建文字图层失败: {e}") + + return layers + + def _draw_text_with_outline_simple(self, draw: ImageDraw.Draw, position: Tuple[int, int], + text: str, font: ImageFont.FreeTypeFont, + text_color: Tuple[int, int, int, int] = (255, 255, 255, 255), + outline_color: Tuple[int, int, int, int] = (0, 0, 0, 255), + outline_width: int = 2): + """简单的文本描边绘制方法""" + x, y = position + + # 绘制描边 + for dx in range(-outline_width, outline_width + 1): + for dy in range(-outline_width, outline_width + 1): + if dx == 0 and dy == 0: + continue + draw.text((x + dx, y + dy), text, font=font, fill=outline_color) + + # 绘制主文本 + draw.text((x, y), text, font=font, fill=text_color) + + def _draw_text_with_shadow_simple(self, draw: ImageDraw.Draw, position: Tuple[int, int], + text: str, font: ImageFont.FreeTypeFont, + text_color: Tuple[int, int, int, int] = (255, 255, 255, 255), + shadow_color: Tuple[int, int, int, int] = (0, 0, 0, 128), + shadow_offset: Tuple[int, int] = (2, 2)): + """简单的文本阴影绘制方法""" + x, y = position + shadow_x, shadow_y = shadow_offset + + # 绘制阴影 + draw.text((x + shadow_x, y + shadow_y), text, font=font, fill=shadow_color) + + # 绘制主文本 + draw.text((x, y), text, font=font, fill=text_color) + + def _create_title_layer(self, content: Dict[str, Any], gradient_start: int, center_x: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]: + """创建标题图层""" + try: + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + + title_text = content.get("title", "默认标题") + title_target_width = int((right_margin - left_margin) * 0.98) + + # 使用指定的中文字体 + title_size, title_actual_width = self._calculate_optimal_font_size_enhanced( + title_text, title_target_width, max_size=140, min_size=40 + ) + + try: + title_font = ImageFont.truetype(self.chinese_font_path, title_size) + except: + title_font = self.text_renderer._load_default_font(title_size) + + # 重新计算实际尺寸 + bbox = title_font.getbbox(title_text) + text_w = bbox[2] - bbox[0] + text_h = bbox[3] - bbox[1] + title_x = center_x - text_w // 2 + title_y = gradient_start + 40 + + # 绘制带描边的标题 + self._draw_text_with_outline_simple( + draw, (title_x, title_y), title_text, title_font, + text_color=(255, 255, 255, 255), + outline_color=(0, 30, 80, 200), + outline_width=4 + ) + + return canvas + except Exception as e: + logger.error(f"创建标题层失败: {e}") + import traceback + traceback.print_exc() + return None + + def _create_subtitle_layer(self, content: Dict[str, Any], gradient_start: int, center_x: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]: + """创建副标题图层""" + try: + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + + subtitle_text = content.get("slogan", "") + if not subtitle_text: + return None + + subtitle_target_width = int((right_margin - left_margin) * 0.95) + subtitle_size, subtitle_actual_width = self._calculate_optimal_font_size_enhanced( + subtitle_text, subtitle_target_width, max_size=75, min_size=20 + ) + + # 使用指定的中文字体 + try: + subtitle_font = ImageFont.truetype(self.chinese_font_path, subtitle_size) + except: + subtitle_font = self.text_renderer._load_default_font(subtitle_size) + + bbox = subtitle_font.getbbox(subtitle_text) + sub_text_w = bbox[2] - bbox[0] + sub_text_h = bbox[3] - bbox[1] + subtitle_x = center_x - sub_text_w // 2 + subtitle_y = gradient_start + 40 + 100 + 30 # title_y + title_height + spacing + + # 绘制带阴影的副标题 + self._draw_text_with_shadow_simple( + draw, (subtitle_x, subtitle_y), subtitle_text, subtitle_font, + text_color=(255, 255, 255, 255), + shadow_color=(0, 0, 0, 180), + shadow_offset=(2, 2) + ) + + return canvas + except Exception as e: + logger.error(f"创建副标题层失败: {e}") + import traceback + traceback.print_exc() + return None + + def _create_decoration_layer(self, content: Dict[str, Any], gradient_start: int, center_x: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]: + """创建装饰元素图层""" + try: + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + + # 获取标题信息用于装饰线定位 + title_text = content.get("title", "默认标题") + title_target_width = int((right_margin - left_margin) * 0.98) + title_size, title_actual_width = self._calculate_optimal_font_size_enhanced( + title_text, title_target_width, max_size=140, min_size=40 + ) + + # 使用中文字体计算标题尺寸 + try: + title_font = ImageFont.truetype(self.chinese_font_path, title_size) + except: + title_font = self.text_renderer._load_default_font(title_size) + + bbox = title_font.getbbox(title_text) + text_w = bbox[2] - bbox[0] + text_h = bbox[3] - bbox[1] + title_x = center_x - text_w // 2 + title_y = gradient_start + 40 + + # 在标题下方添加装饰线 + line_y = title_y + text_h + 5 + line_start_x = title_x - text_w * 0.025 + line_end_x = title_x + text_w * 1.025 + draw.line([(line_start_x, line_y), (line_end_x, line_y)], fill=(215, 215, 215, 80), width=3) + + return canvas + except Exception as e: + logger.error(f"创建装饰层失败: {e}") + import traceback + traceback.print_exc() + return None + + def _create_left_column_layer(self, content: Dict[str, Any], y: int, x: int, width: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]: + """创建左栏内容图层""" + try: + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + + # 使用中文字体 + + # 按钮 + try: + button_font = ImageFont.truetype(self.chinese_font_path, 30) + except: + button_font = self.text_renderer._load_default_font(30) + + button_text = content.get("content_button", "套餐内容") + bbox = button_font.getbbox(button_text) + button_text_width = bbox[2] - bbox[0] + button_width = button_text_width + 40 + button_height = 50 + + # 绘制简单的矩形按钮 + draw.rounded_rectangle([x, y, x + button_width, y + button_height], + radius=20, fill=(0, 140, 210, 180), + outline=(255, 255, 255, 255), width=1) + draw.text((x + 20, y + (button_height - 30) // 2), button_text, font=button_font, fill=(255, 255, 255)) + + # 项目列表 + items = content.get("content_items", []) + if items: + try: + list_font = ImageFont.truetype(self.chinese_font_path, 28) + except: + list_font = self.text_renderer._load_default_font(28) + + list_y = y + button_height + 20 + line_spacing = 36 + + for i, item in enumerate(items): + item_y = list_y + i * line_spacing + draw.text((x, item_y), "• " + item, font=list_font, fill=(255, 255, 255)) + + return canvas + except Exception as e: + logger.error(f"创建左栏内容层失败: {e}") + import traceback + traceback.print_exc() + return None + + def _create_right_column_layer(self, content: Dict[str, Any], y: int, x: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]: + """创建右栏内容图层""" + try: + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + + # 使用中文字体 + + # 价格 + price_text = content.get('price', '') + if price_text: + 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 + ) + + try: + price_font = ImageFont.truetype(self.chinese_font_path, price_size) + suffix_font = ImageFont.truetype(self.chinese_font_path, int(price_size * 0.3)) + except: + price_font = self.text_renderer._load_default_font(price_size) + suffix_font = self.text_renderer._load_default_font(int(price_size * 0.3)) + + price_bbox = price_font.getbbox(price_text) + price_height = price_bbox[3] - price_bbox[1] + suffix_bbox = suffix_font.getbbox("CNY起") + suffix_width = suffix_bbox[2] - suffix_bbox[0] + suffix_height = suffix_bbox[3] - suffix_bbox[1] + + price_x = right_margin - price_actual_width - suffix_width + self._draw_text_with_shadow_simple(draw, (price_x, y), price_text, price_font) + + suffix_y = y + price_height - suffix_height + 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", "") + if ticket_text: + 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 + ) + + try: + ticket_font = ImageFont.truetype(self.chinese_font_path, ticket_size) + except: + ticket_font = self.text_renderer._load_default_font(ticket_size) + + ticket_x = right_margin - ticket_actual_width + ticket_y = y + price_height + 35 + self._draw_text_with_shadow_simple(draw, (ticket_x, ticket_y), ticket_text, ticket_font) + + ticket_bbox = ticket_font.getbbox(ticket_text) + ticket_height = ticket_bbox[3] - ticket_bbox[1] + + # 备注 + remarks = content.get("remarks", []) + if remarks: + try: + remarks_font = ImageFont.truetype(self.chinese_font_path, 16) + except: + remarks_font = self.text_renderer._load_default_font(16) + + remarks_y = ticket_y + ticket_height + 30 + for i, remark in enumerate(remarks): + remark_bbox = remarks_font.getbbox(remark) + remark_width = remark_bbox[2] - remark_bbox[0] + draw.text((right_margin - remark_width, remarks_y + i * 21), remark, font=remarks_font, fill=(255, 255, 255, 200)) + + return canvas + except Exception as e: + logger.error(f"创建右栏内容层失败: {e}") + import traceback + traceback.print_exc() + return None + + def _create_footer_layer(self, content: Dict[str, Any], footer_y: int, left_margin: int, right_margin: int, canvas_size: Tuple[int, int]) -> Optional[Image.Image]: + """创建页脚图层""" + try: + canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(canvas) + + # 使用中文字体 + try: + font = ImageFont.truetype(self.chinese_font_path, 18) + except: + font = self.text_renderer._load_default_font(18) + + # 标签(左下角) + tag_text = content.get("tag", "") + if tag_text: + draw.text((left_margin, footer_y), tag_text, font=font, fill=(255, 255, 255)) + + # 分页信息(右下角) + pagination_text = content.get("pagination", "") + if pagination_text: + pagination_bbox = font.getbbox(pagination_text) + pagination_width = pagination_bbox[2] - pagination_bbox[0] + draw.text((right_margin - pagination_width, footer_y), pagination_text, font=font, fill=(255, 255, 255)) + + return canvas + except Exception as e: + logger.error(f"创建页脚层失败: {e}") + import traceback + traceback.print_exc() + return None \ No newline at end of file