复用了vibrant方法创建json

This commit is contained in:
jinye_huang 2025-08-04 13:02:08 +08:00
parent cdfa9a3699
commit b57c32a317

View File

@ -15,7 +15,7 @@ import importlib
import base64
import binascii
from io import BytesIO
from typing import List, Dict, Any, Optional, Type, Union, cast
from typing import List, Dict, Any, Optional, Type, Union, cast, Tuple
from datetime import datetime
from pathlib import Path
from PIL import Image
@ -1069,29 +1069,37 @@ class PosterService:
}
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风格的文字对象"""
"""创建VibrantTemplate风格的文字对象复用VibrantTemplate的精确计算"""
text_objects = []
# 计算内容区域边距模拟VibrantTemplate的计算
content_margin = max(40, int(canvas_width * 0.1))
content_width = canvas_width - 2 * content_margin
# 复用VibrantTemplate的边距计算逻辑
left_margin, right_margin = self._calculate_vibrant_content_margins(content, canvas_width, canvas_width // 2)
content_width = right_margin - left_margin
# 标题位置和样式
# 标题位置和样式使用VibrantTemplate的精确参数
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)
# 使用VibrantTemplate的精确计算目标宽度为内容区域的98%字体范围40-140
title_target_width = int(content_width * 0.98)
title_size, title_actual_width = self._calculate_vibrant_font_size_precise(
title, title_target_width, min_size=40, max_size=140
)
# 居中计算与VibrantTemplate一致
title_x = canvas_width // 2 - title_actual_width // 2
title_obj = {
"type": "textbox",
"version": "5.3.0",
"originX": "center",
"originX": "left", # 改为left使用计算的x位置
"originY": "top",
"left": canvas_width / 2,
"left": title_x,
"top": title_y,
"width": content_width,
"width": title_actual_width,
"height": title_size + 20,
"fill": "#ffffff",
"stroke": "#001e50",
"strokeWidth": 2,
"stroke": "#001e50",
"strokeWidth": 4, # 与VibrantTemplate一致的描边宽度
"fontFamily": "Arial Black, sans-serif",
"fontWeight": "bold",
"fontSize": title_size,
@ -1103,25 +1111,64 @@ class PosterService:
"type": "title",
"layer": "content",
"level": 2,
"style": "vibrant_title"
"style": "vibrant_title",
"target_width": title_target_width,
"actual_width": title_actual_width
},
"selectable": True,
"evented": True
}
text_objects.append(title_obj)
# 添加标题下方装饰线与VibrantTemplate一致
line_y = title_y + title_size + 5
line_start_x = title_x - title_actual_width * 0.025
line_end_x = title_x + title_actual_width * 1.025
decoration_line = {
"type": "line",
"version": "5.3.0",
"originX": "center",
"originY": "center",
"left": (line_start_x + line_end_x) / 2,
"top": line_y,
"x1": line_start_x - (line_start_x + line_end_x) / 2,
"y1": 0,
"x2": line_end_x - (line_start_x + line_end_x) / 2,
"y2": 0,
"stroke": "rgba(215, 215, 215, 0.3)",
"strokeWidth": 3,
"name": "title_decoration_line",
"data": {
"type": "decoration",
"layer": "content",
"level": 2
},
"selectable": False,
"evented": False
}
text_objects.append(decoration_line)
# 副标题位置和样式
# 副标题位置和样式使用VibrantTemplate的精确参数
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)
# 使用VibrantTemplate的精确计算目标宽度为内容区域的95%字体范围20-75
subtitle_target_width = int(content_width * 0.95)
subtitle_size, subtitle_actual_width = self._calculate_vibrant_font_size_precise(
slogan, subtitle_target_width, min_size=20, max_size=75
)
# 居中计算与VibrantTemplate一致
subtitle_x = canvas_width // 2 - subtitle_actual_width // 2
subtitle_obj = {
"type": "textbox",
"version": "5.3.0",
"originX": "center",
"originX": "left", # 改为left使用计算的x位置
"originY": "top",
"left": canvas_width / 2,
"left": subtitle_x,
"top": subtitle_y,
"width": content_width,
"width": subtitle_actual_width,
"height": subtitle_size + 15,
"fill": "#ffffff",
"shadow": "rgba(0, 0, 0, 0.7) 2px 2px 5px",
@ -1136,28 +1183,30 @@ class PosterService:
"type": "slogan",
"layer": "content",
"level": 2,
"style": "vibrant_subtitle"
"style": "vibrant_subtitle",
"target_width": subtitle_target_width,
"actual_width": subtitle_actual_width
},
"selectable": True,
"evented": True
}
text_objects.append(subtitle_obj)
# 双栏布局
# 双栏布局使用VibrantTemplate的精确边距
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
right_column_x = left_margin + left_column_width
# 左栏:内容按钮和项目列表
left_objects = self._create_left_column_objects(content, content_margin, column_start_y, left_column_width, scale_ratio)
left_objects = self._create_left_column_objects(content, left_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)
# 右栏:价格和票种信息使用VibrantTemplate的精确参数
right_objects = self._create_right_column_objects_precise(content, right_column_x, column_start_y, right_margin, 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)
footer_objects = self._create_footer_objects(content, left_margin, canvas_height - int(30 * scale_ratio), content_width, scale_ratio)
text_objects.extend(footer_objects)
return text_objects
@ -1397,4 +1446,242 @@ class PosterService:
}
objects.append(pagination_obj)
return objects
def _calculate_vibrant_content_margins(self, content: Dict[str, Any], width: int, center_x: int) -> Tuple[int, int]:
"""复用VibrantTemplate的边距计算逻辑"""
# 计算标题位置
title_text = content.get("title", "")
title_target_width = int(width * 0.95)
title_size, title_width = self._calculate_vibrant_font_size_precise(
title_text, title_target_width, min_size=40, max_size=130
)
title_x = center_x - title_width // 2
# 计算副标题位置
slogan_text = content.get("slogan", "")
subtitle_target_width = int(width * 0.9)
subtitle_size, subtitle_width = self._calculate_vibrant_font_size_precise(
slogan_text, subtitle_target_width, max_size=50, min_size=20
)
subtitle_x = center_x - subtitle_width // 2
# 计算内容区域边距 - 与VibrantTemplate一致
padding = 20
content_left_margin = min(title_x, subtitle_x) - padding
content_right_margin = max(title_x + title_width, subtitle_x + subtitle_width) + padding
# 确保边距不超出合理范围
content_left_margin = max(40, content_left_margin)
content_right_margin = min(width - 40, content_right_margin)
# 如果内容区域太窄,强制使用更宽的区域
min_content_width = int(width * 0.75) # 至少使用75%的宽度
current_width = content_right_margin - content_left_margin
if current_width < min_content_width:
extra_width = min_content_width - current_width
content_left_margin = max(30, content_left_margin - extra_width // 2)
content_right_margin = min(width - 30, content_right_margin + extra_width // 2)
return content_left_margin, content_right_margin
def _calculate_vibrant_font_size_precise(self, text: str, target_width: int, min_size: int = 10, max_size: int = 120) -> Tuple[int, int]:
"""复用VibrantTemplate的精确字体大小计算算法"""
if not text:
return min_size, 0
# 简化的二分查找算法,估算字体大小
tolerance = 0.08 # 容差值
# 使用字符估算来模拟精确计算
avg_char_width_factor = {
'': 1.5, # 中文字符通常比英文宽
'': 0.6, # 英文字符相对较窄
}
# 分析文本,统计中英文字符
chinese_chars = sum(1 for char in text if '\u4e00' <= char <= '\u9fff')
english_chars = len(text) - chinese_chars
# 估算平均字符宽度
estimated_char_width = (chinese_chars * avg_char_width_factor[''] +
english_chars * avg_char_width_factor['']) / max(1, len(text))
# 二分查找最佳字体大小
low = min_size
high = max_size
best_size = min_size
best_width = 0
for _ in range(20): # 限制迭代次数
mid = (low + high) // 2
# 估算当前字体大小下的文本宽度
estimated_width = len(text) * estimated_char_width * mid * 0.6
# 检查是否在容差范围内
if target_width * (1 - tolerance) <= estimated_width <= target_width * (1 + tolerance):
best_size = mid
best_width = int(estimated_width)
break
if estimated_width < target_width:
if estimated_width > best_width:
best_width = int(estimated_width)
best_size = mid
low = mid + 1
else:
high = mid - 1
# 确保在范围内
best_size = max(min_size, min(max_size, best_size))
# 重新计算最终宽度
final_width = int(len(text) * estimated_char_width * best_size * 0.6)
logger.info(f"精确字体计算 - 文本:'{text[:10]}...', 目标宽度:{target_width}, 字体大小:{best_size}, 实际宽度:{final_width}")
return best_size, final_width
def _create_right_column_objects_precise(self, content: Dict[str, Any], x: int, y: int, right_margin: int, scale_ratio: float) -> List[Dict[str, Any]]:
"""创建右栏对象使用VibrantTemplate的精确价格计算"""
objects = []
column_width = right_margin - x
# 价格使用VibrantTemplate的精确参数
if price := content.get("price"):
# VibrantTemplate参数目标宽度为栏宽的70%字体范围40-120
price_target_width = int(column_width * 0.7)
price_size, price_actual_width = self._calculate_vibrant_font_size_precise(
str(price), price_target_width, min_size=40, max_size=120
)
# 计算"CNY起"后缀
suffix_text = "CNY起"
suffix_size = int(price_size * 0.3) # VibrantTemplate中后缀是价格字体的30%
suffix_estimated_width = len(suffix_text) * suffix_size * 0.6
# 右对齐价格和后缀
price_x = right_margin - price_actual_width - suffix_estimated_width
# 价格文本
price_obj = {
"type": "textbox",
"version": "5.3.0",
"originX": "left",
"originY": "top",
"left": price_x,
"top": y,
"width": price_actual_width,
"height": price_size + 20,
"fill": "#ffffff",
"shadow": "rgba(0, 0, 0, 0.7) 2px 2px 5px",
"fontFamily": "Arial Black, sans-serif",
"fontWeight": "bold",
"fontSize": price_size,
"text": f"¥{price}",
"textAlign": "left",
"name": "vibrant_price",
"data": {
"type": "price",
"layer": "content",
"level": 2,
"target_width": price_target_width,
"actual_width": price_actual_width
},
"selectable": True,
"evented": True
}
objects.append(price_obj)
# "CNY起"后缀
suffix_y = y + price_size - suffix_size # 与价格底部对齐
suffix_obj = {
"type": "textbox",
"version": "5.3.0",
"originX": "left",
"originY": "top",
"left": price_x + price_actual_width,
"top": suffix_y,
"width": suffix_estimated_width,
"height": suffix_size + 5,
"fill": "#ffffff",
"fontFamily": "Arial, sans-serif",
"fontWeight": "normal",
"fontSize": suffix_size,
"text": suffix_text,
"textAlign": "left",
"name": "price_suffix",
"data": {
"type": "price_suffix",
"layer": "content",
"level": 2
},
"selectable": True,
"evented": True
}
objects.append(suffix_obj)
# 价格下划线与VibrantTemplate一致
underline_y = y + price_size + int(18 * scale_ratio)
underline = {
"type": "line",
"version": "5.3.0",
"originX": "center",
"originY": "center",
"left": (price_x - 10 + right_margin) / 2,
"top": underline_y,
"x1": (price_x - 10) - (price_x - 10 + right_margin) / 2,
"y1": 0,
"x2": right_margin - (price_x - 10 + right_margin) / 2,
"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)
# 票种使用VibrantTemplate的精确参数
if ticket_type := content.get("ticket_type"):
ticket_y = y + price_size + int(35 * scale_ratio)
# VibrantTemplate参数目标宽度为栏宽的70%字体范围30-60
ticket_target_width = int(column_width * 0.7)
ticket_size, ticket_actual_width = self._calculate_vibrant_font_size_precise(
ticket_type, ticket_target_width, min_size=30, max_size=60
)
ticket_x = right_margin - ticket_actual_width
ticket_obj = {
"type": "textbox",
"version": "5.3.0",
"originX": "left",
"originY": "top",
"left": ticket_x,
"top": ticket_y,
"width": ticket_actual_width,
"height": ticket_size + 10,
"fill": "#ffffff",
"shadow": "rgba(0, 0, 0, 0.5) 1px 1px 3px",
"fontFamily": "Arial, sans-serif",
"fontWeight": "normal",
"fontSize": ticket_size,
"text": ticket_type,
"textAlign": "left",
"name": "ticket_type",
"data": {
"type": "ticket_type",
"layer": "content",
"level": 2,
"target_width": ticket_target_width,
"actual_width": ticket_actual_width
},
"selectable": True,
"evented": True
}
objects.append(ticket_obj)
return objects