455 lines
13 KiB
Python
455 lines
13 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
Fabric.js JSON 生成器
|
|||
|
|
负责生成 Fabric.js 兼容的 JSON 格式
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import logging
|
|||
|
|
import base64
|
|||
|
|
from io import BytesIO
|
|||
|
|
from typing import Dict, Any, Optional, List, Tuple
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from PIL import Image
|
|||
|
|
PIL_AVAILABLE = True
|
|||
|
|
except ImportError:
|
|||
|
|
PIL_AVAILABLE = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class FabricGenerator:
|
|||
|
|
"""
|
|||
|
|
Fabric.js JSON 生成器
|
|||
|
|
|
|||
|
|
职责:
|
|||
|
|
- 生成 Fabric.js 兼容的 JSON
|
|||
|
|
- 创建各种图形对象(图片、文本、形状)
|
|||
|
|
- 处理布局计算
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# Fabric.js 版本
|
|||
|
|
FABRIC_VERSION = "5.3.0"
|
|||
|
|
|
|||
|
|
# 基础画布尺寸
|
|||
|
|
BASE_WIDTH = 1350
|
|||
|
|
BASE_HEIGHT = 1800
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self.logger = logging.getLogger(f"{__name__}.FabricGenerator")
|
|||
|
|
|
|||
|
|
def generate(
|
|||
|
|
self,
|
|||
|
|
content: Dict[str, Any],
|
|||
|
|
template_id: str,
|
|||
|
|
image_size: Tuple[int, int],
|
|||
|
|
background_image: Optional['Image.Image'] = None,
|
|||
|
|
template_handler=None
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
生成 Fabric.js JSON
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: 海报内容
|
|||
|
|
template_id: 模板 ID
|
|||
|
|
image_size: 画布尺寸 (width, height)
|
|||
|
|
background_image: 背景图片
|
|||
|
|
template_handler: 模板处理器实例
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Fabric.js JSON 字典
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
self.logger.info(f"开始生成 Fabric.js JSON,模板: {template_id}")
|
|||
|
|
|
|||
|
|
# 如果有模板处理器且支持统一渲染
|
|||
|
|
if template_handler and hasattr(template_handler, '_unified_render'):
|
|||
|
|
return self._generate_with_template(
|
|||
|
|
content, template_handler, background_image
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 否则使用通用逻辑
|
|||
|
|
return self._generate_generic(
|
|||
|
|
content, template_id, image_size, background_image
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
self.logger.error(f"Fabric.js JSON 生成失败: {e}")
|
|||
|
|
return self._create_empty_json(image_size)
|
|||
|
|
|
|||
|
|
def _generate_with_template(
|
|||
|
|
self,
|
|||
|
|
content: Dict[str, Any],
|
|||
|
|
template_handler,
|
|||
|
|
background_image: Optional['Image.Image']
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""使用模板处理器生成"""
|
|||
|
|
try:
|
|||
|
|
render_result = template_handler._unified_render(
|
|||
|
|
images=background_image,
|
|||
|
|
content=content,
|
|||
|
|
theme_color=None,
|
|||
|
|
glass_intensity=1.5,
|
|||
|
|
output_format='json'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if "error" in render_result:
|
|||
|
|
raise Exception(render_result["error"])
|
|||
|
|
|
|||
|
|
return render_result["fabric_json"]
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
self.logger.error(f"模板渲染失败: {e}")
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def _generate_generic(
|
|||
|
|
self,
|
|||
|
|
content: Dict[str, Any],
|
|||
|
|
template_id: str,
|
|||
|
|
image_size: Tuple[int, int],
|
|||
|
|
background_image: Optional['Image.Image']
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""通用生成逻辑"""
|
|||
|
|
width, height = image_size
|
|||
|
|
objects = []
|
|||
|
|
|
|||
|
|
# 1. 背景图片
|
|||
|
|
if background_image:
|
|||
|
|
objects.append(self.create_image_object(
|
|||
|
|
background_image, width, height, is_background=True
|
|||
|
|
))
|
|||
|
|
else:
|
|||
|
|
objects.append(self.create_placeholder_object(width, height))
|
|||
|
|
|
|||
|
|
# 2. 渐变遮罩
|
|||
|
|
gradient_start = self._calculate_gradient_start(content, height)
|
|||
|
|
objects.append(self.create_gradient_object(width, height, gradient_start))
|
|||
|
|
|
|||
|
|
# 3. 文本对象
|
|||
|
|
text_objects = self.create_text_objects(content, width, height, gradient_start)
|
|||
|
|
objects.extend(text_objects)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"version": self.FABRIC_VERSION,
|
|||
|
|
"width": width,
|
|||
|
|
"height": height,
|
|||
|
|
"objects": objects
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _create_empty_json(self, image_size: Tuple[int, int]) -> Dict[str, Any]:
|
|||
|
|
"""创建空的 Fabric.js JSON"""
|
|||
|
|
return {
|
|||
|
|
"version": self.FABRIC_VERSION,
|
|||
|
|
"width": image_size[0],
|
|||
|
|
"height": image_size[1],
|
|||
|
|
"objects": []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _calculate_gradient_start(self, content: Dict[str, Any], height: int) -> int:
|
|||
|
|
"""计算渐变起始位置"""
|
|||
|
|
# 估算内容高度
|
|||
|
|
estimated_height = self._estimate_content_height(content)
|
|||
|
|
# 渐变从内容区域上方开始
|
|||
|
|
return max(int(height * 0.4), height - estimated_height - 100)
|
|||
|
|
|
|||
|
|
def _estimate_content_height(self, content: Dict[str, Any]) -> int:
|
|||
|
|
"""估算内容高度"""
|
|||
|
|
height = 0
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
if content.get('title'):
|
|||
|
|
height += 80
|
|||
|
|
|
|||
|
|
# 副标题
|
|||
|
|
if content.get('subtitle'):
|
|||
|
|
height += 50
|
|||
|
|
|
|||
|
|
# 价格
|
|||
|
|
if content.get('price'):
|
|||
|
|
height += 60
|
|||
|
|
|
|||
|
|
# 描述
|
|||
|
|
if content.get('description'):
|
|||
|
|
lines = len(content['description']) // 20 + 1
|
|||
|
|
height += lines * 30
|
|||
|
|
|
|||
|
|
# 标签
|
|||
|
|
if content.get('tags'):
|
|||
|
|
height += 40
|
|||
|
|
|
|||
|
|
return height + 100 # 额外边距
|
|||
|
|
|
|||
|
|
# ========== 对象创建方法 ==========
|
|||
|
|
|
|||
|
|
def create_image_object(
|
|||
|
|
self,
|
|||
|
|
image: 'Image.Image',
|
|||
|
|
canvas_width: int,
|
|||
|
|
canvas_height: int,
|
|||
|
|
is_background: bool = False
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
创建图片对象
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
image: PIL Image
|
|||
|
|
canvas_width: 画布宽度
|
|||
|
|
canvas_height: 画布高度
|
|||
|
|
is_background: 是否为背景图片
|
|||
|
|
"""
|
|||
|
|
# 转换为 base64
|
|||
|
|
image_base64 = self._image_to_base64(image)
|
|||
|
|
|
|||
|
|
# 计算缩放
|
|||
|
|
scale_x = canvas_width / image.width
|
|||
|
|
scale_y = canvas_height / image.height
|
|||
|
|
scale = max(scale_x, scale_y) if is_background else min(scale_x, scale_y)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"type": "image",
|
|||
|
|
"version": self.FABRIC_VERSION,
|
|||
|
|
"left": 0,
|
|||
|
|
"top": 0,
|
|||
|
|
"width": image.width,
|
|||
|
|
"height": image.height,
|
|||
|
|
"scaleX": scale,
|
|||
|
|
"scaleY": scale,
|
|||
|
|
"src": f"data:image/png;base64,{image_base64}",
|
|||
|
|
"selectable": not is_background,
|
|||
|
|
"evented": not is_background,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create_placeholder_object(
|
|||
|
|
self,
|
|||
|
|
canvas_width: int,
|
|||
|
|
canvas_height: int
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""创建占位符对象"""
|
|||
|
|
return {
|
|||
|
|
"type": "rect",
|
|||
|
|
"version": self.FABRIC_VERSION,
|
|||
|
|
"left": 0,
|
|||
|
|
"top": 0,
|
|||
|
|
"width": canvas_width,
|
|||
|
|
"height": canvas_height,
|
|||
|
|
"fill": "#f0f0f0",
|
|||
|
|
"selectable": False,
|
|||
|
|
"evented": False,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create_gradient_object(
|
|||
|
|
self,
|
|||
|
|
canvas_width: int,
|
|||
|
|
canvas_height: int,
|
|||
|
|
gradient_start: int,
|
|||
|
|
colors: Optional[Dict[str, Tuple[int, int, int]]] = None
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
创建渐变遮罩对象
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
canvas_width: 画布宽度
|
|||
|
|
canvas_height: 画布高度
|
|||
|
|
gradient_start: 渐变起始位置
|
|||
|
|
colors: 渐变颜色
|
|||
|
|
"""
|
|||
|
|
if colors is None:
|
|||
|
|
colors = {
|
|||
|
|
'top': (255, 255, 255),
|
|||
|
|
'bottom': (240, 240, 240)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
top_color = colors.get('top', (255, 255, 255))
|
|||
|
|
bottom_color = colors.get('bottom', (240, 240, 240))
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"type": "rect",
|
|||
|
|
"version": self.FABRIC_VERSION,
|
|||
|
|
"left": 0,
|
|||
|
|
"top": gradient_start,
|
|||
|
|
"width": canvas_width,
|
|||
|
|
"height": canvas_height - gradient_start,
|
|||
|
|
"fill": {
|
|||
|
|
"type": "linear",
|
|||
|
|
"coords": {
|
|||
|
|
"x1": 0,
|
|||
|
|
"y1": 0,
|
|||
|
|
"x2": 0,
|
|||
|
|
"y2": canvas_height - gradient_start
|
|||
|
|
},
|
|||
|
|
"colorStops": [
|
|||
|
|
{"offset": 0, "color": f"rgba({top_color[0]},{top_color[1]},{top_color[2]},0.8)"},
|
|||
|
|
{"offset": 1, "color": f"rgba({bottom_color[0]},{bottom_color[1]},{bottom_color[2]},0.95)"}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
"selectable": False,
|
|||
|
|
"evented": False,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create_text_object(
|
|||
|
|
self,
|
|||
|
|
text: str,
|
|||
|
|
left: int,
|
|||
|
|
top: int,
|
|||
|
|
font_size: int = 24,
|
|||
|
|
font_family: str = "Arial",
|
|||
|
|
fill: str = "#000000",
|
|||
|
|
font_weight: str = "normal",
|
|||
|
|
text_align: str = "left",
|
|||
|
|
**kwargs
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
创建文本对象
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
text: 文本内容
|
|||
|
|
left: 左边距
|
|||
|
|
top: 上边距
|
|||
|
|
font_size: 字体大小
|
|||
|
|
font_family: 字体
|
|||
|
|
fill: 填充颜色
|
|||
|
|
font_weight: 字重
|
|||
|
|
text_align: 对齐方式
|
|||
|
|
"""
|
|||
|
|
obj = {
|
|||
|
|
"type": "text",
|
|||
|
|
"version": self.FABRIC_VERSION,
|
|||
|
|
"left": left,
|
|||
|
|
"top": top,
|
|||
|
|
"text": text,
|
|||
|
|
"fontSize": font_size,
|
|||
|
|
"fontFamily": font_family,
|
|||
|
|
"fill": fill,
|
|||
|
|
"fontWeight": font_weight,
|
|||
|
|
"textAlign": text_align,
|
|||
|
|
"selectable": True,
|
|||
|
|
"evented": True,
|
|||
|
|
}
|
|||
|
|
obj.update(kwargs)
|
|||
|
|
return obj
|
|||
|
|
|
|||
|
|
def create_text_objects(
|
|||
|
|
self,
|
|||
|
|
content: Dict[str, Any],
|
|||
|
|
canvas_width: int,
|
|||
|
|
canvas_height: int,
|
|||
|
|
gradient_start: int
|
|||
|
|
) -> List[Dict[str, Any]]:
|
|||
|
|
"""
|
|||
|
|
创建所有文本对象
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
content: 内容字典
|
|||
|
|
canvas_width: 画布宽度
|
|||
|
|
canvas_height: 画布高度
|
|||
|
|
gradient_start: 渐变起始位置
|
|||
|
|
"""
|
|||
|
|
objects = []
|
|||
|
|
margin = 40
|
|||
|
|
current_y = gradient_start + 60
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
if content.get('title'):
|
|||
|
|
objects.append(self.create_text_object(
|
|||
|
|
text=content['title'],
|
|||
|
|
left=margin,
|
|||
|
|
top=current_y,
|
|||
|
|
font_size=48,
|
|||
|
|
font_weight="bold",
|
|||
|
|
fill="#333333"
|
|||
|
|
))
|
|||
|
|
current_y += 70
|
|||
|
|
|
|||
|
|
# 副标题
|
|||
|
|
if content.get('subtitle'):
|
|||
|
|
objects.append(self.create_text_object(
|
|||
|
|
text=content['subtitle'],
|
|||
|
|
left=margin,
|
|||
|
|
top=current_y,
|
|||
|
|
font_size=28,
|
|||
|
|
fill="#666666"
|
|||
|
|
))
|
|||
|
|
current_y += 50
|
|||
|
|
|
|||
|
|
# 价格
|
|||
|
|
if content.get('price'):
|
|||
|
|
price_text = f"¥{content['price']}"
|
|||
|
|
if content.get('original_price'):
|
|||
|
|
price_text += f" 原价¥{content['original_price']}"
|
|||
|
|
|
|||
|
|
objects.append(self.create_text_object(
|
|||
|
|
text=price_text,
|
|||
|
|
left=margin,
|
|||
|
|
top=current_y,
|
|||
|
|
font_size=36,
|
|||
|
|
font_weight="bold",
|
|||
|
|
fill="#ff4444"
|
|||
|
|
))
|
|||
|
|
current_y += 60
|
|||
|
|
|
|||
|
|
# 描述
|
|||
|
|
if content.get('description'):
|
|||
|
|
objects.append(self.create_text_object(
|
|||
|
|
text=content['description'],
|
|||
|
|
left=margin,
|
|||
|
|
top=current_y,
|
|||
|
|
font_size=20,
|
|||
|
|
fill="#888888"
|
|||
|
|
))
|
|||
|
|
|
|||
|
|
return objects
|
|||
|
|
|
|||
|
|
def create_rect_object(
|
|||
|
|
self,
|
|||
|
|
left: int,
|
|||
|
|
top: int,
|
|||
|
|
width: int,
|
|||
|
|
height: int,
|
|||
|
|
fill: str = "#ffffff",
|
|||
|
|
stroke: Optional[str] = None,
|
|||
|
|
stroke_width: int = 1,
|
|||
|
|
rx: int = 0,
|
|||
|
|
ry: int = 0,
|
|||
|
|
**kwargs
|
|||
|
|
) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
创建矩形对象
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
left: 左边距
|
|||
|
|
top: 上边距
|
|||
|
|
width: 宽度
|
|||
|
|
height: 高度
|
|||
|
|
fill: 填充颜色
|
|||
|
|
stroke: 边框颜色
|
|||
|
|
stroke_width: 边框宽度
|
|||
|
|
rx: 圆角 X
|
|||
|
|
ry: 圆角 Y
|
|||
|
|
"""
|
|||
|
|
obj = {
|
|||
|
|
"type": "rect",
|
|||
|
|
"version": self.FABRIC_VERSION,
|
|||
|
|
"left": left,
|
|||
|
|
"top": top,
|
|||
|
|
"width": width,
|
|||
|
|
"height": height,
|
|||
|
|
"fill": fill,
|
|||
|
|
"rx": rx,
|
|||
|
|
"ry": ry,
|
|||
|
|
}
|
|||
|
|
if stroke:
|
|||
|
|
obj["stroke"] = stroke
|
|||
|
|
obj["strokeWidth"] = stroke_width
|
|||
|
|
obj.update(kwargs)
|
|||
|
|
return obj
|
|||
|
|
|
|||
|
|
# ========== 工具方法 ==========
|
|||
|
|
|
|||
|
|
def _image_to_base64(self, image: 'Image.Image', format: str = 'PNG') -> str:
|
|||
|
|
"""将 PIL Image 转换为 base64"""
|
|||
|
|
buffer = BytesIO()
|
|||
|
|
image.save(buffer, format=format)
|
|||
|
|
return base64.b64encode(buffer.getvalue()).decode('utf-8')
|