同步运行区

This commit is contained in:
jinye_huang 2025-08-04 11:23:50 +08:00
parent cf338c17a2
commit 5acbe8d7b7

View File

@ -13,6 +13,7 @@ import time
import json import json
import importlib import importlib
import base64 import base64
import binascii
from io import BytesIO from io import BytesIO
from typing import List, Dict, Any, Optional, Type, Union, cast from typing import List, Dict, Any, Optional, Type, Union, cast
from datetime import datetime from datetime import datetime
@ -163,14 +164,15 @@ class PosterService:
async def generate_poster(self, async def generate_poster(self,
template_id: str, template_id: str,
poster_content: Optional[Dict[str, Any]], poster_content: Optional[Dict[str, Any]],
content_id: Optional[int], content_id: Optional[str],
product_id: Optional[int], product_id: Optional[str],
scenic_spot_id: Optional[int], scenic_spot_id: Optional[str],
images_base64: Optional[List[str]] , images_base64: Optional[List[str]] ,
num_variations: int = 1, num_variations: int = 1,
force_llm_generation: bool = False, force_llm_generation: bool = False,
generate_psd: 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生成内容 force_llm_generation: 是否强制使用LLM生成内容
generate_psd: 是否生成PSD分层文件 generate_psd: 是否生成PSD分层文件
psd_output_path: PSD文件输出路径可选默认自动生成 psd_output_path: PSD文件输出路径可选默认自动生成
generate_fabric_json: 是否生成Fabric.js JSON格式
Returns: Returns:
生成结果字典包含PNG图像和可选的PSD文件 生成结果字典包含PNG图像和可选的PSD文件Fabric.js JSON
""" """
start_time = time.time() 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. 动态加载模板处理器 # 1. 动态加载模板处理器
template_handler = self._load_template_handler(template_id) template_handler = self._load_template_handler(template_id)
if not template_handler: if not template_handler:
@ -200,7 +215,7 @@ class PosterService:
final_content = poster_content final_content = poster_content
if force_llm_generation or not final_content: if force_llm_generation or not final_content:
logger.info(f"为模板 {template_id} 按需生成内容...") 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: if not final_content:
raise ValueError("无法获取用于生成海报的内容") raise ValueError("无法获取用于生成海报的内容")
@ -224,15 +239,58 @@ class PosterService:
if images_base64.startswith("data:"): if images_base64.startswith("data:"):
images_base64 = images_base64.split(",", 1)[1] 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 # 解码base64
image_bytes = base64.b64decode(images_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对象 # 创建PIL Image对象
images = Image.open(BytesIO(image_bytes)) image_io = BytesIO(image_bytes)
logger.info("图片解码成功") 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: 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') images = Image.new('RGB', template_size, color='black')
logger.info(f"创建默认黑色背景图,尺寸: {template_size}") logger.info(f"创建默认黑色背景图,尺寸: {template_size}")
@ -256,6 +314,7 @@ class PosterService:
# 5. 保存海报并返回结果 # 5. 保存海报并返回结果
variations = [] variations = []
psd_files = [] psd_files = []
fabric_jsons = []
i=0 ## 用于多个海报时,指定海报的编号,此时只有一个没有用上,但是接口开放着。 i=0 ## 用于多个海报时,指定海报的编号,此时只有一个没有用上,但是接口开放着。
output_path = self._save_poster(posters, template_id, i) output_path = self._save_poster(posters, template_id, i)
if output_path: if output_path:
@ -279,6 +338,11 @@ class PosterService:
if psd_result: if psd_result:
psd_files.append(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) self._update_template_stats(template_id, bool(variations), time.time() - start_time)
@ -287,11 +351,13 @@ class PosterService:
"templateId": template_id, "templateId": template_id,
"resultImagesBase64": variations, "resultImagesBase64": variations,
"psdFiles": psd_files if psd_files else None, "psdFiles": psd_files if psd_files else None,
"fabricJsons": fabric_jsons if fabric_jsons else None,
"metadata": { "metadata": {
"generation_time": f"{time.time() - start_time:.2f}s", "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, "model_used": self.ai_agent.config.model if force_llm_generation or not poster_content else None,
"num_variations": len(variations), "num_variations": len(variations),
"psd_generated": bool(psd_files) "psd_generated": bool(psd_files),
"fabric_json_generated": bool(fabric_jsons)
} }
} }
except Exception as e: except Exception as e:
@ -340,7 +406,8 @@ class PosterService:
logger.warning(f"更新模板统计失败: {e}") logger.warning(f"更新模板统计失败: {e}")
async def _generate_content_with_llm(self, template_id: str, content_id: Optional[str], 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生成海报内容""" """使用LLM生成海报内容"""
# 获取提示词 - 直接从数据库模板信息中获取 # 获取提示词 - 直接从数据库模板信息中获取
template_info = self._templates.get(template_id, {}) template_info = self._templates.get(template_id, {})
@ -482,7 +549,7 @@ class PosterService:
content: Dict[str, Any], template_id: str, content: Dict[str, Any], template_id: str,
variation_id: int, custom_output_path: Optional[str] = None) -> Optional[Dict[str, Any]]: variation_id: int, custom_output_path: Optional[str] = None) -> Optional[Dict[str, Any]]:
""" """
生成PSD分层文件 生成Fabric.js JSON文件保持接口兼容性实际生成JSON而非PSD
Args: Args:
template_handler: 模板处理器实例 template_handler: 模板处理器实例
@ -493,84 +560,583 @@ class PosterService:
custom_output_path: 自定义输出路径 custom_output_path: 自定义输出路径
Returns: Returns:
PSD文件信息字典包含文件路径base64编码等 JSON文件信息字典包含文件路径base64编码等
""" """
try: try:
# 检查模板是否支持PSD生成 # 获取图像尺寸
if not hasattr(template_handler, 'generate_layered_psd'): image_size = [images.width, images.height] if hasattr(images, 'width') else [900, 1200]
logger.warning(f"模板 {template_id} 不支持PSD分层输出")
return None
# 生成PSD文件路径 # 生成Fabric.js JSON数据
fabric_json = self._generate_fabric_json(content, template_id, image_size, images)
# 生成JSON文件路径
if custom_output_path: if custom_output_path:
psd_filename = custom_output_path json_filename = custom_output_path
if not psd_filename.endswith('.psd'): if not json_filename.endswith('.json'):
psd_filename += '.psd' json_filename += '.json'
else: else:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') 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')}" 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) output_dir = self.output_manager.get_topic_dir(topic_id)
psd_path = output_dir / psd_filename json_path = output_dir / json_filename
# 调用模板的PSD生成方法 # 保存JSON文件
logger.info(f"开始生成PSD分层文件: {psd_path}") logger.info(f"开始生成Fabric.js JSON文件: {json_path}")
generated_psd_path = template_handler.generate_layered_psd( try:
images=images, with open(json_path, 'w', encoding='utf-8') as f:
content=content, json.dump(fabric_json, f, ensure_ascii=False, indent=2)
output_path=str(psd_path) logger.info(f"Fabric.js JSON文件保存成功: {json_path}")
) except Exception as e:
logger.error(f"保存JSON文件失败: {e}")
if not generated_psd_path or not Path(generated_psd_path).exists():
logger.error("PSD文件生成失败或文件不存在")
return None return None
# 获取文件信息 # 获取文件信息
file_size = Path(generated_psd_path).stat().st_size file_size = Path(json_path).stat().st_size
# 可选生成PSD的base64编码注意PSD文件通常较大 # 生成JSON的base64编码
psd_base64 = None json_base64 = None
if file_size < 10 * 1024 * 1024: # 如果文件小于10MB才生成base64
try: try:
with open(generated_psd_path, 'rb') as f: json_string = json.dumps(fabric_json, ensure_ascii=False)
psd_base64 = base64.b64encode(f.read()).decode('utf-8') json_base64 = base64.b64encode(json_string.encode('utf-8')).decode('utf-8')
except Exception as e: except Exception as e:
logger.warning(f"生成PSD文件base64编码失败: {e}") logger.warning(f"生成JSON base64编码失败: {e}")
# 生成预览图从PSD合成PNG预览 logger.info(f"Fabric.js JSON文件生成成功: {json_path} ({file_size/1024:.1f}KB)")
preview_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预览图生成成功")
except Exception as e:
logger.warning(f"生成PSD预览图失败: {e}")
logger.info(f"PSD文件生成成功: {generated_psd_path} ({file_size/1024:.1f}KB)")
return { return {
"id": f"{template_id}_v{variation_id}_psd", "id": f"{template_id}_v{variation_id}_fabric",
"filename": psd_filename, "filename": json_filename,
"data": psd_base64, "data": json_base64,
"size": file_size, "size": file_size,
"format": "PSD" "format": "JSON",
"json_data": fabric_json # 添加原始JSON数据
} }
except Exception as e: except Exception as e:
logger.error(f"生成PSD文件时发生错误: {e}", exc_info=True) logger.error(f"生成Fabric.js JSON文件时发生错误: {e}", exc_info=True)
return None return None
def _get_psd_layer_count(self, psd_path: str) -> Optional[int]: def _get_json_object_count(self, json_path: str) -> Optional[int]:
"""获取PSD文件的图层数量""" """获取Fabric.js JSON文件的对象数量"""
try: try:
from psd_tools import PSDImage with open(json_path, 'r', encoding='utf-8') as f:
psd = PSDImage.open(psd_path) fabric_data = json.load(f)
return len(list(psd)) return len(fabric_data.get('objects', []))
except Exception as e: except Exception as e:
logger.warning(f"获取PSD图层数量失败: {e}") logger.warning(f"获取JSON对象数量失败: {e}")
return None 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
}