复用了vibrant方法创建json
This commit is contained in:
parent
cdfa9a3699
commit
b57c32a317
@ -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,
|
||||
"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
|
||||
@ -1398,3 +1447,241 @@ 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
|
||||
Loading…
x
Reference in New Issue
Block a user