同步运行区
This commit is contained in:
parent
cf338c17a2
commit
5acbe8d7b7
@ -13,6 +13,7 @@ import time
|
||||
import json
|
||||
import importlib
|
||||
import base64
|
||||
import binascii
|
||||
from io import BytesIO
|
||||
from typing import List, Dict, Any, Optional, Type, Union, cast
|
||||
from datetime import datetime
|
||||
@ -163,14 +164,15 @@ class PosterService:
|
||||
async def generate_poster(self,
|
||||
template_id: str,
|
||||
poster_content: Optional[Dict[str, Any]],
|
||||
content_id: Optional[int],
|
||||
product_id: Optional[int],
|
||||
scenic_spot_id: Optional[int],
|
||||
content_id: Optional[str],
|
||||
product_id: Optional[str],
|
||||
scenic_spot_id: Optional[str],
|
||||
images_base64: Optional[List[str]] ,
|
||||
num_variations: int = 1,
|
||||
force_llm_generation: bool = False,
|
||||
generate_psd: bool = False,
|
||||
psd_output_path: Optional[str] = None) -> Dict[str, Any]:
|
||||
psd_output_path: Optional[str] = None,
|
||||
generate_fabric_json: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
统一的海报生成入口
|
||||
|
||||
@ -185,12 +187,25 @@ class PosterService:
|
||||
force_llm_generation: 是否强制使用LLM生成内容
|
||||
generate_psd: 是否生成PSD分层文件
|
||||
psd_output_path: PSD文件输出路径(可选,默认自动生成)
|
||||
generate_fabric_json: 是否生成Fabric.js JSON格式
|
||||
|
||||
Returns:
|
||||
生成结果字典,包含PNG图像和可选的PSD文件
|
||||
生成结果字典,包含PNG图像和可选的PSD文件、Fabric.js JSON
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# 添加参数调试信息
|
||||
logger.info("=" * 100)
|
||||
logger.info("海报生成服务 - 接收到的参数:")
|
||||
logger.info(f" template_id: {template_id} (类型: {type(template_id)})")
|
||||
logger.info(f" content_id: {content_id} (类型: {type(content_id)})")
|
||||
logger.info(f" product_id: {product_id} (类型: {type(product_id)})")
|
||||
logger.info(f" scenic_spot_id: {scenic_spot_id} (类型: {type(scenic_spot_id)})")
|
||||
logger.info(f" poster_content: {poster_content is not None}")
|
||||
logger.info(f" poster_content详细内容: {poster_content}")
|
||||
logger.info(f" force_llm_generation: {force_llm_generation}")
|
||||
logger.info("=" * 100)
|
||||
|
||||
# 1. 动态加载模板处理器
|
||||
template_handler = self._load_template_handler(template_id)
|
||||
if not template_handler:
|
||||
@ -200,7 +215,7 @@ class PosterService:
|
||||
final_content = poster_content
|
||||
if force_llm_generation or not final_content:
|
||||
logger.info(f"为模板 {template_id} 按需生成内容...")
|
||||
final_content = await self._generate_content_with_llm(template_id, content_id, product_id, scenic_spot_id)
|
||||
final_content = await self._generate_content_with_llm(template_id, content_id, product_id, scenic_spot_id, poster_content)
|
||||
|
||||
if not final_content:
|
||||
raise ValueError("无法获取用于生成海报的内容")
|
||||
@ -224,15 +239,58 @@ class PosterService:
|
||||
if images_base64.startswith("data:"):
|
||||
images_base64 = images_base64.split(",", 1)[1]
|
||||
|
||||
# 彻底清理base64字符串 - 移除所有空白字符
|
||||
images_base64 = ''.join(images_base64.split())
|
||||
|
||||
# 验证base64字符串长度(应该是4的倍数)
|
||||
if len(images_base64) % 4 != 0:
|
||||
# 添加必要的填充
|
||||
images_base64 += '=' * (4 - len(images_base64) % 4)
|
||||
logger.info(f"为base64字符串添加了填充,最终长度: {len(images_base64)}")
|
||||
|
||||
logger.info(f"准备解码base64数据,长度: {len(images_base64)}, 前20字符: {images_base64[:20]}...")
|
||||
|
||||
# 解码base64
|
||||
image_bytes = base64.b64decode(images_base64)
|
||||
logger.info(f"base64解码成功,图片数据大小: {len(image_bytes)} bytes")
|
||||
|
||||
# 验证解码后的数据不为空
|
||||
if len(image_bytes) == 0:
|
||||
raise ValueError("解码后的图片数据为空")
|
||||
|
||||
# 检查文件头判断图片格式
|
||||
file_header = image_bytes[:10]
|
||||
if file_header.startswith(b'\xff\xd8\xff'):
|
||||
logger.info("检测到JPEG格式图片")
|
||||
elif file_header.startswith(b'\x89PNG'):
|
||||
logger.info("检测到PNG格式图片")
|
||||
else:
|
||||
logger.warning(f"未识别的图片格式,文件头: {file_header.hex()}")
|
||||
|
||||
# 创建PIL Image对象
|
||||
images = Image.open(BytesIO(image_bytes))
|
||||
logger.info("图片解码成功")
|
||||
image_io = BytesIO(image_bytes)
|
||||
images = Image.open(image_io)
|
||||
|
||||
# 验证图片是否成功打开
|
||||
images.verify() # 验证图片完整性
|
||||
|
||||
# 重新打开图片(verify后需要重新打开)
|
||||
image_io.seek(0)
|
||||
images = Image.open(image_io)
|
||||
|
||||
logger.info(f"图片解码成功,格式: {images.format}, 尺寸: {images.size}, 模式: {images.mode}")
|
||||
|
||||
except binascii.Error as e:
|
||||
logger.error(f"Base64解码失败: {e}")
|
||||
logger.error(f"问题数据长度: {len(images_base64) if 'images_base64' in locals() else 'unknown'}")
|
||||
# 创建一个与目标大小一致的纯黑底图
|
||||
images = Image.new('RGB', template_size, color='black')
|
||||
logger.info(f"创建默认黑色背景图,尺寸: {template_size}")
|
||||
except Exception as e:
|
||||
logger.error(f"图片解码失败: {e}")
|
||||
logger.error(f"图片处理失败: {e}")
|
||||
logger.error(f"错误类型: {type(e).__name__}")
|
||||
if 'image_bytes' in locals():
|
||||
logger.error(f"图片数据大小: {len(image_bytes)} bytes, 前20字节: {image_bytes[:20].hex()}")
|
||||
# 创建一个与目标大小一致的纯黑底图
|
||||
images = Image.new('RGB', template_size, color='black')
|
||||
logger.info(f"创建默认黑色背景图,尺寸: {template_size}")
|
||||
@ -256,6 +314,7 @@ class PosterService:
|
||||
# 5. 保存海报并返回结果
|
||||
variations = []
|
||||
psd_files = []
|
||||
fabric_jsons = []
|
||||
i=0 ## 用于多个海报时,指定海报的编号,此时只有一个没有用上,但是接口开放着。
|
||||
output_path = self._save_poster(posters, template_id, i)
|
||||
if output_path:
|
||||
@ -279,6 +338,11 @@ class PosterService:
|
||||
if psd_result:
|
||||
psd_files.append(psd_result)
|
||||
|
||||
# 7. 如果需要,生成Fabric.js JSON
|
||||
if generate_fabric_json:
|
||||
fabric_json = self._generate_fabric_json(final_content, template_id, image_size, images)
|
||||
fabric_jsons.append(fabric_json)
|
||||
|
||||
# 记录模板使用情况
|
||||
self._update_template_stats(template_id, bool(variations), time.time() - start_time)
|
||||
|
||||
@ -287,11 +351,13 @@ class PosterService:
|
||||
"templateId": template_id,
|
||||
"resultImagesBase64": variations,
|
||||
"psdFiles": psd_files if psd_files else None,
|
||||
"fabricJsons": fabric_jsons if fabric_jsons else None,
|
||||
"metadata": {
|
||||
"generation_time": f"{time.time() - start_time:.2f}s",
|
||||
"model_used": self.ai_agent.config.model if force_llm_generation or not poster_content else None,
|
||||
"num_variations": len(variations),
|
||||
"psd_generated": bool(psd_files)
|
||||
"psd_generated": bool(psd_files),
|
||||
"fabric_json_generated": bool(fabric_jsons)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
@ -340,7 +406,8 @@ class PosterService:
|
||||
logger.warning(f"更新模板统计失败: {e}")
|
||||
|
||||
async def _generate_content_with_llm(self, template_id: str, content_id: Optional[str],
|
||||
product_id: Optional[str], scenic_spot_id: Optional[str]) -> Optional[Dict[str, Any]]:
|
||||
product_id: Optional[str], scenic_spot_id: Optional[str],
|
||||
poster_content: Optional[Any] = None) -> Optional[Dict[str, Any]]:
|
||||
"""使用LLM生成海报内容"""
|
||||
# 获取提示词 - 直接从数据库模板信息中获取
|
||||
template_info = self._templates.get(template_id, {})
|
||||
@ -482,7 +549,7 @@ class PosterService:
|
||||
content: Dict[str, Any], template_id: str,
|
||||
variation_id: int, custom_output_path: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
生成PSD分层文件
|
||||
生成Fabric.js JSON文件(保持接口兼容性,实际生成JSON而非PSD)
|
||||
|
||||
Args:
|
||||
template_handler: 模板处理器实例
|
||||
@ -493,84 +560,583 @@ class PosterService:
|
||||
custom_output_path: 自定义输出路径
|
||||
|
||||
Returns:
|
||||
PSD文件信息字典,包含文件路径、base64编码等
|
||||
JSON文件信息字典,包含文件路径、base64编码等
|
||||
"""
|
||||
try:
|
||||
# 检查模板是否支持PSD生成
|
||||
if not hasattr(template_handler, 'generate_layered_psd'):
|
||||
logger.warning(f"模板 {template_id} 不支持PSD分层输出")
|
||||
return None
|
||||
# 获取图像尺寸
|
||||
image_size = [images.width, images.height] if hasattr(images, 'width') else [900, 1200]
|
||||
|
||||
# 生成PSD文件路径
|
||||
# 生成Fabric.js JSON数据
|
||||
fabric_json = self._generate_fabric_json(content, template_id, image_size, images)
|
||||
|
||||
# 生成JSON文件路径
|
||||
if custom_output_path:
|
||||
psd_filename = custom_output_path
|
||||
if not psd_filename.endswith('.psd'):
|
||||
psd_filename += '.psd'
|
||||
json_filename = custom_output_path
|
||||
if not json_filename.endswith('.json'):
|
||||
json_filename += '.json'
|
||||
else:
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
psd_filename = f"{template_id}_layered_v{variation_id}_{timestamp}.psd"
|
||||
json_filename = f"{template_id}_fabric_v{variation_id}_{timestamp}.json"
|
||||
|
||||
# 获取输出目录
|
||||
topic_id = f"poster_{template_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
output_dir = self.output_manager.get_topic_dir(topic_id)
|
||||
psd_path = output_dir / psd_filename
|
||||
json_path = output_dir / json_filename
|
||||
|
||||
# 调用模板的PSD生成方法
|
||||
logger.info(f"开始生成PSD分层文件: {psd_path}")
|
||||
generated_psd_path = template_handler.generate_layered_psd(
|
||||
images=images,
|
||||
content=content,
|
||||
output_path=str(psd_path)
|
||||
)
|
||||
|
||||
if not generated_psd_path or not Path(generated_psd_path).exists():
|
||||
logger.error("PSD文件生成失败或文件不存在")
|
||||
# 保存JSON文件
|
||||
logger.info(f"开始生成Fabric.js JSON文件: {json_path}")
|
||||
try:
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(fabric_json, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"Fabric.js JSON文件保存成功: {json_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"保存JSON文件失败: {e}")
|
||||
return None
|
||||
|
||||
# 获取文件信息
|
||||
file_size = Path(generated_psd_path).stat().st_size
|
||||
file_size = Path(json_path).stat().st_size
|
||||
|
||||
# 可选:生成PSD的base64编码(注意:PSD文件通常较大)
|
||||
psd_base64 = None
|
||||
if file_size < 10 * 1024 * 1024: # 如果文件小于10MB,才生成base64
|
||||
try:
|
||||
with open(generated_psd_path, 'rb') as f:
|
||||
psd_base64 = base64.b64encode(f.read()).decode('utf-8')
|
||||
except Exception as e:
|
||||
logger.warning(f"生成PSD文件base64编码失败: {e}")
|
||||
|
||||
# 生成预览图(从PSD合成PNG预览)
|
||||
preview_base64 = None
|
||||
# 生成JSON的base64编码
|
||||
json_base64 = None
|
||||
try:
|
||||
from psd_tools import PSDImage
|
||||
psd = PSDImage.open(generated_psd_path)
|
||||
preview_image = psd.composite()
|
||||
if preview_image:
|
||||
preview_base64 = self._image_to_base64(preview_image)
|
||||
logger.info("PSD预览图生成成功")
|
||||
json_string = json.dumps(fabric_json, ensure_ascii=False)
|
||||
json_base64 = base64.b64encode(json_string.encode('utf-8')).decode('utf-8')
|
||||
except Exception as e:
|
||||
logger.warning(f"生成PSD预览图失败: {e}")
|
||||
logger.warning(f"生成JSON base64编码失败: {e}")
|
||||
|
||||
logger.info(f"PSD文件生成成功: {generated_psd_path} ({file_size/1024:.1f}KB)")
|
||||
logger.info(f"Fabric.js JSON文件生成成功: {json_path} ({file_size/1024:.1f}KB)")
|
||||
|
||||
return {
|
||||
"id": f"{template_id}_v{variation_id}_psd",
|
||||
"filename": psd_filename,
|
||||
"data": psd_base64,
|
||||
"id": f"{template_id}_v{variation_id}_fabric",
|
||||
"filename": json_filename,
|
||||
"data": json_base64,
|
||||
"size": file_size,
|
||||
"format": "PSD"
|
||||
"format": "JSON",
|
||||
"json_data": fabric_json # 添加原始JSON数据
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成PSD文件时发生错误: {e}", exc_info=True)
|
||||
logger.error(f"生成Fabric.js JSON文件时发生错误: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _get_psd_layer_count(self, psd_path: str) -> Optional[int]:
|
||||
"""获取PSD文件的图层数量"""
|
||||
def _get_json_object_count(self, json_path: str) -> Optional[int]:
|
||||
"""获取Fabric.js JSON文件的对象数量"""
|
||||
try:
|
||||
from psd_tools import PSDImage
|
||||
psd = PSDImage.open(psd_path)
|
||||
return len(list(psd))
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
fabric_data = json.load(f)
|
||||
return len(fabric_data.get('objects', []))
|
||||
except Exception as e:
|
||||
logger.warning(f"获取PSD图层数量失败: {e}")
|
||||
return None
|
||||
logger.warning(f"获取JSON对象数量失败: {e}")
|
||||
return None
|
||||
|
||||
def _generate_fabric_json(self, content: Dict[str, Any], template_id: str, image_size: List[int], images: Image.Image = None) -> Dict[str, Any]:
|
||||
"""
|
||||
生成支持多级分层的Fabric.js JSON格式
|
||||
|
||||
Args:
|
||||
content: 海报内容数据
|
||||
template_id: 模板ID
|
||||
image_size: 图像尺寸 [width, height]
|
||||
images: 用户上传的图片
|
||||
|
||||
Returns:
|
||||
Dict: 支持多级分层的Fabric.js JSON格式数据
|
||||
"""
|
||||
try:
|
||||
fabric_objects = []
|
||||
|
||||
# 基础画布尺寸
|
||||
canvas_width, canvas_height = image_size[0], image_size[1]
|
||||
|
||||
# 1. 图片层组 (最底层 - 用户上传的图片)
|
||||
image_group = self._create_image_layer(images, canvas_width, canvas_height)
|
||||
fabric_objects.append(image_group)
|
||||
|
||||
# 2. 背景层组 (第二层)
|
||||
background_group = self._create_background_layer(canvas_width, canvas_height, template_id)
|
||||
fabric_objects.append(background_group)
|
||||
|
||||
# 3. 内容层组 (中间层)
|
||||
content_group = self._create_content_layer(content, canvas_width, canvas_height)
|
||||
fabric_objects.append(content_group)
|
||||
|
||||
# 4. 装饰层组 (顶层)
|
||||
decoration_group = self._create_decoration_layer(content, canvas_width, canvas_height)
|
||||
fabric_objects.append(decoration_group)
|
||||
|
||||
# 构建完整的Fabric.js JSON
|
||||
fabric_json = {
|
||||
"version": "5.3.0",
|
||||
"objects": fabric_objects,
|
||||
"background": "transparent",
|
||||
"backgroundImage": None,
|
||||
"overlayImage": None,
|
||||
"clipPath": None,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"viewportTransform": [1, 0, 0, 1, 0, 0],
|
||||
"backgroundVpt": True,
|
||||
"overlayVpt": True,
|
||||
"selection": True,
|
||||
"preserveObjectStacking": True,
|
||||
"snapAngle": 0,
|
||||
"snapThreshold": 10,
|
||||
"centeredScaling": False,
|
||||
"centeredRotation": True,
|
||||
"interactive": True,
|
||||
"skipTargetFind": False,
|
||||
"enableRetinaScaling": True,
|
||||
"imageSmoothingEnabled": True,
|
||||
"perPixelTargetFind": False,
|
||||
"targetFindTolerance": 0,
|
||||
"skipOffscreen": True,
|
||||
"includeDefaultValues": True
|
||||
}
|
||||
|
||||
logger.info(f"成功生成多级分层Fabric.js JSON,包含 {len(fabric_objects)} 个层组")
|
||||
return fabric_json
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成Fabric.js JSON失败: {e}")
|
||||
return {
|
||||
"version": "5.3.0",
|
||||
"objects": [],
|
||||
"background": "transparent",
|
||||
"width": image_size[0],
|
||||
"height": image_size[1]
|
||||
}
|
||||
|
||||
def _create_image_layer(self, images: Image.Image, canvas_width: int, canvas_height: int) -> Dict[str, Any]:
|
||||
"""创建图片层组(最底层)"""
|
||||
image_objects = []
|
||||
|
||||
if images and hasattr(images, 'width'):
|
||||
# 将PIL图像转换为base64以便在Fabric.js中使用
|
||||
image_base64 = self._image_to_base64(images)
|
||||
|
||||
# 计算图片的缩放比例,保持宽高比
|
||||
image_width, image_height = images.width, images.height
|
||||
scale_x = canvas_width / image_width
|
||||
scale_y = canvas_height / image_height
|
||||
scale = min(scale_x, scale_y) # 保持宽高比的适应缩放
|
||||
|
||||
# 计算居中位置
|
||||
scaled_width = image_width * scale
|
||||
scaled_height = image_height * scale
|
||||
left = (canvas_width - scaled_width) / 2
|
||||
top = (canvas_height - scaled_height) / 2
|
||||
|
||||
image_objects.append({
|
||||
"type": "image",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": left,
|
||||
"top": top,
|
||||
"width": image_width,
|
||||
"height": image_height,
|
||||
"scaleX": scale,
|
||||
"scaleY": scale,
|
||||
"angle": 0,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"src": f"data:image/png;base64,{image_base64}",
|
||||
"crossOrigin": "anonymous",
|
||||
"name": "user_uploaded_image",
|
||||
"data": {
|
||||
"type": "user_image",
|
||||
"replaceable": True,
|
||||
"original_size": [image_width, image_height],
|
||||
"scale_ratio": scale
|
||||
},
|
||||
"selectable": True,
|
||||
"evented": True,
|
||||
"moveCursor": "move",
|
||||
"cornerStyle": "circle",
|
||||
"cornerSize": 12,
|
||||
"transparentCorners": False,
|
||||
"cornerColor": "#4dabf7",
|
||||
"cornerStrokeColor": "#ffffff",
|
||||
"borderColor": "#4dabf7",
|
||||
"borderScaleFactor": 2
|
||||
})
|
||||
else:
|
||||
# 如果没有图片,创建一个占位符
|
||||
image_objects.append({
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"fill": "#f8f9fa",
|
||||
"stroke": "#dee2e6",
|
||||
"strokeWidth": 2,
|
||||
"strokeDashArray": [10, 5],
|
||||
"name": "image_placeholder",
|
||||
"data": {
|
||||
"type": "placeholder",
|
||||
"replaceable": True,
|
||||
"placeholder_text": "点击上传图片"
|
||||
},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "group",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"angle": 0,
|
||||
"scaleX": 1,
|
||||
"scaleY": 1,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"name": "image_layer",
|
||||
"data": {"layer": "image", "level": 0, "replaceable": True},
|
||||
"objects": image_objects,
|
||||
"selectable": False,
|
||||
"evented": True
|
||||
}
|
||||
|
||||
def _create_background_layer(self, canvas_width: int, canvas_height: int, template_id: str) -> Dict[str, Any]:
|
||||
"""创建背景层组"""
|
||||
background_objects = []
|
||||
|
||||
# 主背景
|
||||
background_objects.append({
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"fill": "transparent",
|
||||
"stroke": None,
|
||||
"name": "main_background",
|
||||
"selectable": False,
|
||||
"evented": False
|
||||
})
|
||||
|
||||
# 渐变背景(可选)
|
||||
if template_id:
|
||||
background_objects.append({
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"fill": {
|
||||
"type": "linear",
|
||||
"coords": {
|
||||
"x1": 0,
|
||||
"y1": 0,
|
||||
"x2": 0,
|
||||
"y2": canvas_height
|
||||
},
|
||||
"colorStops": [
|
||||
{"offset": 0, "color": "#ffffff", "opacity": 0.8},
|
||||
{"offset": 1, "color": "#f8f9fa", "opacity": 0.9}
|
||||
]
|
||||
},
|
||||
"name": "gradient_background",
|
||||
"selectable": False,
|
||||
"evented": False
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "group",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"angle": 0,
|
||||
"scaleX": 1,
|
||||
"scaleY": 1,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"name": "background_layer",
|
||||
"data": {"layer": "background", "level": 1},
|
||||
"objects": background_objects,
|
||||
"selectable": False,
|
||||
"evented": False
|
||||
}
|
||||
|
||||
def _create_content_layer(self, content: Dict[str, Any], canvas_width: int, canvas_height: int) -> Dict[str, Any]:
|
||||
"""创建内容层组,包含多个子分层"""
|
||||
content_objects = []
|
||||
|
||||
# 标题组
|
||||
title_group = self._create_title_group(content, canvas_width)
|
||||
if title_group["objects"]:
|
||||
content_objects.append(title_group)
|
||||
|
||||
# 正文组
|
||||
body_group = self._create_body_group(content, canvas_width)
|
||||
if body_group["objects"]:
|
||||
content_objects.append(body_group)
|
||||
|
||||
# 价格信息组
|
||||
price_group = self._create_price_group(content, canvas_width)
|
||||
if price_group["objects"]:
|
||||
content_objects.append(price_group)
|
||||
|
||||
return {
|
||||
"type": "group",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"angle": 0,
|
||||
"scaleX": 1,
|
||||
"scaleY": 1,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"name": "content_layer",
|
||||
"data": {"layer": "content", "level": 2},
|
||||
"objects": content_objects
|
||||
}
|
||||
|
||||
def _create_title_group(self, content: Dict[str, Any], canvas_width: int) -> Dict[str, Any]:
|
||||
"""创建标题分组"""
|
||||
title_objects = []
|
||||
|
||||
# 主标题
|
||||
if content.get('title'):
|
||||
title_objects.append({
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 50,
|
||||
"top": 80,
|
||||
"width": canvas_width - 100,
|
||||
"height": 80,
|
||||
"fill": "#2c3e50",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "bold",
|
||||
"fontSize": 48,
|
||||
"text": str(content['title']),
|
||||
"textAlign": "center",
|
||||
"name": "main_title",
|
||||
"data": {"type": "title", "priority": "high"}
|
||||
})
|
||||
|
||||
# 副标题
|
||||
if content.get('slogan'):
|
||||
title_objects.append({
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 50,
|
||||
"top": 170,
|
||||
"width": canvas_width - 100,
|
||||
"height": 40,
|
||||
"fill": "#7f8c8d",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "normal",
|
||||
"fontSize": 24,
|
||||
"text": str(content['slogan']),
|
||||
"textAlign": "center",
|
||||
"name": "subtitle",
|
||||
"data": {"type": "subtitle", "priority": "medium"}
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "group",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": 250,
|
||||
"name": "title_group",
|
||||
"data": {"section": "title", "level": 3},
|
||||
"objects": title_objects
|
||||
}
|
||||
|
||||
def _create_body_group(self, content: Dict[str, Any], canvas_width: int) -> Dict[str, Any]:
|
||||
"""创建正文分组"""
|
||||
body_objects = []
|
||||
|
||||
if content.get('content'):
|
||||
text_content = content['content']
|
||||
if isinstance(text_content, list):
|
||||
text_content = '\n'.join(text_content)
|
||||
|
||||
body_objects.append({
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 50,
|
||||
"top": 280,
|
||||
"width": canvas_width - 100,
|
||||
"height": 120,
|
||||
"fill": "#34495e",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "normal",
|
||||
"fontSize": 18,
|
||||
"text": str(text_content),
|
||||
"textAlign": "left",
|
||||
"lineHeight": 1.4,
|
||||
"name": "main_content",
|
||||
"data": {"type": "content", "priority": "medium"}
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "group",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 250,
|
||||
"width": canvas_width,
|
||||
"height": 150,
|
||||
"name": "body_group",
|
||||
"data": {"section": "body", "level": 3},
|
||||
"objects": body_objects
|
||||
}
|
||||
|
||||
def _create_price_group(self, content: Dict[str, Any], canvas_width: int) -> Dict[str, Any]:
|
||||
"""创建价格信息分组"""
|
||||
price_objects = []
|
||||
|
||||
if content.get('price'):
|
||||
# 价格背景
|
||||
price_objects.append({
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 40,
|
||||
"top": 420,
|
||||
"width": canvas_width - 80,
|
||||
"height": 80,
|
||||
"fill": "#e74c3c",
|
||||
"rx": 10,
|
||||
"ry": 10,
|
||||
"name": "price_background"
|
||||
})
|
||||
|
||||
# 价格文字
|
||||
price_objects.append({
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 50,
|
||||
"top": 440,
|
||||
"width": canvas_width - 100,
|
||||
"height": 40,
|
||||
"fill": "#ffffff",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "bold",
|
||||
"fontSize": 36,
|
||||
"text": str(content['price']),
|
||||
"textAlign": "center",
|
||||
"name": "price_text",
|
||||
"data": {"type": "price", "priority": "high"}
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "group",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 400,
|
||||
"width": canvas_width,
|
||||
"height": 100,
|
||||
"name": "price_group",
|
||||
"data": {"section": "price", "level": 3},
|
||||
"objects": price_objects
|
||||
}
|
||||
|
||||
def _create_decoration_layer(self, content: Dict[str, Any], canvas_width: int, canvas_height: int) -> Dict[str, Any]:
|
||||
"""创建装饰层组"""
|
||||
decoration_objects = []
|
||||
|
||||
# 装饰边框
|
||||
decoration_objects.append({
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 20,
|
||||
"top": 20,
|
||||
"width": canvas_width - 40,
|
||||
"height": canvas_height - 40,
|
||||
"fill": "transparent",
|
||||
"stroke": "#3498db",
|
||||
"strokeWidth": 3,
|
||||
"strokeDashArray": [10, 5],
|
||||
"name": "decoration_border",
|
||||
"selectable": False
|
||||
})
|
||||
|
||||
# 角落装饰
|
||||
decoration_objects.append({
|
||||
"type": "circle",
|
||||
"version": "5.3.0",
|
||||
"originX": "center",
|
||||
"originY": "center",
|
||||
"left": canvas_width - 50,
|
||||
"top": 50,
|
||||
"radius": 20,
|
||||
"fill": "#f39c12",
|
||||
"name": "corner_decoration",
|
||||
"selectable": False
|
||||
})
|
||||
|
||||
return {
|
||||
"type": "group",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"angle": 0,
|
||||
"scaleX": 1,
|
||||
"scaleY": 1,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"name": "decoration_layer",
|
||||
"data": {"layer": "decoration", "level": 3},
|
||||
"objects": decoration_objects,
|
||||
"selectable": False
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user