#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 海报渲染器 负责海报图片的渲染和输出 """ import logging import base64 from io import BytesIO from typing import Dict, Any, Optional, List, Tuple from pathlib import Path logger = logging.getLogger(__name__) try: from PIL import Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False class PosterRenderer: """ 海报渲染器 职责: - 调用模板渲染海报图片 - 处理图片格式转换 - 管理输出文件 """ def __init__(self, output_dir: Optional[str] = None): """ 初始化渲染器 Args: output_dir: 输出目录 """ self._output_dir = Path(output_dir) if output_dir else None self.logger = logging.getLogger(f"{__name__}.PosterRenderer") def render( self, template_handler, images: List['Image.Image'], content: Dict[str, Any], output_format: str = 'png' ) -> Dict[str, Any]: """ 渲染海报 Args: template_handler: 模板处理器实例 images: 输入图片列表 content: 海报内容 output_format: 输出格式 (png, json, both) Returns: 渲染结果字典 """ try: self.logger.info(f"开始渲染海报,格式: {output_format}") # 获取第一张图片作为背景 background = images[0] if images else None # 检查模板是否支持统一渲染 if hasattr(template_handler, '_unified_render'): return self._render_unified( template_handler, background, content, output_format ) # 使用传统渲染 return self._render_legacy( template_handler, images, content, output_format ) except Exception as e: self.logger.error(f"渲染失败: {e}") return {"success": False, "error": str(e)} def _render_unified( self, template_handler, background: Optional['Image.Image'], content: Dict[str, Any], output_format: str ) -> Dict[str, Any]: """使用统一渲染方法""" try: result = template_handler._unified_render( images=background, content=content, theme_color=None, glass_intensity=1.5, output_format=output_format ) if "error" in result: return {"success": False, "error": result["error"]} return { "success": True, "image": result.get("image"), "fabric_json": result.get("fabric_json"), "layout_params": result.get("layout_params"), } except Exception as e: self.logger.error(f"统一渲染失败: {e}") return {"success": False, "error": str(e)} def _render_legacy( self, template_handler, images: List['Image.Image'], content: Dict[str, Any], output_format: str ) -> Dict[str, Any]: """使用传统渲染方法""" try: # 调用模板的 generate 方法 if hasattr(template_handler, 'generate'): result = template_handler.generate(images, content) if isinstance(result, Image.Image): return { "success": True, "image": result, } elif isinstance(result, dict): return { "success": True, **result } return {"success": False, "error": "模板不支持渲染"} except Exception as e: self.logger.error(f"传统渲染失败: {e}") return {"success": False, "error": str(e)} def image_to_base64(self, image: 'Image.Image', format: str = 'PNG') -> str: """ 将图片转换为 base64 Args: image: PIL Image format: 图片格式 Returns: base64 字符串 """ buffer = BytesIO() image.save(buffer, format=format) return base64.b64encode(buffer.getvalue()).decode('utf-8') def base64_to_image(self, base64_str: str) -> Optional['Image.Image']: """ 将 base64 转换为图片 Args: base64_str: base64 字符串 Returns: PIL Image """ try: # 移除可能的 data URL 前缀 if ',' in base64_str: base64_str = base64_str.split(',', 1)[1] image_data = base64.b64decode(base64_str) image = Image.open(BytesIO(image_data)) if image.mode != 'RGBA': image = image.convert('RGBA') return image except Exception as e: self.logger.error(f"base64 转图片失败: {e}") return None def save_image( self, image: 'Image.Image', filename: str, format: str = 'PNG', quality: int = 95 ) -> Optional[Path]: """ 保存图片到文件 Args: image: PIL Image filename: 文件名 format: 图片格式 quality: 质量 Returns: 保存路径 """ if not self._output_dir: self.logger.warning("未设置输出目录") return None try: self._output_dir.mkdir(parents=True, exist_ok=True) file_path = self._output_dir / filename save_kwargs = {'format': format} if format.upper() in ('JPEG', 'WEBP'): save_kwargs['quality'] = quality image.save(file_path, **save_kwargs) self.logger.info(f"图片已保存: {file_path}") return file_path except Exception as e: self.logger.error(f"保存图片失败: {e}") return None def resize_image( self, image: 'Image.Image', target_size: Tuple[int, int], mode: str = 'cover' ) -> 'Image.Image': """ 调整图片尺寸 Args: image: PIL Image target_size: 目标尺寸 (width, height) mode: 调整模式 (cover, contain, stretch) Returns: 调整后的图片 """ target_width, target_height = target_size if mode == 'stretch': return image.resize(target_size, Image.Resampling.LANCZOS) # 计算缩放比例 scale_x = target_width / image.width scale_y = target_height / image.height if mode == 'cover': scale = max(scale_x, scale_y) else: # contain scale = min(scale_x, scale_y) new_width = int(image.width * scale) new_height = int(image.height * scale) resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS) if mode == 'cover': # 裁剪到目标尺寸 left = (new_width - target_width) // 2 top = (new_height - target_height) // 2 return resized.crop((left, top, left + target_width, top + target_height)) else: # 创建目标尺寸画布并居中 result = Image.new('RGBA', target_size, (255, 255, 255, 255)) x = (target_width - new_width) // 2 y = (target_height - new_height) // 2 result.paste(resized, (x, y)) return result