This commit is contained in:
jinye_huang 2025-08-04 13:45:14 +08:00
commit 6c4d10b157
2 changed files with 392 additions and 41 deletions

View File

@ -28,7 +28,7 @@ class PosterGenerateRequest(BaseModel):
json_schema_extra = {
"example": {
"templateId": "vibrant",
"imagesBase64": ["base64_encoded_image_1", "base64_encoded_image_2"],
"imagesBase64": "",
"numVariations": 1,
"forceLlmGeneration":False,
"generatePsd": True,

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, Tuple
from typing import List, Dict, Any, Optional, Type, Union, cast
from datetime import datetime
from pathlib import Path
from PIL import Image
@ -229,11 +229,24 @@ class PosterService:
# raise ValueError("无法获取指定的图片")
# # 3. 图片解码
# # 3. 图片解码
images = None
# 获取模板的默认尺寸,如果获取不到则使用标准尺寸
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:
try:
logger.info(f"🎯 接收到 {len(images_base64)} 张图片的base64数据")
@ -277,8 +290,7 @@ class PosterService:
elif file_header.startswith(b'\x89PNG'):
logger.info("✅ 检测到PNG格式图片")
else:
logger.warning(f"⚠️ 未识别的图片格式,文件头: {file_header.hex()}")
logger.warning(f"⚠️ ⚠️ 未识别的图片格式,文件头: {file_header.hex()}")
# 创建PIL Image对象
image_io = BytesIO(image_bytes)
images = Image.open(image_io)
@ -670,10 +682,10 @@ class PosterService:
# 生成JSON的base64编码
json_base64 = None
try:
try:
json_string = json.dumps(fabric_json, ensure_ascii=False)
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.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]:
"""
生成与VibrantTemplate视觉效果一致的Fabric.js JSON格式
完全复制VibrantTemplate的渲染逻辑生成Fabric.js JSON
Args:
content: 海报内容数据
@ -712,33 +724,42 @@ class PosterService:
images: 用户上传的图片
Returns:
Dict: 扁平化的Fabric.js JSON格式数据
Dict: 完全匹配VibrantTemplate的Fabric.js JSON格式数据
"""
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)
# VibrantTemplate的基础尺寸900x1200
base_width, base_height = 900, 1200
# 最终输出尺寸1350x1800
final_width, final_height = image_size[0], image_size[1]
# 1. 用户上传的图片(最底层 - Level 0
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)
else:
# 占位符
placeholder_object = self._create_placeholder_object(canvas_width, canvas_height)
placeholder_object = self._create_placeholder_object(final_width, final_height)
fabric_objects.append(placeholder_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)
# 2. 估算内容高度复制VibrantTemplate逻辑
estimated_height = self._estimate_vibrant_content_height(content)
# 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)
# 3. VibrantTemplate风格的文字布局Level 2
text_objects = self._create_vibrant_text_objects(content, canvas_width, canvas_height, gradient_start, scale_ratio)
# 6. 按VibrantTemplate精确位置渲染文字Level 2
# 缩放渐变起始位置到最终尺寸
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.js JSON
@ -749,8 +770,8 @@ class PosterService:
"backgroundImage": None,
"overlayImage": None,
"clipPath": None,
"width": canvas_width,
"height": canvas_height,
"width": final_width,
"height": final_height,
"viewportTransform": [1, 0, 0, 1, 0, 0],
"backgroundVpt": True,
"overlayVpt": True,
@ -767,10 +788,19 @@ class PosterService:
"perPixelTargetFind": False,
"targetFindTolerance": 0,
"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
except Exception as e:
@ -928,11 +958,11 @@ class PosterService:
"text": text_content,
"textAlign": "center" if key in ['title', 'slogan', 'price'] else "left",
"lineHeight": 1.2,
"angle": 0,
"flipX": False,
"flipY": False,
"opacity": 1,
"visible": True,
"angle": 0,
"flipX": False,
"flipY": False,
"opacity": 1,
"visible": True,
"name": f"text_{key}",
"data": {
"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]:
"""创建模拟毛玻璃效果的渐变背景(简化版本,提高兼容性)"""
"""创建VibrantTemplate风格的毛玻璃渐变背景"""
return {
"type": "rect",
"version": "5.3.0",
@ -1061,19 +1091,33 @@ class PosterService:
"top": gradient_start,
"width": canvas_width,
"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": "",
"strokeWidth": 0,
"angle": 0,
"flipX": False,
"flipY": False,
"opacity": 0.8,
"opacity": 0.85,
"visible": True,
"name": "glass_gradient",
"data": {
"type": "glass_effect",
"layer": "background",
"level": 1
"level": 1,
"effect": "vibrant_glass"
},
"selectable": False,
"evented": False
@ -1124,7 +1168,8 @@ class PosterService:
"level": 2,
"style": "vibrant_title",
"target_width": title_target_width,
"actual_width": title_actual_width
"actual_width": title_actual_width,
"font_path": "/assets/font/兰亭粗黑简.TTF"
},
"selectable": True,
"evented": True
@ -1196,7 +1241,8 @@ class PosterService:
"level": 2,
"style": "vibrant_subtitle",
"target_width": subtitle_target_width,
"actual_width": subtitle_actual_width
"actual_width": subtitle_actual_width,
"font_path": "/assets/font/兰亭粗黑简.TTF"
},
"selectable": True,
"evented": True
@ -1279,7 +1325,7 @@ class PosterService:
"width": button_width - 20,
"height": button_height,
"fill": "#ffffff",
"fontFamily": "Arial, sans-serif",
"fontFamily": "Arial, sans-serif",
"fontWeight": "bold",
"fontSize": int(30 * scale_ratio),
"text": button_text,
@ -1312,7 +1358,7 @@ class PosterService:
"fontWeight": "normal",
"fontSize": font_size,
"text": f"{item}",
"textAlign": "left",
"textAlign": "left",
"name": f"content_item_{i}",
"data": {"type": "content_item", "layer": "content", "level": 2},
"selectable": True,
@ -1598,7 +1644,8 @@ class PosterService:
"layer": "content",
"level": 2,
"target_width": price_target_width,
"actual_width": price_actual_width
"actual_width": price_actual_width,
"font_path": "/assets/font/兰亭粗黑简.TTF"
},
"selectable": True,
"evented": True
@ -1695,4 +1742,308 @@ class PosterService:
}
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