From d15a72e489588c817c113f753f237f8b7c6f1ea3 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Mon, 4 Aug 2025 12:17:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=89=81=E5=B9=B3?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/services/poster.py | 578 ++++++++++++++--------------------------- 1 file changed, 188 insertions(+), 390 deletions(-) diff --git a/api/services/poster.py b/api/services/poster.py index 35a9a26..0893484 100644 --- a/api/services/poster.py +++ b/api/services/poster.py @@ -631,7 +631,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]: """ - 生成支持多级分层的Fabric.js JSON格式 + 生成扁平化的Fabric.js JSON格式(不使用group,每个对象独立成层) Args: content: 海报内容数据 @@ -640,7 +640,7 @@ class PosterService: images: 用户上传的图片 Returns: - Dict: 支持多级分层的Fabric.js JSON格式数据 + Dict: 扁平化的Fabric.js JSON格式数据 """ try: fabric_objects = [] @@ -648,21 +648,27 @@ class PosterService: # 基础画布尺寸 canvas_width, canvas_height = image_size[0], image_size[1] - # 1. 图片层组 (最底层 - 用户上传的图片) - image_group = self._create_image_layer(images, canvas_width, canvas_height) - fabric_objects.append(image_group) + # 1. 用户上传的图片(最底层 - Level 0) + if images and hasattr(images, 'width'): + image_object = self._create_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. 背景层组 (第二层) - background_group = self._create_background_layer(canvas_width, canvas_height, template_id) - fabric_objects.append(background_group) + # 2. 背景层(Level 1) + if template_id: + background_object = self._create_background_object(canvas_width, canvas_height) + fabric_objects.append(background_object) - # 3. 内容层组 (中间层) - content_group = self._create_content_layer(content, canvas_width, canvas_height) - fabric_objects.append(content_group) + # 3. 内容文字层(Level 2) + text_objects = self._create_text_objects(content, canvas_width, canvas_height) + fabric_objects.extend(text_objects) - # 4. 装饰层组 (顶层) - decoration_group = self._create_decoration_layer(content, canvas_width, canvas_height) - fabric_objects.append(decoration_group) + # 4. 装饰层(Level 3) + decoration_objects = self._create_decoration_objects(canvas_width, canvas_height) + fabric_objects.extend(decoration_objects) # 构建完整的Fabric.js JSON fabric_json = { @@ -693,7 +699,7 @@ class PosterService: "includeDefaultValues": True } - logger.info(f"成功生成多级分层Fabric.js JSON,包含 {len(fabric_objects)} 个层组") + logger.info(f"成功生成扁平化Fabric.js JSON,包含 {len(fabric_objects)} 个独立对象") return fabric_json except Exception as e: @@ -705,117 +711,58 @@ class PosterService: "width": image_size[0], "height": image_size[1] } - - def _create_image_layer(self, images: Image.Image, canvas_width: int, canvas_height: int) -> Dict[str, Any]: - """创建图片层组(最底层)""" - image_objects = [] + + def _create_image_object(self, images: Image.Image, canvas_width: int, canvas_height: int) -> Dict[str, Any]: + """创建用户上传的图片对象(最底层)""" + # 将PIL图像转换为base64 + image_base64 = self._image_to_base64(images) - if images and hasattr(images, 'width'): - # 将PIL图像转换为base64以便在Fabric.js中使用 - image_base64 = self._image_to_base64(images) - - # 计算图片的缩放比例,保持宽高比 - image_width, image_height = images.width, images.height - scale_x = canvas_width / image_width - scale_y = canvas_height / image_height - scale = min(scale_x, scale_y) # 保持宽高比的适应缩放 - - # 计算居中位置 - scaled_width = image_width * scale - scaled_height = image_height * scale - left = (canvas_width - scaled_width) / 2 - top = (canvas_height - scaled_height) / 2 - - image_objects.append({ - "type": "image", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": left, - "top": top, - "width": image_width, - "height": image_height, - "scaleX": scale, - "scaleY": scale, - "angle": 0, - "flipX": False, - "flipY": False, - "opacity": 1, - "visible": True, - "src": f"data:image/png;base64,{image_base64}", - "crossOrigin": "anonymous", - "name": "user_uploaded_image", - "data": { - "type": "user_image", - "replaceable": True, - "original_size": [image_width, image_height], - "scale_ratio": scale - }, - "selectable": True, - "evented": True, - "moveCursor": "move", - "cornerStyle": "circle", - "cornerSize": 12, - "transparentCorners": False, - "cornerColor": "#4dabf7", - "cornerStrokeColor": "#ffffff", - "borderColor": "#4dabf7", - "borderScaleFactor": 2 - }) - else: - # 如果没有图片,创建一个占位符 - image_objects.append({ - "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], - "name": "image_placeholder", - "data": { - "type": "placeholder", - "replaceable": True, - "placeholder_text": "点击上传图片" - }, - "selectable": True, - "evented": True - }) + # 计算图片的缩放比例,保持宽高比 + image_width, image_height = images.width, images.height + scale_x = canvas_width / image_width + scale_y = canvas_height / image_height + scale = min(scale_x, scale_y) # 保持宽高比的适应缩放 + + # 计算居中位置 + scaled_width = image_width * scale + scaled_height = image_height * scale + left = (canvas_width - scaled_width) / 2 + top = (canvas_height - scaled_height) / 2 return { - "type": "group", + "type": "image", "version": "5.3.0", "originX": "left", "originY": "top", - "left": 0, - "top": 0, - "width": canvas_width, - "height": canvas_height, + "left": left, + "top": top, + "width": image_width, + "height": image_height, + "scaleX": scale, + "scaleY": scale, "angle": 0, - "scaleX": 1, - "scaleY": 1, "flipX": False, "flipY": False, "opacity": 1, "visible": True, - "name": "image_layer", - "data": {"layer": "image", "level": 0, "replaceable": True}, - "objects": image_objects, - "selectable": False, + "src": f"data:image/png;base64,{image_base64}", + "crossOrigin": "anonymous", + "name": "user_uploaded_image", + "data": { + "type": "user_image", + "layer": "image", + "level": 0, + "replaceable": True, + "original_size": [image_width, image_height], + "scale_ratio": scale + }, + "selectable": True, "evented": True } - - def _create_background_layer(self, canvas_width: int, canvas_height: int, template_id: str) -> Dict[str, Any]: - """创建背景层组""" - background_objects = [] - - # 主背景 - background_objects.append({ + + def _create_placeholder_object(self, canvas_width: int, canvas_height: int) -> Dict[str, Any]: + """创建图片占位符对象""" + return { "type": "rect", "version": "5.3.0", "originX": "left", @@ -824,269 +771,118 @@ class PosterService: "top": 0, "width": canvas_width, "height": canvas_height, - "fill": "transparent", + "fill": "#f8f9fa", + "stroke": "#dee2e6", + "strokeWidth": 2, + "strokeDashArray": [10, 5], + "angle": 0, + "flipX": False, + "flipY": False, + "opacity": 1, + "visible": True, + "name": "image_placeholder", + "data": { + "type": "placeholder", + "layer": "image", + "level": 0, + "replaceable": True, + "placeholder_text": "点击上传图片" + }, + "selectable": True, + "evented": True + } + + def _create_background_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, + "fill": "rgba(255, 255, 255, 0.8)", "stroke": None, - "name": "main_background", + "angle": 0, + "flipX": False, + "flipY": False, + "opacity": 0.8, + "visible": True, + "name": "background_overlay", + "data": { + "type": "background", + "layer": "background", + "level": 1 + }, "selectable": False, "evented": False - }) + } + + def _create_text_objects(self, content: Dict[str, Any], canvas_width: int, canvas_height: int) -> List[Dict[str, Any]]: + """创建文本对象列表""" + text_objects = [] - # 渐变背景(可选) - if template_id: - background_objects.append({ - "type": "rect", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 0, - "top": 0, - "width": canvas_width, - "height": canvas_height, - "fill": { - "type": "linear", - "coords": { - "x1": 0, - "y1": 0, - "x2": 0, - "y2": canvas_height + # 文本元素配置 + text_configs = { + 'title': {'fontSize': 48, 'top': 100, 'fontWeight': 'bold', 'fill': '#2c3e50'}, + 'slogan': {'fontSize': 24, 'top': 180, 'fontWeight': 'normal', 'fill': '#7f8c8d'}, + 'content': {'fontSize': 18, 'top': 250, 'fontWeight': 'normal', 'fill': '#34495e'}, + 'price': {'fontSize': 36, 'top': 400, 'fontWeight': 'bold', 'fill': '#e74c3c'}, + 'remarks': {'fontSize': 14, 'top': 500, 'fontWeight': 'normal', 'fill': '#95a5a6'} + } + + 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) + + text_object = { + "type": "textbox", + "version": "5.3.0", + "originX": "left", + "originY": "top", + "left": 50, + "top": config['top'], + "width": canvas_width - 100, + "height": 60 if key != 'content' else 120, + "fill": config['fill'], + "fontFamily": "Arial, sans-serif", + "fontWeight": config['fontWeight'], + "fontSize": config['fontSize'], + "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, + "name": f"text_{key}", + "data": { + "type": "text", + "layer": "content", + "level": 2, + "content_type": key, + "priority": "high" if key in ['title', 'price'] else "medium" }, - "colorStops": [ - {"offset": 0, "color": "#ffffff", "opacity": 0.8}, - {"offset": 1, "color": "#f8f9fa", "opacity": 0.9} - ] - }, - "name": "gradient_background", - "selectable": False, - "evented": False - }) + "selectable": True, + "evented": True + } + text_objects.append(text_object) - return { - "type": "group", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 0, - "top": 0, - "width": canvas_width, - "height": canvas_height, - "angle": 0, - "scaleX": 1, - "scaleY": 1, - "flipX": False, - "flipY": False, - "opacity": 1, - "visible": True, - "name": "background_layer", - "data": {"layer": "background", "level": 1}, - "objects": background_objects, - "selectable": False, - "evented": False - } + return text_objects - def _create_content_layer(self, content: Dict[str, Any], canvas_width: int, canvas_height: int) -> Dict[str, Any]: - """创建内容层组,包含多个子分层""" - content_objects = [] - - # 标题组 - title_group = self._create_title_group(content, canvas_width) - if title_group["objects"]: - content_objects.append(title_group) - - # 正文组 - body_group = self._create_body_group(content, canvas_width) - if body_group["objects"]: - content_objects.append(body_group) - - # 价格信息组 - price_group = self._create_price_group(content, canvas_width) - if price_group["objects"]: - content_objects.append(price_group) - - return { - "type": "group", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 0, - "top": 0, - "width": canvas_width, - "height": canvas_height, - "angle": 0, - "scaleX": 1, - "scaleY": 1, - "flipX": False, - "flipY": False, - "opacity": 1, - "visible": True, - "name": "content_layer", - "data": {"layer": "content", "level": 2}, - "objects": content_objects - } - - def _create_title_group(self, content: Dict[str, Any], canvas_width: int) -> Dict[str, Any]: - """创建标题分组""" - title_objects = [] - - # 主标题 - if content.get('title'): - title_objects.append({ - "type": "textbox", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 50, - "top": 80, - "width": canvas_width - 100, - "height": 80, - "fill": "#2c3e50", - "fontFamily": "Arial, sans-serif", - "fontWeight": "bold", - "fontSize": 48, - "text": str(content['title']), - "textAlign": "center", - "name": "main_title", - "data": {"type": "title", "priority": "high"} - }) - - # 副标题 - if content.get('slogan'): - title_objects.append({ - "type": "textbox", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 50, - "top": 170, - "width": canvas_width - 100, - "height": 40, - "fill": "#7f8c8d", - "fontFamily": "Arial, sans-serif", - "fontWeight": "normal", - "fontSize": 24, - "text": str(content['slogan']), - "textAlign": "center", - "name": "subtitle", - "data": {"type": "subtitle", "priority": "medium"} - }) - - return { - "type": "group", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 0, - "top": 0, - "width": canvas_width, - "height": 250, - "name": "title_group", - "data": {"section": "title", "level": 3}, - "objects": title_objects - } - - def _create_body_group(self, content: Dict[str, Any], canvas_width: int) -> Dict[str, Any]: - """创建正文分组""" - body_objects = [] - - if content.get('content'): - text_content = content['content'] - if isinstance(text_content, list): - text_content = '\n'.join(text_content) - - body_objects.append({ - "type": "textbox", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 50, - "top": 280, - "width": canvas_width - 100, - "height": 120, - "fill": "#34495e", - "fontFamily": "Arial, sans-serif", - "fontWeight": "normal", - "fontSize": 18, - "text": str(text_content), - "textAlign": "left", - "lineHeight": 1.4, - "name": "main_content", - "data": {"type": "content", "priority": "medium"} - }) - - return { - "type": "group", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 0, - "top": 250, - "width": canvas_width, - "height": 150, - "name": "body_group", - "data": {"section": "body", "level": 3}, - "objects": body_objects - } - - def _create_price_group(self, content: Dict[str, Any], canvas_width: int) -> Dict[str, Any]: - """创建价格信息分组""" - price_objects = [] - - if content.get('price'): - # 价格背景 - price_objects.append({ - "type": "rect", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 40, - "top": 420, - "width": canvas_width - 80, - "height": 80, - "fill": "#e74c3c", - "rx": 10, - "ry": 10, - "name": "price_background" - }) - - # 价格文字 - price_objects.append({ - "type": "textbox", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 50, - "top": 440, - "width": canvas_width - 100, - "height": 40, - "fill": "#ffffff", - "fontFamily": "Arial, sans-serif", - "fontWeight": "bold", - "fontSize": 36, - "text": str(content['price']), - "textAlign": "center", - "name": "price_text", - "data": {"type": "price", "priority": "high"} - }) - - return { - "type": "group", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 0, - "top": 400, - "width": canvas_width, - "height": 100, - "name": "price_group", - "data": {"section": "price", "level": 3}, - "objects": price_objects - } - - def _create_decoration_layer(self, content: Dict[str, Any], canvas_width: int, canvas_height: int) -> Dict[str, Any]: - """创建装饰层组""" + def _create_decoration_objects(self, canvas_width: int, canvas_height: int) -> List[Dict[str, Any]]: + """创建装饰对象列表""" decoration_objects = [] # 装饰边框 - decoration_objects.append({ + border_object = { "type": "rect", "version": "5.3.0", "originX": "left", @@ -1099,12 +895,24 @@ class PosterService: "stroke": "#3498db", "strokeWidth": 3, "strokeDashArray": [10, 5], + "angle": 0, + "flipX": False, + "flipY": False, + "opacity": 1, + "visible": True, "name": "decoration_border", - "selectable": False - }) + "data": { + "type": "decoration", + "layer": "decoration", + "level": 3 + }, + "selectable": False, + "evented": False + } + decoration_objects.append(border_object) # 角落装饰 - decoration_objects.append({ + corner_object = { "type": "circle", "version": "5.3.0", "originX": "center", @@ -1113,30 +921,20 @@ class PosterService: "top": 50, "radius": 20, "fill": "#f39c12", - "name": "corner_decoration", - "selectable": False - }) - - return { - "type": "group", - "version": "5.3.0", - "originX": "left", - "originY": "top", - "left": 0, - "top": 0, - "width": canvas_width, - "height": canvas_height, "angle": 0, - "scaleX": 1, - "scaleY": 1, "flipX": False, "flipY": False, "opacity": 1, "visible": True, - "name": "decoration_layer", - "data": {"layer": "decoration", "level": 3}, - "objects": decoration_objects, - "selectable": False + "name": "corner_decoration", + "data": { + "type": "decoration", + "layer": "decoration", + "level": 3 + }, + "selectable": False, + "evented": False } - - \ No newline at end of file + decoration_objects.append(corner_object) + + return decoration_objects \ No newline at end of file