增加矩阵模板
This commit is contained in:
parent
27c2170239
commit
1256bda865
441
poster/templates/matrix_template.py
Normal file
441
poster/templates/matrix_template.py
Normal 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user