TravelContentCreator/domain/poster/poster_renderer.py

275 lines
8.0 KiB
Python

#!/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