from PIL import Image, ImageDraw import io import os def load_image(file_path, scale=1): """加载图片并应用缩放""" img = Image.open(file_path).convert('RGBA') if scale > 1: new_size = (int(img.width * scale), int(img.height * scale)) img = img.resize(new_size, Image.LANCZOS) return img def generate_symmetrical_face_card(original_image_path, scale=1): """ 生成JQK中心对称图案 输入:原始图片路径 输出:中心对称的图像数组(上半部分、下半部分) """ original = load_image(original_image_path, scale) width, height = original.size half_height = height // 2 # 创建上半部分 top_half = original.crop((0, 0, width, half_height)) # 创建下半部分并翻转 bottom_half = original.crop((0, half_height, width, height)) bottom_half = bottom_half.transpose(Image.FLIP_TOP_BOTTOM) return top_half, bottom_half def render_background(canvas, layer, scale): """渲染背景层""" if layer.properties: properties = layer.properties width = canvas.size[0] height = canvas.size[1] # 解析color(如 '#FF0000' 或 'rgb(255,0,0)') bg_color = properties.get('color', '#FFFFFF') # 创建背景矩形 draw = ImageDraw.Draw(canvas, 'RGBA') draw.rectangle(((0, 0), (width, height)), fill=bg_color + 'FF') # 如果有纹理或图案路径 texture_path = properties.get('texture_path') if texture_path and os.path.exists(texture_path): texture = load_image(texture_path, scale) bg_height = height // 4 for y in range(0, height, bg_height): canvas.paste(texture, (0, y), texture) def render_image_layer(canvas, project, layer, scale): """渲染图片层(人像、花色等)""" if not layer.file_ref or not layer.file_ref.file_path: return asset_path = os.path.join(project.media_root, layer.file_ref.file_path) if not os.path.exists(asset_path): return image = load_image(asset_path, scale) # 获取位置信息 properties = layer.properties or {} x = properties.get('x', 0) y = properties.get('y', 0) width = properties.get('width', image.size[0]) height = properties.get('height', image.size[1]) # 计算实际坐标 canvas_width, canvas_height = canvas.size actual_x = (x / project.card_width) * canvas_width actual_y = (y / project.card_height) * canvas_height # 计算实际尺寸 actual_w = (width / project.card_width) * canvas_width actual_h = (height / project.card_height) * canvas_height # 裁剪图片 cropped = image.copy() cropped.thumbnail((actual_w, actual_h), Image.LANCZOS) # 计算居中位置 paste_x = actual_x + (actual_w - cropped.size[0]) / 2 paste_y = actual_y + (actual_h - cropped.size[1]) / 2 canvas.paste(cropped, (int(paste_x), int(paste_y)), cropped) def render_text_layer(canvas, layer, scale): """渲染文字层""" properties = layer.properties or {} draw = ImageDraw.Draw(canvas, 'RGBA') text = properties.get('text', '') x = properties.get('x', 0) y = properties.get('y', 0) # 解析字体和颜色 font = properties.get('font', None) if font and isinstance(font, dict): font_size = int(font.get('size', 24) * scale) font_path = font.get('path') from PIL import ImageFont if font_path and os.path.exists(font_path): try: custom_font = ImageFont.truetype(font_path, font_size) except: custom_font = None else: custom_font = None else: from PIL import ImageFont custom_font = ImageFont.load_default() # 转换颜色 color = properties.get('color', '#000000') if color.startswith('#'): r = int(color[1:3], 16) g = int(color[3:5], 16) b = int(color[5:7], 16) fill = (r, g, b, 255) else: fill = (0, 0, 0, 255) # 计算实际坐标和尺寸 canvas_width, canvas_height = canvas.size actual_x = (x / project.card_width) * canvas_width actual_y = (y / project.card_height) * canvas_height bbox = draw.textbbox((0, 0), text, font=custom_font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] # 居中计算 paste_x = actual_x + (canvas_width * project.card_width * 0.3 - text_width) / 2 paste_y = actual_y + (canvas_height * project.card_height * 0.5 - text_height) / 2 draw.text((int(paste_x), int(paste_y)), text, font=custom_font, fill=fill) def generate_card_png(project, card_key, resolution='standard', scale_map={ 'standard': 1, 'hd': 2, 'ultra-hd': 4 }): """ 生成单张牌的PNG图片 Args: project: Project对象 card_key: 牌面key(如'hearts-A', 'spades-K', 'joker-big') resolution: 分辨率(standard/hd/ultra-hd) scale_map: 分辨率对应的缩放比例 Returns: Image对象 """ scale = scale_map.get(resolution, 1) # 创建基础画布 # 牌面坐标系 x_offset = int(50 * scale) y_offset = int(50 * scale) draw_width = int((project.card_width - 100) * scale) draw_height = int((project.card_height - 100) * scale) canvas = Image.new('RGBA', (draw_width, draw_height)) draw = ImageDraw.Draw(canvas, 'RGBA') draw.rectangle(((0, 0), (draw_width, draw_height)), fill=(255, 255, 255, 255)) # 获取卡片类型的所有图层 layers = CardLayer.objects.filter( project=project, card_key=card_key, visible=True ).order_by('z_index') # 渲染各图层 for layer in layers: layer_type = layer.layer_type if layer_type == 'background': render_background(canvas, layer, scale) elif layer_type == 'image': render_image_layer(canvas, project, layer, scale) elif layer_type == 'text': render_text_layer(canvas, layer, scale) return canvas