增加矩阵模板

This commit is contained in:
zejie_chen 2025-10-24 11:57:01 +08:00
parent 27c2170239
commit 1256bda865

View File

@ -0,0 +1,441 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Matrix风格矩阵拼接风格海报模板
支持将多张图片按照指定的矩阵布局拼接带有自定义边框
"""
import base64
from io import BytesIO
from typing import List, Tuple, Optional, Dict, Any, Union
from PIL import Image, ImageDraw
from .base_template import BaseTemplate
class MatrixTemplate(BaseTemplate):
"""
矩阵拼接模板用于将多张图片按照指定的矩阵布局拼接成一张海报
支持自定义边框宽度和颜色可以生成PNG或Fabric.js JSON格式
"""
def __init__(self, size: Tuple[int, int] = (1350, 1800)):
super().__init__(size)
def generate(self,
images: List[Union[str, Image.Image]],
size: Optional[Tuple[int, int]] = None,
layout: Tuple[int, int] = (2, 2),
border_width: Optional[Dict[str, List[int]]] = None,
border_color: Optional[Dict[str, List[Tuple[int, int, int]]]] = None,
generate_fabric_json: bool = False) -> Union[Image.Image, Dict[str, Any]]:
"""
生成矩阵拼接海报
Args:
images (List[Union[str, Image.Image]]): 图片列表可以是路径或PIL Image对象
size (Optional[Tuple[int, int]]): 输出海报尺寸 (, )默认使用初始化时的尺寸
layout (Tuple[int, int]): 矩阵布局 (, )例如 (2, 3) 表示2行3列
border_width (Optional[Dict[str, List[int]]]): 边框宽度配置
格式: {
'row': [上边框, 行间1, 行间2, ..., 下边框], # 长度为 rows+1
'col': [左边框, 列间1, 列间2, ..., 右边框] # 长度为 cols+1
}
border_color (Optional[Dict[str, List[Tuple[int, int, int]]]]): 边框颜色配置
格式: {
'row': [(R,G,B), (R,G,B), ...], # 长度为 rows+1
'col': [(R,G,B), (R,G,B), ...] # 长度为 cols+1
}
generate_fabric_json (bool): 是否生成Fabric.js JSON格式
Returns:
Union[Image.Image, Dict[str, Any]]:
- 如果 generate_fabric_json=False: 返回PIL Image对象
- 如果 generate_fabric_json=True: 返回包含 'image' 'fabric_json' 的字典
"""
# 使用指定的尺寸或默认尺寸
output_size = size or self.size
rows, cols = layout
# 验证输入
if not images:
return None
# 验证图片数量必须与布局匹配
if len(images) != rows * cols:
return None
# 加载图片
loaded_images = []
for img in images[:rows * cols]:
if isinstance(img, str):
try:
loaded_img = Image.open(img)
if loaded_img.mode != 'RGB':
loaded_img = loaded_img.convert('RGB')
loaded_images.append(loaded_img)
except Exception:
return None
elif isinstance(img, Image.Image):
loaded_images.append(img)
else:
return None
# 设置默认边框
if border_width is None:
border_width = {
'row': [0] * (rows + 1),
'col': [0] * (cols + 1)
}
if border_color is None:
border_color = {
'row': [(255, 255, 255)] * (rows + 1),
'col': [(255, 255, 255)] * (cols + 1)
}
# 验证边框配置
if len(border_width['row']) != rows + 1:
return None
if len(border_width['col']) != cols + 1:
return None
if len(border_color['row']) != rows + 1:
return None
if len(border_color['col']) != cols + 1:
return None
# 第1步计算所有单元格的位置和大小
cells = self._calculate_cell_positions(output_size, layout, border_width)
# 第2步裁剪所有子图
cropped_images = self._prepare_cropped_images(loaded_images, cells)
# 第3步准备边框配方位置和颜色信息
border_recipe = self._prepare_border_recipe(output_size, layout, border_width, border_color)
# 第4步生成输出只是按配方组装不做任何计算
if generate_fabric_json:
# 同时生成PNG和JSON
poster_image = self._render_png(
cropped_images, cells, border_recipe, output_size, border_color
)
fabric_json = self._render_json(
cropped_images, cells, border_recipe, output_size, border_color
)
return {
'image': poster_image,
'fabric_json': fabric_json
}
else:
# 仅生成PNG
return self._render_png(
cropped_images, cells, border_recipe, output_size, border_color
)
def _calculate_cell_positions(self,
canvas_size: Tuple[int, int],
layout: Tuple[int, int],
border_width: Dict[str, List[int]]) -> List[Dict[str, Any]]:
"""
计算每个单元格的位置和大小
Args:
canvas_size: 画布尺寸 (, )
layout: 矩阵布局 (, )
border_width: 边框宽度配置
Returns:
List[Dict[str, Any]]: 每个单元格的位置信息
[
{
'row': 行索引,
'col': 列索引,
'x': x坐标,
'y': y坐标,
'width': 宽度,
'height': 高度
},
...
]
"""
canvas_width, canvas_height = canvas_size
rows, cols = layout
# 计算可用空间
total_row_border = sum(border_width['row'])
total_col_border = sum(border_width['col'])
available_width = canvas_width - total_col_border
available_height = canvas_height - total_row_border
# 计算每个单元格的尺寸
cell_width = available_width // cols
cell_height = available_height // rows
# 计算每个单元格的位置
cells = []
current_y = border_width['row'][0]
for row in range(rows):
current_x = border_width['col'][0]
for col in range(cols):
cells.append({
'row': row,
'col': col,
'x': current_x,
'y': current_y,
'width': cell_width,
'height': cell_height
})
# 移动到下一列
current_x += cell_width + border_width['col'][col + 1]
# 移动到下一行
current_y += cell_height + border_width['row'][row + 1]
return cells
def _prepare_cropped_images(self,
images: List[Image.Image],
cells: List[Dict[str, Any]]) -> List[Image.Image]:
"""
准备所有裁剪后的子图
Args:
images: 原始图片列表
cells: 单元格位置信息
Returns:
裁剪后的图片列表
"""
cropped_images = []
for i, cell in enumerate(cells):
if i >= len(images):
break
cropped_img = self._crop_image_to_fit(
images[i],
(cell['width'], cell['height'])
)
cropped_images.append(cropped_img)
return cropped_images
def _prepare_border_recipe(self,
canvas_size: Tuple[int, int],
layout: Tuple[int, int],
border_width: Dict[str, List[int]],
border_color: Dict[str, List[Tuple[int, int, int]]]) -> List[Dict[str, Any]]:
"""
准备边框配方所有边框的位置和颜色信息
Args:
canvas_size: 画布尺寸
layout: 矩阵布局
border_width: 边框宽度
border_color: 边框颜色
Returns:
边框配方列表每个元素包含边框的位置和颜色
"""
canvas_width, canvas_height = canvas_size
rows, cols = layout
border_rects = []
# 计算可用空间
total_row_border = sum(border_width['row'])
total_col_border = sum(border_width['col'])
available_width = canvas_width - total_col_border
available_height = canvas_height - total_row_border
cell_width = available_width // cols
cell_height = available_height // rows
# 准备水平边框(行边框)
current_y = 0
for i in range(rows + 1):
width = border_width['row'][i]
if width > 0:
border_rects.append({
'x': 0,
'y': current_y,
'width': canvas_width,
'height': width,
'color': border_color['row'][i]
})
current_y += width
if i < rows:
current_y += cell_height
# 准备垂直边框(列边框)
current_x = 0
for i in range(cols + 1):
width = border_width['col'][i]
if width > 0:
border_rects.append({
'x': current_x,
'y': 0,
'width': width,
'height': canvas_height,
'color': border_color['col'][i]
})
current_x += width
if i < cols:
current_x += cell_width
return border_rects
def _crop_image_to_fit(self, image: Image.Image, target_size: Tuple[int, int]) -> Image.Image:
"""
裁剪图片以适应目标尺寸参考 resize_image 中的 crop 算法
Args:
image: 原始图片
target_size: 目标尺寸 (, )
Returns:
裁剪后的图片
"""
target_width, target_height = target_size
width, height = image.size
# 计算缩放比例
ratio_w = target_width / width
ratio_h = target_height / height
# 使用较大的比例进行缩放,确保图片完全覆盖目标区域
ratio = max(ratio_w, ratio_h)
new_width = int(width * ratio)
new_height = int(height * ratio)
resized = image.resize((new_width, new_height), Image.LANCZOS)
# 计算裁剪区域(居中裁剪)
left = (new_width - target_width) // 2
top = (new_height - target_height) // 2
right = left + target_width
bottom = top + target_height
return resized.crop((left, top, right, bottom))
def _render_png(self,
cropped_images: List[Image.Image],
cells: List[Dict[str, Any]],
border_recipe: List[Dict[str, Any]],
canvas_size: Tuple[int, int],
border_color: Dict[str, List[Tuple[int, int, int]]]) -> Image.Image:
"""
渲染PNG格式海报纯组装不做任何计算
Args:
cropped_images: 已裁剪好的图片列表
cells: 单元格位置信息
border_recipe: 边框配方位置和颜色
canvas_size: 画布尺寸
border_color: 边框颜色用于背景
Returns:
拼接后的海报图片
"""
# 创建画布
canvas = Image.new('RGB', canvas_size, border_color['row'][0])
draw = ImageDraw.Draw(canvas)
# 绘制边框(按配方)
for border in border_recipe:
draw.rectangle(
[(border['x'], border['y']),
(border['x'] + border['width'], border['y'] + border['height'])],
fill=border['color']
)
# 粘贴图片(按配方)
for img, cell in zip(cropped_images, cells):
canvas.paste(img, (cell['x'], cell['y']))
return canvas
def _render_json(self,
cropped_images: List[Image.Image],
cells: List[Dict[str, Any]],
border_recipe: List[Dict[str, Any]],
canvas_size: Tuple[int, int],
border_color: Dict[str, List[Tuple[int, int, int]]]) -> Dict[str, Any]:
"""
渲染Fabric.js JSON格式纯组装不做任何计算
Args:
cropped_images: 已裁剪好的图片列表
cells: 单元格位置信息
border_recipe: 边框配方位置和颜色
canvas_size: 画布尺寸
border_color: 边框颜色用于背景
Returns:
Fabric.js JSON对象
"""
canvas_width, canvas_height = canvas_size
fabric_objects = []
# 添加背景矩形
fabric_objects.append({
'type': 'rect',
'left': 0,
'top': 0,
'width': canvas_width,
'height': canvas_height,
'fill': self._rgb_to_hex(border_color['row'][0]),
'selectable': False,
'evented': False
})
# 添加边框矩形(按配方)
for border in border_recipe:
fabric_objects.append({
'type': 'rect',
'left': border['x'],
'top': border['y'],
'width': border['width'],
'height': border['height'],
'fill': self._rgb_to_hex(border['color']),
'selectable': False,
'evented': False
})
# 添加图片对象(按配方)
for img, cell in zip(cropped_images, cells):
img_base64 = self._image_to_base64(img)
fabric_objects.append({
'type': 'image',
'left': cell['x'],
'top': cell['y'],
'width': cell['width'],
'height': cell['height'],
'src': f"data:image/png;base64,{img_base64}",
'scaleX': 1,
'scaleY': 1,
'cropX': 0,
'cropY': 0,
'selectable': True,
'evented': True
})
# 构建完整的Fabric.js JSON
fabric_json = {
'version': '5.3.0',
'objects': fabric_objects,
'background': self._rgb_to_hex(border_color['row'][0]),
'width': canvas_width,
'height': canvas_height
}
return fabric_json
def _rgb_to_hex(self, rgb: Tuple[int, int, int]) -> str:
"""将RGB颜色转换为十六进制格式"""
return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
def _image_to_base64(self, image: Image.Image) -> str:
"""将PIL Image转换为base64字符串"""
buffered = BytesIO()
image.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode()
return img_str