Merge branch 'reinstruct' of http://8.134.70.224:8300/jinye_huang/TravelContentCreator into reinstruct
This commit is contained in:
commit
6c4d10b157
@ -28,7 +28,7 @@ class PosterGenerateRequest(BaseModel):
|
|||||||
json_schema_extra = {
|
json_schema_extra = {
|
||||||
"example": {
|
"example": {
|
||||||
"templateId": "vibrant",
|
"templateId": "vibrant",
|
||||||
"imagesBase64": ["base64_encoded_image_1", "base64_encoded_image_2"],
|
"imagesBase64": "",
|
||||||
"numVariations": 1,
|
"numVariations": 1,
|
||||||
"forceLlmGeneration":False,
|
"forceLlmGeneration":False,
|
||||||
"generatePsd": True,
|
"generatePsd": True,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import importlib
|
|||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import List, Dict, Any, Optional, Type, Union, cast, Tuple
|
from typing import List, Dict, Any, Optional, Type, Union, cast
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@ -229,11 +229,24 @@ class PosterService:
|
|||||||
# raise ValueError("无法获取指定的图片")
|
# raise ValueError("无法获取指定的图片")
|
||||||
|
|
||||||
|
|
||||||
# # 3. 图片解码
|
# # 3. 图片解码
|
||||||
images = None
|
images = None
|
||||||
# 获取模板的默认尺寸,如果获取不到则使用标准尺寸
|
# 获取模板的默认尺寸,如果获取不到则使用标准尺寸
|
||||||
template_size = getattr(template_handler, 'size', (900, 1200))
|
template_size = getattr(template_handler, 'size', (900, 1200))
|
||||||
|
|
||||||
|
if images_base64 and len(images_base64) > 0:
|
||||||
|
try:
|
||||||
|
logger.info(f"🎯 接收到 {len(images_base64)} 张图片的base64数据")
|
||||||
|
|
||||||
|
# 处理第一张图片(目前模板只支持单张图片)
|
||||||
|
# 未来可以扩展为处理多张图片
|
||||||
|
first_image_base64 = images_base64[0] if len(images_base64) > 0 else ""
|
||||||
|
|
||||||
|
if not first_image_base64 or not first_image_base64.strip():
|
||||||
|
raise ValueError("第一张图片的base64数据为空")
|
||||||
|
|
||||||
|
logger.info(f"📊 处理第一张图片,base64长度: {len(first_image_base64)}")
|
||||||
|
|
||||||
if images_base64 and len(images_base64) > 0:
|
if images_base64 and len(images_base64) > 0:
|
||||||
try:
|
try:
|
||||||
logger.info(f"🎯 接收到 {len(images_base64)} 张图片的base64数据")
|
logger.info(f"🎯 接收到 {len(images_base64)} 张图片的base64数据")
|
||||||
@ -277,8 +290,7 @@ class PosterService:
|
|||||||
elif file_header.startswith(b'\x89PNG'):
|
elif file_header.startswith(b'\x89PNG'):
|
||||||
logger.info("✅ 检测到PNG格式图片")
|
logger.info("✅ 检测到PNG格式图片")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ 未识别的图片格式,文件头: {file_header.hex()}")
|
logger.warning(f"⚠️ ⚠️ 未识别的图片格式,文件头: {file_header.hex()}")
|
||||||
|
|
||||||
# 创建PIL Image对象
|
# 创建PIL Image对象
|
||||||
image_io = BytesIO(image_bytes)
|
image_io = BytesIO(image_bytes)
|
||||||
images = Image.open(image_io)
|
images = Image.open(image_io)
|
||||||
@ -670,10 +682,10 @@ class PosterService:
|
|||||||
|
|
||||||
# 生成JSON的base64编码
|
# 生成JSON的base64编码
|
||||||
json_base64 = None
|
json_base64 = None
|
||||||
try:
|
try:
|
||||||
json_string = json.dumps(fabric_json, ensure_ascii=False)
|
json_string = json.dumps(fabric_json, ensure_ascii=False)
|
||||||
json_base64 = base64.b64encode(json_string.encode('utf-8')).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"生成JSON base64编码失败: {e}")
|
logger.warning(f"生成JSON base64编码失败: {e}")
|
||||||
|
|
||||||
logger.info(f"Fabric.js JSON文件生成成功: {json_path} ({file_size/1024:.1f}KB)")
|
logger.info(f"Fabric.js JSON文件生成成功: {json_path} ({file_size/1024:.1f}KB)")
|
||||||
@ -703,7 +715,7 @@ class PosterService:
|
|||||||
|
|
||||||
def _generate_fabric_json(self, content: Dict[str, Any], template_id: str, image_size: List[int], images: Image.Image = None) -> Dict[str, Any]:
|
def _generate_fabric_json(self, content: Dict[str, Any], template_id: str, image_size: List[int], images: Image.Image = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
生成与VibrantTemplate视觉效果一致的Fabric.js JSON格式
|
完全复制VibrantTemplate的渲染逻辑生成Fabric.js JSON
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: 海报内容数据
|
content: 海报内容数据
|
||||||
@ -712,33 +724,42 @@ class PosterService:
|
|||||||
images: 用户上传的图片
|
images: 用户上传的图片
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict: 扁平化的Fabric.js JSON格式数据
|
Dict: 完全匹配VibrantTemplate的Fabric.js JSON格式数据
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
fabric_objects = []
|
fabric_objects = []
|
||||||
|
|
||||||
# 基础画布尺寸(VibrantTemplate使用900x1200,最终resize到1350x1800)
|
# VibrantTemplate的基础尺寸(900x1200)
|
||||||
canvas_width, canvas_height = image_size[0], image_size[1]
|
base_width, base_height = 900, 1200
|
||||||
|
# 最终输出尺寸(1350x1800)
|
||||||
# 计算缩放比例以匹配VibrantTemplate的最终输出
|
final_width, final_height = image_size[0], image_size[1]
|
||||||
scale_ratio = min(canvas_width / 900, canvas_height / 1200)
|
|
||||||
|
|
||||||
# 1. 用户上传的图片(最底层 - Level 0)
|
# 1. 用户上传的图片(最底层 - Level 0)
|
||||||
if images and hasattr(images, 'width'):
|
if images and hasattr(images, 'width'):
|
||||||
image_object = self._create_vibrant_image_object(images, canvas_width, canvas_height)
|
# 按VibrantTemplate方式缩放到基础尺寸
|
||||||
|
image_object = self._create_vibrant_image_object_precise(images, final_width, final_height)
|
||||||
fabric_objects.append(image_object)
|
fabric_objects.append(image_object)
|
||||||
else:
|
else:
|
||||||
# 占位符
|
placeholder_object = self._create_placeholder_object(final_width, final_height)
|
||||||
placeholder_object = self._create_placeholder_object(canvas_width, canvas_height)
|
|
||||||
fabric_objects.append(placeholder_object)
|
fabric_objects.append(placeholder_object)
|
||||||
|
|
||||||
# 2. 毛玻璃渐变背景(Level 1)
|
# 2. 估算内容高度(复制VibrantTemplate逻辑)
|
||||||
gradient_start = self._calculate_gradient_start_position(canvas_height)
|
estimated_height = self._estimate_vibrant_content_height(content)
|
||||||
gradient_object = self._create_gradient_background_object(canvas_width, canvas_height, gradient_start)
|
|
||||||
|
# 3. 动态检测渐变起始位置(复制VibrantTemplate逻辑)
|
||||||
|
gradient_start = self._detect_vibrant_gradient_start_position(images, estimated_height, base_height)
|
||||||
|
|
||||||
|
# 4. 提取毛玻璃颜色(复制VibrantTemplate逻辑)
|
||||||
|
glass_colors = self._extract_vibrant_glass_colors(images, gradient_start)
|
||||||
|
|
||||||
|
# 5. 创建精确的毛玻璃效果(Level 1)
|
||||||
|
gradient_object = self._create_vibrant_glass_effect(final_width, final_height, gradient_start, glass_colors)
|
||||||
fabric_objects.append(gradient_object)
|
fabric_objects.append(gradient_object)
|
||||||
|
|
||||||
# 3. VibrantTemplate风格的文字布局(Level 2)
|
# 6. 按VibrantTemplate精确位置渲染文字(Level 2)
|
||||||
text_objects = self._create_vibrant_text_objects(content, canvas_width, canvas_height, gradient_start, scale_ratio)
|
# 缩放渐变起始位置到最终尺寸
|
||||||
|
scaled_gradient_start = int(gradient_start * final_height / base_height)
|
||||||
|
text_objects = self._create_vibrant_text_layout_precise(content, final_width, final_height, scaled_gradient_start)
|
||||||
fabric_objects.extend(text_objects)
|
fabric_objects.extend(text_objects)
|
||||||
|
|
||||||
# 构建完整的Fabric.js JSON
|
# 构建完整的Fabric.js JSON
|
||||||
@ -749,8 +770,8 @@ class PosterService:
|
|||||||
"backgroundImage": None,
|
"backgroundImage": None,
|
||||||
"overlayImage": None,
|
"overlayImage": None,
|
||||||
"clipPath": None,
|
"clipPath": None,
|
||||||
"width": canvas_width,
|
"width": final_width,
|
||||||
"height": canvas_height,
|
"height": final_height,
|
||||||
"viewportTransform": [1, 0, 0, 1, 0, 0],
|
"viewportTransform": [1, 0, 0, 1, 0, 0],
|
||||||
"backgroundVpt": True,
|
"backgroundVpt": True,
|
||||||
"overlayVpt": True,
|
"overlayVpt": True,
|
||||||
@ -767,10 +788,19 @@ class PosterService:
|
|||||||
"perPixelTargetFind": False,
|
"perPixelTargetFind": False,
|
||||||
"targetFindTolerance": 0,
|
"targetFindTolerance": 0,
|
||||||
"skipOffscreen": True,
|
"skipOffscreen": True,
|
||||||
"includeDefaultValues": True
|
"includeDefaultValues": True,
|
||||||
|
"metadata": {
|
||||||
|
"template": "VibrantTemplate",
|
||||||
|
"base_size": [base_width, base_height],
|
||||||
|
"final_size": [final_width, final_height],
|
||||||
|
"gradient_start": gradient_start,
|
||||||
|
"scaled_gradient_start": scaled_gradient_start,
|
||||||
|
"estimated_content_height": estimated_height,
|
||||||
|
"glass_colors": glass_colors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"成功生成扁平化Fabric.js JSON,包含 {len(fabric_objects)} 个独立对象")
|
logger.info(f"成功生成VibrantTemplate精确Fabric.js JSON,包含 {len(fabric_objects)} 个对象")
|
||||||
return fabric_json
|
return fabric_json
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -928,11 +958,11 @@ class PosterService:
|
|||||||
"text": text_content,
|
"text": text_content,
|
||||||
"textAlign": "center" if key in ['title', 'slogan', 'price'] else "left",
|
"textAlign": "center" if key in ['title', 'slogan', 'price'] else "left",
|
||||||
"lineHeight": 1.2,
|
"lineHeight": 1.2,
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"flipX": False,
|
"flipX": False,
|
||||||
"flipY": False,
|
"flipY": False,
|
||||||
"opacity": 1,
|
"opacity": 1,
|
||||||
"visible": True,
|
"visible": True,
|
||||||
"name": f"text_{key}",
|
"name": f"text_{key}",
|
||||||
"data": {
|
"data": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@ -1051,7 +1081,7 @@ class PosterService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _create_gradient_background_object(self, canvas_width: int, canvas_height: int, gradient_start: int) -> Dict[str, Any]:
|
def _create_gradient_background_object(self, canvas_width: int, canvas_height: int, gradient_start: int) -> Dict[str, Any]:
|
||||||
"""创建模拟毛玻璃效果的渐变背景(简化版本,提高兼容性)"""
|
"""创建VibrantTemplate风格的毛玻璃渐变背景"""
|
||||||
return {
|
return {
|
||||||
"type": "rect",
|
"type": "rect",
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
@ -1061,19 +1091,33 @@ class PosterService:
|
|||||||
"top": gradient_start,
|
"top": gradient_start,
|
||||||
"width": canvas_width,
|
"width": canvas_width,
|
||||||
"height": canvas_height - gradient_start,
|
"height": canvas_height - gradient_start,
|
||||||
"fill": "rgba(0, 50, 120, 0.6)", # 简化为单一颜色,提高兼容性
|
"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.7)"},
|
||||||
|
{"offset": 1, "color": "rgba(0, 30, 80, 0.9)"}
|
||||||
|
]
|
||||||
|
},
|
||||||
"stroke": "",
|
"stroke": "",
|
||||||
"strokeWidth": 0,
|
"strokeWidth": 0,
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"flipX": False,
|
"flipX": False,
|
||||||
"flipY": False,
|
"flipY": False,
|
||||||
"opacity": 0.8,
|
"opacity": 0.85,
|
||||||
"visible": True,
|
"visible": True,
|
||||||
"name": "glass_gradient",
|
"name": "glass_gradient",
|
||||||
"data": {
|
"data": {
|
||||||
"type": "glass_effect",
|
"type": "glass_effect",
|
||||||
"layer": "background",
|
"layer": "background",
|
||||||
"level": 1
|
"level": 1,
|
||||||
|
"effect": "vibrant_glass"
|
||||||
},
|
},
|
||||||
"selectable": False,
|
"selectable": False,
|
||||||
"evented": False
|
"evented": False
|
||||||
@ -1124,7 +1168,8 @@ class PosterService:
|
|||||||
"level": 2,
|
"level": 2,
|
||||||
"style": "vibrant_title",
|
"style": "vibrant_title",
|
||||||
"target_width": title_target_width,
|
"target_width": title_target_width,
|
||||||
"actual_width": title_actual_width
|
"actual_width": title_actual_width,
|
||||||
|
"font_path": "/assets/font/兰亭粗黑简.TTF"
|
||||||
},
|
},
|
||||||
"selectable": True,
|
"selectable": True,
|
||||||
"evented": True
|
"evented": True
|
||||||
@ -1196,7 +1241,8 @@ class PosterService:
|
|||||||
"level": 2,
|
"level": 2,
|
||||||
"style": "vibrant_subtitle",
|
"style": "vibrant_subtitle",
|
||||||
"target_width": subtitle_target_width,
|
"target_width": subtitle_target_width,
|
||||||
"actual_width": subtitle_actual_width
|
"actual_width": subtitle_actual_width,
|
||||||
|
"font_path": "/assets/font/兰亭粗黑简.TTF"
|
||||||
},
|
},
|
||||||
"selectable": True,
|
"selectable": True,
|
||||||
"evented": True
|
"evented": True
|
||||||
@ -1279,7 +1325,7 @@ class PosterService:
|
|||||||
"width": button_width - 20,
|
"width": button_width - 20,
|
||||||
"height": button_height,
|
"height": button_height,
|
||||||
"fill": "#ffffff",
|
"fill": "#ffffff",
|
||||||
"fontFamily": "Arial, sans-serif",
|
"fontFamily": "Arial, sans-serif",
|
||||||
"fontWeight": "bold",
|
"fontWeight": "bold",
|
||||||
"fontSize": int(30 * scale_ratio),
|
"fontSize": int(30 * scale_ratio),
|
||||||
"text": button_text,
|
"text": button_text,
|
||||||
@ -1312,7 +1358,7 @@ class PosterService:
|
|||||||
"fontWeight": "normal",
|
"fontWeight": "normal",
|
||||||
"fontSize": font_size,
|
"fontSize": font_size,
|
||||||
"text": f"• {item}",
|
"text": f"• {item}",
|
||||||
"textAlign": "left",
|
"textAlign": "left",
|
||||||
"name": f"content_item_{i}",
|
"name": f"content_item_{i}",
|
||||||
"data": {"type": "content_item", "layer": "content", "level": 2},
|
"data": {"type": "content_item", "layer": "content", "level": 2},
|
||||||
"selectable": True,
|
"selectable": True,
|
||||||
@ -1598,7 +1644,8 @@ class PosterService:
|
|||||||
"layer": "content",
|
"layer": "content",
|
||||||
"level": 2,
|
"level": 2,
|
||||||
"target_width": price_target_width,
|
"target_width": price_target_width,
|
||||||
"actual_width": price_actual_width
|
"actual_width": price_actual_width,
|
||||||
|
"font_path": "/assets/font/兰亭粗黑简.TTF"
|
||||||
},
|
},
|
||||||
"selectable": True,
|
"selectable": True,
|
||||||
"evented": True
|
"evented": True
|
||||||
@ -1695,4 +1742,308 @@ class PosterService:
|
|||||||
}
|
}
|
||||||
objects.append(ticket_obj)
|
objects.append(ticket_obj)
|
||||||
|
|
||||||
return objects
|
return objects
|
||||||
|
|
||||||
|
def _estimate_vibrant_content_height(self, content: Dict[str, Any]) -> int:
|
||||||
|
"""复制VibrantTemplate的内容高度估算逻辑"""
|
||||||
|
standard_margin = 25
|
||||||
|
title_height = 100
|
||||||
|
subtitle_height = 80
|
||||||
|
button_height = 40
|
||||||
|
content_items = content.get("content_items", [])
|
||||||
|
content_line_height = 32
|
||||||
|
content_list_height = len(content_items) * content_line_height
|
||||||
|
price_height = 90
|
||||||
|
ticket_height = 60
|
||||||
|
remarks = content.get("remarks", [])
|
||||||
|
if isinstance(remarks, str):
|
||||||
|
remarks = [remarks]
|
||||||
|
remarks_height = len(remarks) * 25 + 10
|
||||||
|
footer_height = 40
|
||||||
|
total_height = (
|
||||||
|
20 + title_height + standard_margin + subtitle_height + standard_margin +
|
||||||
|
button_height + 15 + content_list_height + price_height + ticket_height +
|
||||||
|
remarks_height + footer_height + 30
|
||||||
|
)
|
||||||
|
logger.info(f"VibrantTemplate估算内容高度: {total_height}")
|
||||||
|
return total_height
|
||||||
|
|
||||||
|
def _detect_vibrant_gradient_start_position(self, image: Image.Image, estimated_height: int, base_height: int) -> int:
|
||||||
|
"""复制VibrantTemplate的动态渐变起始位置检测"""
|
||||||
|
if not image or not hasattr(image, 'width'):
|
||||||
|
# 如果没有图像,使用估算位置
|
||||||
|
bottom_margin = 60
|
||||||
|
gradient_start = max(base_height - estimated_height - bottom_margin, base_height // 2)
|
||||||
|
logger.info(f"无图像,使用估算渐变起始位置: {gradient_start}")
|
||||||
|
return gradient_start
|
||||||
|
|
||||||
|
# 临时缩放图像到基础尺寸进行分析
|
||||||
|
temp_image = image.resize((900, 1200), Image.LANCZOS)
|
||||||
|
if temp_image.mode != 'RGB':
|
||||||
|
temp_image = temp_image.convert('RGB')
|
||||||
|
|
||||||
|
width, height = temp_image.size
|
||||||
|
center_x = width // 2
|
||||||
|
gradient_start = None
|
||||||
|
|
||||||
|
# 从中央开始扫描,寻找亮度>50的像素
|
||||||
|
for y in range(height // 2, height):
|
||||||
|
try:
|
||||||
|
pixel = temp_image.getpixel((center_x, y))
|
||||||
|
if isinstance(pixel, (tuple, list)) and len(pixel) >= 3:
|
||||||
|
brightness = sum(pixel[:3]) / 3
|
||||||
|
if brightness > 50:
|
||||||
|
gradient_start = max(y - 20, height // 2)
|
||||||
|
logger.info(f"检测到亮度>50的像素位置: y={y}, brightness={brightness:.1f}")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果没有找到合适位置,使用估算位置
|
||||||
|
if gradient_start is None:
|
||||||
|
bottom_margin = 60
|
||||||
|
gradient_start = max(height - estimated_height - bottom_margin, height // 2)
|
||||||
|
logger.info(f"未找到合适像素,使用估算渐变起始位置: {gradient_start}")
|
||||||
|
else:
|
||||||
|
logger.info(f"动态检测到渐变起始位置: {gradient_start}")
|
||||||
|
|
||||||
|
return gradient_start
|
||||||
|
|
||||||
|
def _extract_vibrant_glass_colors(self, image: Image.Image, gradient_start: int) -> Dict[str, Tuple[int, int, int]]:
|
||||||
|
"""复制VibrantTemplate的毛玻璃颜色提取逻辑"""
|
||||||
|
if not image or not hasattr(image, 'width'):
|
||||||
|
# 默认蓝色毛玻璃效果
|
||||||
|
default_colors = {
|
||||||
|
"top_color": (0, 5, 15),
|
||||||
|
"bottom_color": (0, 25, 50)
|
||||||
|
}
|
||||||
|
logger.info(f"无图像,使用默认毛玻璃颜色: {default_colors}")
|
||||||
|
return default_colors
|
||||||
|
|
||||||
|
# 临时缩放图像到基础尺寸进行颜色提取
|
||||||
|
temp_image = image.resize((900, 1200), Image.LANCZOS)
|
||||||
|
if temp_image.mode != 'RGB':
|
||||||
|
temp_image = temp_image.convert('RGB')
|
||||||
|
|
||||||
|
width, height = temp_image.size
|
||||||
|
top_samples, bottom_samples = [], []
|
||||||
|
|
||||||
|
# 在渐变起始位置+20px采样顶部颜色
|
||||||
|
top_y = min(gradient_start + 20, height - 1)
|
||||||
|
for x in range(0, width, 20):
|
||||||
|
try:
|
||||||
|
pixel = temp_image.getpixel((x, top_y))
|
||||||
|
if sum(pixel) > 30: # 排除过暗像素
|
||||||
|
top_samples.append(pixel)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 在底部-50px采样底部颜色
|
||||||
|
bottom_y = min(height - 50, height - 1)
|
||||||
|
for x in range(0, width, 20):
|
||||||
|
try:
|
||||||
|
pixel = temp_image.getpixel((x, bottom_y))
|
||||||
|
if sum(pixel) > 30: # 排除过暗像素
|
||||||
|
bottom_samples.append(pixel)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算平均颜色并降低亮度(复制VibrantTemplate逻辑)
|
||||||
|
import numpy as np
|
||||||
|
if top_samples:
|
||||||
|
top_avg = np.mean(top_samples, axis=0)
|
||||||
|
top_color = tuple(max(0, int(c * 0.1)) for c in top_avg) # 10%亮度
|
||||||
|
else:
|
||||||
|
top_color = (0, 5, 15)
|
||||||
|
|
||||||
|
if bottom_samples:
|
||||||
|
bottom_avg = np.mean(bottom_samples, axis=0)
|
||||||
|
bottom_color = tuple(max(0, int(c * 0.2)) for c in bottom_avg) # 20%亮度
|
||||||
|
else:
|
||||||
|
bottom_color = (0, 25, 50)
|
||||||
|
|
||||||
|
colors = {
|
||||||
|
"top_color": top_color,
|
||||||
|
"bottom_color": bottom_color
|
||||||
|
}
|
||||||
|
logger.info(f"提取毛玻璃颜色: 顶部={top_color}({len(top_samples)}样本), 底部={bottom_color}({len(bottom_samples)}样本)")
|
||||||
|
return colors
|
||||||
|
|
||||||
|
def _create_vibrant_glass_effect(self, canvas_width: int, canvas_height: int, gradient_start: int, glass_colors: Dict) -> Dict[str, Any]:
|
||||||
|
"""创建VibrantTemplate精确的毛玻璃效果"""
|
||||||
|
# 缩放渐变起始位置到最终尺寸
|
||||||
|
scaled_gradient_start = int(gradient_start * canvas_height / 1200)
|
||||||
|
|
||||||
|
top_color = glass_colors["top_color"]
|
||||||
|
bottom_color = glass_colors["bottom_color"]
|
||||||
|
|
||||||
|
# 创建复杂的渐变效果,模拟VibrantTemplate的数学公式
|
||||||
|
gradient_stops = []
|
||||||
|
|
||||||
|
# 生成多个渐变停止点以模拟复杂的数学渐变
|
||||||
|
import math
|
||||||
|
for i in range(10):
|
||||||
|
ratio = i / 9 # 0到1
|
||||||
|
# 复制VibrantTemplate的smooth_ratio公式
|
||||||
|
smooth_ratio = 0.5 - 0.5 * math.cos(ratio * math.pi)
|
||||||
|
|
||||||
|
# 插值颜色
|
||||||
|
r = int((1 - smooth_ratio) * top_color[0] + smooth_ratio * bottom_color[0])
|
||||||
|
g = int((1 - smooth_ratio) * top_color[1] + smooth_ratio * bottom_color[1])
|
||||||
|
b = int((1 - smooth_ratio) * top_color[2] + smooth_ratio * bottom_color[2])
|
||||||
|
|
||||||
|
# 复制VibrantTemplate的透明度计算
|
||||||
|
alpha_smooth = ratio ** (1.1 / 1.5) # intensity=1.5
|
||||||
|
alpha = 0.02 + 0.98 * alpha_smooth
|
||||||
|
|
||||||
|
gradient_stops.append({
|
||||||
|
"offset": ratio,
|
||||||
|
"color": f"rgba({r}, {g}, {b}, {alpha:.3f})"
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"创建毛玻璃效果: 起始位置={scaled_gradient_start}, 渐变点={len(gradient_stops)}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "rect",
|
||||||
|
"version": "5.3.0",
|
||||||
|
"originX": "left",
|
||||||
|
"originY": "top",
|
||||||
|
"left": 0,
|
||||||
|
"top": scaled_gradient_start,
|
||||||
|
"width": canvas_width,
|
||||||
|
"height": canvas_height - scaled_gradient_start,
|
||||||
|
"fill": {
|
||||||
|
"type": "linear",
|
||||||
|
"coords": {
|
||||||
|
"x1": 0,
|
||||||
|
"y1": 0,
|
||||||
|
"x2": 0,
|
||||||
|
"y2": canvas_height - scaled_gradient_start
|
||||||
|
},
|
||||||
|
"colorStops": gradient_stops
|
||||||
|
},
|
||||||
|
"stroke": "",
|
||||||
|
"strokeWidth": 0,
|
||||||
|
"angle": 0,
|
||||||
|
"flipX": False,
|
||||||
|
"flipY": False,
|
||||||
|
"opacity": 0.85,
|
||||||
|
"visible": True,
|
||||||
|
"name": "vibrant_glass_effect",
|
||||||
|
"data": {
|
||||||
|
"type": "glass_effect",
|
||||||
|
"layer": "background",
|
||||||
|
"level": 1,
|
||||||
|
"effect": "vibrant_template_precise"
|
||||||
|
},
|
||||||
|
"selectable": False,
|
||||||
|
"evented": False
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_vibrant_image_object_precise(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": "vibrant_background_image",
|
||||||
|
"data": {
|
||||||
|
"type": "background_image",
|
||||||
|
"layer": "image",
|
||||||
|
"level": 0,
|
||||||
|
"replaceable": True
|
||||||
|
},
|
||||||
|
"selectable": True,
|
||||||
|
"evented": True
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_vibrant_text_layout_precise(self, content: Dict[str, Any], canvas_width: int, canvas_height: int, gradient_start: int) -> List[Dict[str, Any]]:
|
||||||
|
"""复制VibrantTemplate的精确文本布局逻辑"""
|
||||||
|
text_objects = []
|
||||||
|
|
||||||
|
# 计算基础参数(缩放到最终尺寸)
|
||||||
|
center_x = canvas_width // 2
|
||||||
|
scale_factor = canvas_height / 1800 # 从1800缩放到最终高度
|
||||||
|
|
||||||
|
# 简化版本:使用固定边距
|
||||||
|
margin_ratio = 0.1
|
||||||
|
left_margin = int(canvas_width * margin_ratio)
|
||||||
|
right_margin = int(canvas_width * (1 - margin_ratio))
|
||||||
|
|
||||||
|
# 标题(VibrantTemplate: gradient_start + 40)
|
||||||
|
title_y = gradient_start + int(40 * scale_factor)
|
||||||
|
if title := content.get("title"):
|
||||||
|
title_size = int(80 * scale_factor)
|
||||||
|
title_obj = {
|
||||||
|
"type": "textbox",
|
||||||
|
"version": "5.3.0",
|
||||||
|
"originX": "center",
|
||||||
|
"originY": "top",
|
||||||
|
"left": center_x,
|
||||||
|
"top": title_y,
|
||||||
|
"width": right_margin - left_margin,
|
||||||
|
"height": title_size + 20,
|
||||||
|
"fill": "#ffffff",
|
||||||
|
"stroke": "#001e50",
|
||||||
|
"strokeWidth": 4,
|
||||||
|
"fontFamily": "兰亭粗黑简, 微软雅黑, Microsoft YaHei, PingFang SC, Hiragino Sans GB, Arial Black, sans-serif",
|
||||||
|
"fontWeight": "bold",
|
||||||
|
"fontSize": title_size,
|
||||||
|
"text": title,
|
||||||
|
"textAlign": "center",
|
||||||
|
"lineHeight": 1.1,
|
||||||
|
"name": "vibrant_title_precise",
|
||||||
|
"data": {"type": "title", "layer": "content", "level": 2},
|
||||||
|
"selectable": True,
|
||||||
|
"evented": True
|
||||||
|
}
|
||||||
|
text_objects.append(title_obj)
|
||||||
|
|
||||||
|
# 副标题
|
||||||
|
subtitle_y = title_y + int(130 * scale_factor)
|
||||||
|
if slogan := content.get("slogan"):
|
||||||
|
subtitle_size = int(40 * scale_factor)
|
||||||
|
subtitle_obj = {
|
||||||
|
"type": "textbox",
|
||||||
|
"version": "5.3.0",
|
||||||
|
"originX": "center",
|
||||||
|
"originY": "top",
|
||||||
|
"left": center_x,
|
||||||
|
"top": subtitle_y,
|
||||||
|
"width": right_margin - left_margin,
|
||||||
|
"height": subtitle_size + 15,
|
||||||
|
"fill": "#ffffff",
|
||||||
|
"shadow": "rgba(0, 0, 0, 0.7) 2px 2px 5px",
|
||||||
|
"fontFamily": "兰亭粗黑简, 微软雅黑, Microsoft YaHei, PingFang SC, Hiragino Sans GB, Arial, sans-serif",
|
||||||
|
"fontWeight": "normal",
|
||||||
|
"fontSize": subtitle_size,
|
||||||
|
"text": slogan,
|
||||||
|
"textAlign": "center",
|
||||||
|
"lineHeight": 1.2,
|
||||||
|
"name": "vibrant_slogan_precise",
|
||||||
|
"data": {"type": "slogan", "layer": "content", "level": 2},
|
||||||
|
"selectable": True,
|
||||||
|
"evented": True
|
||||||
|
}
|
||||||
|
text_objects.append(subtitle_obj)
|
||||||
|
|
||||||
|
logger.info(f"创建VibrantTemplate精确文本布局: {len(text_objects)}个对象")
|
||||||
|
return text_objects
|
||||||
Loading…
x
Reference in New Issue
Block a user