#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Fabric.js JSON 生成器 负责生成 Fabric.js 兼容的 JSON 格式 """ import logging import base64 from io import BytesIO from typing import Dict, Any, Optional, List, Tuple logger = logging.getLogger(__name__) try: from PIL import Image PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False class FabricGenerator: """ Fabric.js JSON 生成器 职责: - 生成 Fabric.js 兼容的 JSON - 创建各种图形对象(图片、文本、形状) - 处理布局计算 """ # Fabric.js 版本 FABRIC_VERSION = "5.3.0" # 基础画布尺寸 BASE_WIDTH = 1350 BASE_HEIGHT = 1800 def __init__(self): self.logger = logging.getLogger(f"{__name__}.FabricGenerator") def generate( self, content: Dict[str, Any], template_id: str, image_size: Tuple[int, int], background_image: Optional['Image.Image'] = None, template_handler=None ) -> Dict[str, Any]: """ 生成 Fabric.js JSON Args: content: 海报内容 template_id: 模板 ID image_size: 画布尺寸 (width, height) background_image: 背景图片 template_handler: 模板处理器实例 Returns: Fabric.js JSON 字典 """ try: self.logger.info(f"开始生成 Fabric.js JSON,模板: {template_id}") # 如果有模板处理器且支持统一渲染 if template_handler and hasattr(template_handler, '_unified_render'): return self._generate_with_template( content, template_handler, background_image ) # 否则使用通用逻辑 return self._generate_generic( content, template_id, image_size, background_image ) except Exception as e: self.logger.error(f"Fabric.js JSON 生成失败: {e}") return self._create_empty_json(image_size) def _generate_with_template( self, content: Dict[str, Any], template_handler, background_image: Optional['Image.Image'] ) -> Dict[str, Any]: """使用模板处理器生成""" try: render_result = template_handler._unified_render( images=background_image, content=content, theme_color=None, glass_intensity=1.5, output_format='json' ) if "error" in render_result: raise Exception(render_result["error"]) return render_result["fabric_json"] except Exception as e: self.logger.error(f"模板渲染失败: {e}") raise def _generate_generic( self, content: Dict[str, Any], template_id: str, image_size: Tuple[int, int], background_image: Optional['Image.Image'] ) -> Dict[str, Any]: """通用生成逻辑""" width, height = image_size objects = [] # 1. 背景图片 if background_image: objects.append(self.create_image_object( background_image, width, height, is_background=True )) else: objects.append(self.create_placeholder_object(width, height)) # 2. 渐变遮罩 gradient_start = self._calculate_gradient_start(content, height) objects.append(self.create_gradient_object(width, height, gradient_start)) # 3. 文本对象 text_objects = self.create_text_objects(content, width, height, gradient_start) objects.extend(text_objects) return { "version": self.FABRIC_VERSION, "width": width, "height": height, "objects": objects } def _create_empty_json(self, image_size: Tuple[int, int]) -> Dict[str, Any]: """创建空的 Fabric.js JSON""" return { "version": self.FABRIC_VERSION, "width": image_size[0], "height": image_size[1], "objects": [] } def _calculate_gradient_start(self, content: Dict[str, Any], height: int) -> int: """计算渐变起始位置""" # 估算内容高度 estimated_height = self._estimate_content_height(content) # 渐变从内容区域上方开始 return max(int(height * 0.4), height - estimated_height - 100) def _estimate_content_height(self, content: Dict[str, Any]) -> int: """估算内容高度""" height = 0 # 标题 if content.get('title'): height += 80 # 副标题 if content.get('subtitle'): height += 50 # 价格 if content.get('price'): height += 60 # 描述 if content.get('description'): lines = len(content['description']) // 20 + 1 height += lines * 30 # 标签 if content.get('tags'): height += 40 return height + 100 # 额外边距 # ========== 对象创建方法 ========== def create_image_object( self, image: 'Image.Image', canvas_width: int, canvas_height: int, is_background: bool = False ) -> Dict[str, Any]: """ 创建图片对象 Args: image: PIL Image canvas_width: 画布宽度 canvas_height: 画布高度 is_background: 是否为背景图片 """ # 转换为 base64 image_base64 = self._image_to_base64(image) # 计算缩放 scale_x = canvas_width / image.width scale_y = canvas_height / image.height scale = max(scale_x, scale_y) if is_background else min(scale_x, scale_y) return { "type": "image", "version": self.FABRIC_VERSION, "left": 0, "top": 0, "width": image.width, "height": image.height, "scaleX": scale, "scaleY": scale, "src": f"data:image/png;base64,{image_base64}", "selectable": not is_background, "evented": not is_background, } def create_placeholder_object( self, canvas_width: int, canvas_height: int ) -> Dict[str, Any]: """创建占位符对象""" return { "type": "rect", "version": self.FABRIC_VERSION, "left": 0, "top": 0, "width": canvas_width, "height": canvas_height, "fill": "#f0f0f0", "selectable": False, "evented": False, } def create_gradient_object( self, canvas_width: int, canvas_height: int, gradient_start: int, colors: Optional[Dict[str, Tuple[int, int, int]]] = None ) -> Dict[str, Any]: """ 创建渐变遮罩对象 Args: canvas_width: 画布宽度 canvas_height: 画布高度 gradient_start: 渐变起始位置 colors: 渐变颜色 """ if colors is None: colors = { 'top': (255, 255, 255), 'bottom': (240, 240, 240) } top_color = colors.get('top', (255, 255, 255)) bottom_color = colors.get('bottom', (240, 240, 240)) return { "type": "rect", "version": self.FABRIC_VERSION, "left": 0, "top": gradient_start, "width": canvas_width, "height": canvas_height - gradient_start, "fill": { "type": "linear", "coords": { "x1": 0, "y1": 0, "x2": 0, "y2": canvas_height - gradient_start }, "colorStops": [ {"offset": 0, "color": f"rgba({top_color[0]},{top_color[1]},{top_color[2]},0.8)"}, {"offset": 1, "color": f"rgba({bottom_color[0]},{bottom_color[1]},{bottom_color[2]},0.95)"} ] }, "selectable": False, "evented": False, } def create_text_object( self, text: str, left: int, top: int, font_size: int = 24, font_family: str = "Arial", fill: str = "#000000", font_weight: str = "normal", text_align: str = "left", **kwargs ) -> Dict[str, Any]: """ 创建文本对象 Args: text: 文本内容 left: 左边距 top: 上边距 font_size: 字体大小 font_family: 字体 fill: 填充颜色 font_weight: 字重 text_align: 对齐方式 """ obj = { "type": "text", "version": self.FABRIC_VERSION, "left": left, "top": top, "text": text, "fontSize": font_size, "fontFamily": font_family, "fill": fill, "fontWeight": font_weight, "textAlign": text_align, "selectable": True, "evented": True, } obj.update(kwargs) return obj def create_text_objects( self, content: Dict[str, Any], canvas_width: int, canvas_height: int, gradient_start: int ) -> List[Dict[str, Any]]: """ 创建所有文本对象 Args: content: 内容字典 canvas_width: 画布宽度 canvas_height: 画布高度 gradient_start: 渐变起始位置 """ objects = [] margin = 40 current_y = gradient_start + 60 # 标题 if content.get('title'): objects.append(self.create_text_object( text=content['title'], left=margin, top=current_y, font_size=48, font_weight="bold", fill="#333333" )) current_y += 70 # 副标题 if content.get('subtitle'): objects.append(self.create_text_object( text=content['subtitle'], left=margin, top=current_y, font_size=28, fill="#666666" )) current_y += 50 # 价格 if content.get('price'): price_text = f"¥{content['price']}" if content.get('original_price'): price_text += f" 原价¥{content['original_price']}" objects.append(self.create_text_object( text=price_text, left=margin, top=current_y, font_size=36, font_weight="bold", fill="#ff4444" )) current_y += 60 # 描述 if content.get('description'): objects.append(self.create_text_object( text=content['description'], left=margin, top=current_y, font_size=20, fill="#888888" )) return objects def create_rect_object( self, left: int, top: int, width: int, height: int, fill: str = "#ffffff", stroke: Optional[str] = None, stroke_width: int = 1, rx: int = 0, ry: int = 0, **kwargs ) -> Dict[str, Any]: """ 创建矩形对象 Args: left: 左边距 top: 上边距 width: 宽度 height: 高度 fill: 填充颜色 stroke: 边框颜色 stroke_width: 边框宽度 rx: 圆角 X ry: 圆角 Y """ obj = { "type": "rect", "version": self.FABRIC_VERSION, "left": left, "top": top, "width": width, "height": height, "fill": fill, "rx": rx, "ry": ry, } if stroke: obj["stroke"] = stroke obj["strokeWidth"] = stroke_width obj.update(kwargs) return obj # ========== 工具方法 ========== def _image_to_base64(self, image: 'Image.Image', format: str = 'PNG') -> str: """将 PIL Image 转换为 base64""" buffer = BytesIO() image.save(buffer, format=format) return base64.b64encode(buffer.getvalue()).decode('utf-8')