196 lines
6.0 KiB
Python
196 lines
6.0 KiB
Python
|
|
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
|