同步了fabric and vibrant
This commit is contained in:
parent
d15a72e489
commit
610404d60f
@ -213,6 +213,7 @@ class PosterService:
|
||||
|
||||
# 2. 准备内容 (LLM或用户提供)
|
||||
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, poster_content)
|
||||
@ -283,22 +284,22 @@ class PosterService:
|
||||
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}")
|
||||
# 创建一个与目标大小一致的透明底图
|
||||
images = Image.new('RGBA', template_size, color=(0, 0, 0, 0))
|
||||
logger.info(f"创建默认透明背景图,尺寸: {template_size}")
|
||||
except Exception as 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}")
|
||||
# 创建一个与目标大小一致的透明底图
|
||||
images = Image.new('RGBA', template_size, color=(0, 0, 0, 0))
|
||||
logger.info(f"创建默认透明背景图,尺寸: {template_size}")
|
||||
else:
|
||||
logger.warning("未提供图片数据,使用默认黑色背景图")
|
||||
# 创建一个与目标大小一致的纯黑底图
|
||||
images = Image.new('RGB', template_size, color='black')
|
||||
logger.info(f"创建默认黑色背景图,尺寸: {template_size}")
|
||||
logger.warning("未提供图片数据,使用默认透明背景图")
|
||||
# 创建一个与目标大小一致的透明底图
|
||||
images = Image.new('RGBA', template_size, color=(0, 0, 0, 0))
|
||||
logger.info(f"创建默认透明背景图,尺寸: {template_size}")
|
||||
|
||||
# 4. 调用模板生成海报
|
||||
try:
|
||||
@ -425,36 +426,78 @@ class PosterService:
|
||||
data = {}
|
||||
|
||||
def safe_int_convert(id_str: Optional[str]) -> Optional[int]:
|
||||
"""安全将字符串ID转换为整数"""
|
||||
"""安全将字符串ID转换为整数,避免大整数精度丢失"""
|
||||
if not id_str:
|
||||
return None
|
||||
try:
|
||||
# 去除前后空格
|
||||
id_str = id_str.strip()
|
||||
|
||||
# 如果ID包含非数字字符,只提取数字部分或返回None
|
||||
if id_str.isdigit():
|
||||
return int(id_str)
|
||||
# 直接转换纯数字字符串,避免精度丢失
|
||||
converted_id = int(id_str)
|
||||
logger.debug(f"成功转换ID: {id_str} -> {converted_id}")
|
||||
return converted_id
|
||||
else:
|
||||
# 对于类似 "generated_note_1753693091224_0" 的ID,提取数字部分
|
||||
import re
|
||||
numbers = re.findall(r'\d+', id_str)
|
||||
if numbers:
|
||||
return int(numbers[0]) # 使用第一个数字序列
|
||||
# 使用第一个数字序列,但要验证它是有效的大整数
|
||||
first_number = numbers[0]
|
||||
converted_id = int(first_number)
|
||||
logger.debug(f"从复合ID中提取数字: {id_str} -> {converted_id}")
|
||||
return converted_id
|
||||
logger.warning(f"无法从ID中提取有效数字: {id_str}")
|
||||
return None
|
||||
except (ValueError, TypeError):
|
||||
logger.warning(f"无法转换ID为整数: {id_str}")
|
||||
except (ValueError, TypeError, OverflowError) as e:
|
||||
logger.error(f"无法转换ID为整数: {id_str}, 类型: {type(id_str)}, 错误: {e}")
|
||||
return None
|
||||
|
||||
# 添加详细的数据获取调试信息
|
||||
logger.info(f"开始获取数据 - content_id: {content_id}, product_id: {product_id}, scenic_spot_id: {scenic_spot_id}")
|
||||
|
||||
if content_id:
|
||||
content_id_int = safe_int_convert(content_id)
|
||||
if content_id_int:
|
||||
data['content'] = self.db_service.get_content_by_id(content_id_int)
|
||||
logger.info(f"处理content_id: {content_id} (类型: {type(content_id)})")
|
||||
# content_id直接用字符串查询,不需要转换
|
||||
content_data = self.db_service.get_content_by_id(content_id)
|
||||
logger.info(f"从数据库获取的content数据: {content_data is not None}")
|
||||
if content_data:
|
||||
logger.info(f"content数据预览: {list(content_data.keys()) if isinstance(content_data, dict) else type(content_data)}")
|
||||
data['content'] = content_data
|
||||
else:
|
||||
logger.info("未提供content_id")
|
||||
|
||||
if product_id:
|
||||
logger.info(f"处理product_id: {product_id} (类型: {type(product_id)})")
|
||||
product_id_int = safe_int_convert(product_id)
|
||||
logger.info(f"转换后的product_id_int: {product_id_int}")
|
||||
if product_id_int:
|
||||
data['product'] = self.db_service.get_product_by_id(product_id_int)
|
||||
product_data = self.db_service.get_product_by_id(product_id_int)
|
||||
logger.info(f"从数据库获取的product数据: {product_data is not None}")
|
||||
if product_data:
|
||||
logger.info(f"product数据预览: {list(product_data.keys()) if isinstance(product_data, dict) else type(product_data)}")
|
||||
data['product'] = product_data
|
||||
else:
|
||||
logger.warning(f"product_id转换失败: {product_id}")
|
||||
else:
|
||||
logger.info("未提供product_id")
|
||||
|
||||
if scenic_spot_id:
|
||||
logger.info(f"处理scenic_spot_id: {scenic_spot_id} (类型: {type(scenic_spot_id)})")
|
||||
scenic_spot_id_int = safe_int_convert(scenic_spot_id)
|
||||
logger.info(f"转换后的scenic_spot_id_int: {scenic_spot_id_int}")
|
||||
if scenic_spot_id_int:
|
||||
data['scenic_spot'] = self.db_service.get_scenic_spot_by_id(scenic_spot_id_int)
|
||||
scenic_spot_data = self.db_service.get_scenic_spot_by_id(scenic_spot_id_int)
|
||||
logger.info(f"从数据库获取的scenic_spot数据: {scenic_spot_data is not None}")
|
||||
if scenic_spot_data:
|
||||
logger.info(f"scenic_spot数据预览: {list(scenic_spot_data.keys()) if isinstance(scenic_spot_data, dict) else type(scenic_spot_data)}")
|
||||
data['scenic_spot'] = scenic_spot_data
|
||||
else:
|
||||
logger.warning(f"scenic_spot_id转换失败: {scenic_spot_id}")
|
||||
else:
|
||||
logger.info("未提供scenic_spot_id")
|
||||
|
||||
logger.info(f"获取到的数据: content={data.get('content') is not None}, product={data.get('product') is not None}, scenic_spot={data.get('scenic_spot') is not None}")
|
||||
|
||||
@ -489,14 +532,23 @@ class PosterService:
|
||||
详细描述: {product.get('detailedDescription', '')}"""
|
||||
logger.info("产品信息格式化完成")
|
||||
|
||||
# 内容信息格式化
|
||||
# 内容信息格式化 - 优先使用poster_content
|
||||
tweet_info = "无相关内容信息"
|
||||
if data.get('content'):
|
||||
logger.info("正在格式化内容信息...")
|
||||
if poster_content is not None:
|
||||
logger.info(f"使用poster_content作为文章内容,类型: {type(poster_content)}")
|
||||
if isinstance(poster_content, str):
|
||||
tweet_info = f"文章内容: {poster_content}"
|
||||
elif isinstance(poster_content, dict):
|
||||
tweet_info = f"文章内容: {str(poster_content)}"
|
||||
else:
|
||||
tweet_info = f"文章内容: {str(poster_content)}"
|
||||
logger.info(f"poster_content格式化完成,长度: {len(tweet_info)}")
|
||||
elif data.get('content'):
|
||||
logger.info("正在格式化数据库内容信息...")
|
||||
content = data['content']
|
||||
tweet_info = f"""标题: {content.get('title', '')}
|
||||
内容: {content.get('content', '')}"""
|
||||
logger.info("内容信息格式化完成")
|
||||
logger.info("数据库内容信息格式化完成")
|
||||
|
||||
logger.info("开始构建用户提示词...")
|
||||
# 构建用户提示词
|
||||
@ -508,6 +560,15 @@ class PosterService:
|
||||
|
||||
logger.info(f"用户提示词构建成功,长度: {len(user_prompt)}")
|
||||
|
||||
# 输出系统提示词和用户提示词内容以供调试
|
||||
logger.info("=" * 80)
|
||||
logger.info("系统提示词内容:")
|
||||
logger.info(system_prompt)
|
||||
logger.info("=" * 80)
|
||||
logger.info("用户提示词内容:")
|
||||
logger.info(user_prompt)
|
||||
logger.info("=" * 80)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"格式化提示词时发生错误: {e}", exc_info=True)
|
||||
# 提供兜底方案
|
||||
@ -627,11 +688,11 @@ class PosterService:
|
||||
return len(fabric_data.get('objects', []))
|
||||
except Exception as 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格式(不使用group,每个对象独立成层)
|
||||
生成与VibrantTemplate视觉效果一致的Fabric.js JSON格式
|
||||
|
||||
Args:
|
||||
content: 海报内容数据
|
||||
@ -645,31 +706,30 @@ class PosterService:
|
||||
try:
|
||||
fabric_objects = []
|
||||
|
||||
# 基础画布尺寸
|
||||
# 基础画布尺寸(VibrantTemplate使用900x1200,最终resize到1350x1800)
|
||||
canvas_width, canvas_height = image_size[0], image_size[1]
|
||||
|
||||
# 计算缩放比例以匹配VibrantTemplate的最终输出
|
||||
scale_ratio = min(canvas_width / 900, canvas_height / 1200)
|
||||
|
||||
# 1. 用户上传的图片(最底层 - Level 0)
|
||||
if images and hasattr(images, 'width'):
|
||||
image_object = self._create_image_object(images, canvas_width, canvas_height)
|
||||
image_object = self._create_vibrant_image_object(images, canvas_width, canvas_height)
|
||||
fabric_objects.append(image_object)
|
||||
else:
|
||||
# 占位符
|
||||
placeholder_object = self._create_placeholder_object(canvas_width, canvas_height)
|
||||
fabric_objects.append(placeholder_object)
|
||||
|
||||
# 2. 背景层(Level 1)
|
||||
if template_id:
|
||||
background_object = self._create_background_object(canvas_width, canvas_height)
|
||||
fabric_objects.append(background_object)
|
||||
# 2. 毛玻璃渐变背景(Level 1)
|
||||
gradient_start = self._calculate_gradient_start_position(canvas_height)
|
||||
gradient_object = self._create_gradient_background_object(canvas_width, canvas_height, gradient_start)
|
||||
fabric_objects.append(gradient_object)
|
||||
|
||||
# 3. 内容文字层(Level 2)
|
||||
text_objects = self._create_text_objects(content, canvas_width, canvas_height)
|
||||
# 3. VibrantTemplate风格的文字布局(Level 2)
|
||||
text_objects = self._create_vibrant_text_objects(content, canvas_width, canvas_height, gradient_start, scale_ratio)
|
||||
fabric_objects.extend(text_objects)
|
||||
|
||||
# 4. 装饰层(Level 3)
|
||||
decoration_objects = self._create_decoration_objects(canvas_width, canvas_height)
|
||||
fabric_objects.extend(decoration_objects)
|
||||
|
||||
# 构建完整的Fabric.js JSON
|
||||
fabric_json = {
|
||||
"version": "5.3.0",
|
||||
@ -763,23 +823,23 @@ class PosterService:
|
||||
def _create_placeholder_object(self, canvas_width: int, canvas_height: int) -> Dict[str, Any]:
|
||||
"""创建图片占位符对象"""
|
||||
return {
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": canvas_width,
|
||||
"height": canvas_height,
|
||||
"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],
|
||||
"angle": 0,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"angle": 0,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"name": "image_placeholder",
|
||||
"data": {
|
||||
"type": "placeholder",
|
||||
@ -834,21 +894,21 @@ class PosterService:
|
||||
}
|
||||
|
||||
for key, config in text_configs.items():
|
||||
if key in content and content[key]:
|
||||
text_content = content[key]
|
||||
if isinstance(text_content, list):
|
||||
text_content = '\n'.join(text_content)
|
||||
elif not isinstance(text_content, str):
|
||||
text_content = str(text_content)
|
||||
|
||||
if key in content and content[key]:
|
||||
text_content = content[key]
|
||||
if isinstance(text_content, list):
|
||||
text_content = '\n'.join(text_content)
|
||||
elif not isinstance(text_content, str):
|
||||
text_content = str(text_content)
|
||||
|
||||
text_object = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 50,
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 50,
|
||||
"top": config['top'],
|
||||
"width": canvas_width - 100,
|
||||
"width": canvas_width - 100,
|
||||
"height": 60 if key != 'content' else 120,
|
||||
"fill": config['fill'],
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
@ -934,7 +994,433 @@ class PosterService:
|
||||
},
|
||||
"selectable": False,
|
||||
"evented": False
|
||||
}
|
||||
}
|
||||
decoration_objects.append(corner_object)
|
||||
|
||||
return decoration_objects
|
||||
return decoration_objects
|
||||
|
||||
def _calculate_gradient_start_position(self, canvas_height: int) -> int:
|
||||
"""计算毛玻璃渐变开始位置(模拟VibrantTemplate的逻辑)"""
|
||||
# VibrantTemplate中大约从画布的60%位置开始渐变
|
||||
return int(canvas_height * 0.6)
|
||||
|
||||
def _create_vibrant_image_object(self, images: Image.Image, canvas_width: int, canvas_height: int) -> Dict[str, Any]:
|
||||
"""创建VibrantTemplate风格的图片对象"""
|
||||
# 将PIL图像转换为base64
|
||||
image_base64 = self._image_to_base64(images)
|
||||
|
||||
# VibrantTemplate将图片resize到画布大小
|
||||
return {
|
||||
"type": "image",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": images.width,
|
||||
"height": images.height,
|
||||
"scaleX": canvas_width / images.width,
|
||||
"scaleY": canvas_height / images.height,
|
||||
"angle": 0,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"src": f"data:image/png;base64,{image_base64}",
|
||||
"crossOrigin": "anonymous",
|
||||
"name": "background_image",
|
||||
"data": {
|
||||
"type": "background_image",
|
||||
"layer": "image",
|
||||
"level": 0,
|
||||
"replaceable": True
|
||||
},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
|
||||
def _create_gradient_background_object(self, canvas_width: int, canvas_height: int, gradient_start: int) -> Dict[str, Any]:
|
||||
"""创建模拟毛玻璃效果的渐变背景"""
|
||||
return {
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"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": "rgba(0, 30, 80, 0.3)"},
|
||||
{"offset": 0.5, "color": "rgba(0, 50, 120, 0.6)"},
|
||||
{"offset": 1, "color": "rgba(0, 80, 160, 0.8)"}
|
||||
]
|
||||
},
|
||||
"angle": 0,
|
||||
"flipX": False,
|
||||
"flipY": False,
|
||||
"opacity": 1,
|
||||
"visible": True,
|
||||
"name": "glass_gradient",
|
||||
"data": {
|
||||
"type": "glass_effect",
|
||||
"layer": "background",
|
||||
"level": 1
|
||||
},
|
||||
"selectable": False,
|
||||
"evented": False
|
||||
}
|
||||
|
||||
def _create_vibrant_text_objects(self, content: Dict[str, Any], canvas_width: int, canvas_height: int, gradient_start: int, scale_ratio: float) -> List[Dict[str, Any]]:
|
||||
"""创建VibrantTemplate风格的文字对象"""
|
||||
text_objects = []
|
||||
|
||||
# 计算内容区域边距(模拟VibrantTemplate的计算)
|
||||
content_margin = max(40, int(canvas_width * 0.1))
|
||||
content_width = canvas_width - 2 * content_margin
|
||||
|
||||
# 标题位置和样式
|
||||
title_y = gradient_start + int(40 * scale_ratio)
|
||||
if title := content.get("title"):
|
||||
title_size = self._calculate_vibrant_font_size(title, content_width * 0.95, 40, 140, scale_ratio)
|
||||
title_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "center",
|
||||
"originY": "top",
|
||||
"left": canvas_width / 2,
|
||||
"top": title_y,
|
||||
"width": content_width,
|
||||
"height": title_size + 20,
|
||||
"fill": "#ffffff",
|
||||
"stroke": "rgba(0, 30, 80, 0.8)",
|
||||
"strokeWidth": 2,
|
||||
"fontFamily": "Arial Black, sans-serif",
|
||||
"fontWeight": "bold",
|
||||
"fontSize": title_size,
|
||||
"text": title,
|
||||
"textAlign": "center",
|
||||
"lineHeight": 1.1,
|
||||
"name": "vibrant_title",
|
||||
"data": {
|
||||
"type": "title",
|
||||
"layer": "content",
|
||||
"level": 2,
|
||||
"style": "vibrant_title"
|
||||
},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
text_objects.append(title_obj)
|
||||
|
||||
# 副标题位置和样式
|
||||
subtitle_y = title_y + int(100 * scale_ratio)
|
||||
if slogan := content.get("slogan"):
|
||||
subtitle_size = self._calculate_vibrant_font_size(slogan, content_width * 0.9, 20, 75, scale_ratio)
|
||||
subtitle_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "center",
|
||||
"originY": "top",
|
||||
"left": canvas_width / 2,
|
||||
"top": subtitle_y,
|
||||
"width": content_width,
|
||||
"height": subtitle_size + 15,
|
||||
"fill": "#ffffff",
|
||||
"shadow": {
|
||||
"color": "rgba(0, 0, 0, 0.7)",
|
||||
"blur": 5,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2
|
||||
},
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "normal",
|
||||
"fontSize": subtitle_size,
|
||||
"text": slogan,
|
||||
"textAlign": "center",
|
||||
"lineHeight": 1.2,
|
||||
"name": "vibrant_slogan",
|
||||
"data": {
|
||||
"type": "slogan",
|
||||
"layer": "content",
|
||||
"level": 2,
|
||||
"style": "vibrant_subtitle"
|
||||
},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
text_objects.append(subtitle_obj)
|
||||
|
||||
# 双栏布局
|
||||
column_start_y = subtitle_y + int(80 * scale_ratio)
|
||||
left_column_width = int(content_width * 0.5)
|
||||
right_column_x = content_margin + left_column_width
|
||||
|
||||
# 左栏:内容按钮和项目列表
|
||||
left_objects = self._create_left_column_objects(content, content_margin, column_start_y, left_column_width, scale_ratio)
|
||||
text_objects.extend(left_objects)
|
||||
|
||||
# 右栏:价格和票种信息
|
||||
right_objects = self._create_right_column_objects(content, right_column_x, column_start_y, content_margin + content_width, scale_ratio)
|
||||
text_objects.extend(right_objects)
|
||||
|
||||
# 底部标签和分页
|
||||
footer_objects = self._create_footer_objects(content, content_margin, canvas_height - int(30 * scale_ratio), content_width, scale_ratio)
|
||||
text_objects.extend(footer_objects)
|
||||
|
||||
return text_objects
|
||||
|
||||
def _calculate_vibrant_font_size(self, text: str, target_width: int, min_size: int, max_size: int, scale_ratio: float) -> int:
|
||||
"""计算VibrantTemplate风格的字体大小"""
|
||||
# 简化的字体大小计算,基于文本长度和目标宽度
|
||||
char_count = len(text)
|
||||
if char_count == 0:
|
||||
return int(min_size * scale_ratio)
|
||||
|
||||
# 估算字符宽度(中文字符按1.5倍计算)
|
||||
avg_char_width = target_width / max(1, char_count * 1.2)
|
||||
estimated_font_size = int(avg_char_width * 0.8)
|
||||
|
||||
# 应用缩放比例并限制在范围内
|
||||
scaled_size = int(estimated_font_size * scale_ratio)
|
||||
return max(int(min_size * scale_ratio), min(int(max_size * scale_ratio), scaled_size))
|
||||
|
||||
def _create_left_column_objects(self, content: Dict[str, Any], x: int, y: int, width: int, scale_ratio: float) -> List[Dict[str, Any]]:
|
||||
"""创建左栏对象:按钮和项目列表"""
|
||||
objects = []
|
||||
|
||||
# 内容按钮
|
||||
button_text = content.get("content_button", "套餐内容")
|
||||
button_width = min(width - 20, int(len(button_text) * 20 * scale_ratio + 40))
|
||||
button_height = int(50 * scale_ratio)
|
||||
|
||||
# 按钮背景
|
||||
button_bg = {
|
||||
"type": "rect",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": x,
|
||||
"top": y,
|
||||
"width": button_width,
|
||||
"height": button_height,
|
||||
"fill": "rgba(0, 140, 210, 0.7)",
|
||||
"stroke": "#ffffff",
|
||||
"strokeWidth": 1,
|
||||
"rx": 20,
|
||||
"ry": 20,
|
||||
"name": "content_button_bg",
|
||||
"data": {"type": "button", "layer": "content", "level": 2},
|
||||
"selectable": False,
|
||||
"evented": False
|
||||
}
|
||||
objects.append(button_bg)
|
||||
|
||||
# 按钮文字
|
||||
button_text_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "center",
|
||||
"originY": "center",
|
||||
"left": x + button_width / 2,
|
||||
"top": y + button_height / 2,
|
||||
"width": button_width - 20,
|
||||
"height": button_height,
|
||||
"fill": "#ffffff",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "bold",
|
||||
"fontSize": int(30 * scale_ratio),
|
||||
"text": button_text,
|
||||
"textAlign": "center",
|
||||
"name": "content_button_text",
|
||||
"data": {"type": "button_text", "layer": "content", "level": 2},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
objects.append(button_text_obj)
|
||||
|
||||
# 项目列表
|
||||
items = content.get("content_items", [])
|
||||
list_y = y + button_height + int(20 * scale_ratio)
|
||||
font_size = int(28 * scale_ratio)
|
||||
line_spacing = int(36 * scale_ratio)
|
||||
|
||||
for i, item in enumerate(items):
|
||||
item_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": x,
|
||||
"top": list_y + i * line_spacing,
|
||||
"width": width,
|
||||
"height": font_size + 10,
|
||||
"fill": "#ffffff",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "normal",
|
||||
"fontSize": font_size,
|
||||
"text": f"• {item}",
|
||||
"textAlign": "left",
|
||||
"name": f"content_item_{i}",
|
||||
"data": {"type": "content_item", "layer": "content", "level": 2},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
objects.append(item_obj)
|
||||
|
||||
return objects
|
||||
|
||||
def _create_right_column_objects(self, content: Dict[str, Any], x: int, y: int, right_margin: int, scale_ratio: float) -> List[Dict[str, Any]]:
|
||||
"""创建右栏对象:价格和票种"""
|
||||
objects = []
|
||||
column_width = right_margin - x
|
||||
|
||||
# 价格
|
||||
if price := content.get("price"):
|
||||
price_size = self._calculate_vibrant_font_size(str(price), column_width * 0.7, 40, 120, scale_ratio)
|
||||
price_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "right",
|
||||
"originY": "top",
|
||||
"left": right_margin,
|
||||
"top": y,
|
||||
"width": column_width,
|
||||
"height": price_size + 20,
|
||||
"fill": "#ffffff",
|
||||
"shadow": {
|
||||
"color": "rgba(0, 0, 0, 0.7)",
|
||||
"blur": 5,
|
||||
"offsetX": 2,
|
||||
"offsetY": 2
|
||||
},
|
||||
"fontFamily": "Arial Black, sans-serif",
|
||||
"fontWeight": "bold",
|
||||
"fontSize": price_size,
|
||||
"text": f"¥{price}",
|
||||
"textAlign": "right",
|
||||
"name": "vibrant_price",
|
||||
"data": {"type": "price", "layer": "content", "level": 2},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
objects.append(price_obj)
|
||||
|
||||
# 价格下划线
|
||||
underline_y = y + price_size + int(18 * scale_ratio)
|
||||
underline = {
|
||||
"type": "line",
|
||||
"version": "5.3.0",
|
||||
"originX": "center",
|
||||
"originY": "center",
|
||||
"left": right_margin - column_width * 0.5,
|
||||
"top": underline_y,
|
||||
"x1": -column_width * 0.4,
|
||||
"y1": 0,
|
||||
"x2": column_width * 0.4,
|
||||
"y2": 0,
|
||||
"stroke": "rgba(255, 255, 255, 0.3)",
|
||||
"strokeWidth": 2,
|
||||
"name": "price_underline",
|
||||
"data": {"type": "decoration", "layer": "content", "level": 2},
|
||||
"selectable": False,
|
||||
"evented": False
|
||||
}
|
||||
objects.append(underline)
|
||||
|
||||
# 票种
|
||||
if ticket_type := content.get("ticket_type"):
|
||||
ticket_y = y + price_size + int(35 * scale_ratio)
|
||||
ticket_size = self._calculate_vibrant_font_size(ticket_type, column_width * 0.7, 30, 60, scale_ratio)
|
||||
ticket_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "right",
|
||||
"originY": "top",
|
||||
"left": right_margin,
|
||||
"top": ticket_y,
|
||||
"width": column_width,
|
||||
"height": ticket_size + 10,
|
||||
"fill": "#ffffff",
|
||||
"shadow": {
|
||||
"color": "rgba(0, 0, 0, 0.5)",
|
||||
"blur": 3,
|
||||
"offsetX": 1,
|
||||
"offsetY": 1
|
||||
},
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "normal",
|
||||
"fontSize": ticket_size,
|
||||
"text": ticket_type,
|
||||
"textAlign": "right",
|
||||
"name": "ticket_type",
|
||||
"data": {"type": "ticket_type", "layer": "content", "level": 2},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
objects.append(ticket_obj)
|
||||
|
||||
return objects
|
||||
|
||||
def _create_footer_objects(self, content: Dict[str, Any], x: int, y: int, width: int, scale_ratio: float) -> List[Dict[str, Any]]:
|
||||
"""创建底部对象:标签和分页"""
|
||||
objects = []
|
||||
font_size = int(18 * scale_ratio)
|
||||
|
||||
# 左侧标签
|
||||
if tag := content.get("tag"):
|
||||
tag_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "left",
|
||||
"originY": "top",
|
||||
"left": x,
|
||||
"top": y,
|
||||
"width": width // 2,
|
||||
"height": font_size + 5,
|
||||
"fill": "#ffffff",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "normal",
|
||||
"fontSize": font_size,
|
||||
"text": tag,
|
||||
"textAlign": "left",
|
||||
"name": "footer_tag",
|
||||
"data": {"type": "tag", "layer": "content", "level": 2},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
objects.append(tag_obj)
|
||||
|
||||
# 右侧分页
|
||||
if pagination := content.get("pagination"):
|
||||
pagination_obj = {
|
||||
"type": "textbox",
|
||||
"version": "5.3.0",
|
||||
"originX": "right",
|
||||
"originY": "top",
|
||||
"left": x + width,
|
||||
"top": y,
|
||||
"width": width // 2,
|
||||
"height": font_size + 5,
|
||||
"fill": "#ffffff",
|
||||
"fontFamily": "Arial, sans-serif",
|
||||
"fontWeight": "normal",
|
||||
"fontSize": font_size,
|
||||
"text": pagination,
|
||||
"textAlign": "right",
|
||||
"name": "footer_pagination",
|
||||
"data": {"type": "pagination", "layer": "content", "level": 2},
|
||||
"selectable": True,
|
||||
"evented": True
|
||||
}
|
||||
objects.append(pagination_obj)
|
||||
|
||||
return objects
|
||||
Loading…
x
Reference in New Issue
Block a user