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')
|