2025-09-04 12:55:33 +08:00
|
|
|
|
"""
|
|
|
|
|
|
作者:太一
|
|
|
|
|
|
微信:taiyi1224
|
|
|
|
|
|
邮箱:shuobo1224@qq.com
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2025-09-02 16:49:39 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
from image_manager import ImageManager
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Exporter:
|
|
|
|
|
|
"""导出器,处理批量导出功能"""
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def export_batch(project, output_dir=None, filename_template="{name}_{idx}",
|
|
|
|
|
|
format='png', quality=95, keep_exif=False, progress_callback=None):
|
|
|
|
|
|
"""批量导出所有前景图"""
|
|
|
|
|
|
# 如果没有指定输出目录,默认在第一个图片所在目录下创建output文件夹
|
|
|
|
|
|
if output_dir is None and project.foregrounds:
|
|
|
|
|
|
first_image_dir = Path(project.foregrounds[0].file_path).parent
|
|
|
|
|
|
output_dir = first_image_dir / "output"
|
|
|
|
|
|
|
|
|
|
|
|
output_dir = Path(output_dir)
|
|
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
tasks = []
|
|
|
|
|
|
for idx, fg in enumerate(project.foregrounds):
|
|
|
|
|
|
base_name = Path(fg.file_path).stem
|
|
|
|
|
|
filename = filename_template.format(
|
|
|
|
|
|
name=base_name,
|
|
|
|
|
|
idx=idx + 1,
|
|
|
|
|
|
total=len(project.foregrounds),
|
|
|
|
|
|
date=datetime.now().strftime("%Y%m%d")
|
|
|
|
|
|
)
|
|
|
|
|
|
file_path = output_dir / f"{filename}.{format.lower()}"
|
|
|
|
|
|
tasks.append((fg, file_path))
|
|
|
|
|
|
|
|
|
|
|
|
# 使用线程池执行导出任务,限制最大线程数为4,避免系统卡顿
|
|
|
|
|
|
max_workers = min(4, os.cpu_count() or 1) # 最多使用4个线程,或者CPU核心数(取较小值)
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
|
|
|
|
futures = []
|
|
|
|
|
|
for fg, path in tasks:
|
|
|
|
|
|
future = executor.submit(
|
|
|
|
|
|
Exporter._export_single,
|
|
|
|
|
|
project.template, fg, path, format, quality, keep_exif
|
|
|
|
|
|
)
|
|
|
|
|
|
future.add_done_callback(lambda f, i=len(futures):
|
|
|
|
|
|
progress_callback(i + 1, len(tasks)) if progress_callback else None)
|
|
|
|
|
|
futures.append(future)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否有错误
|
|
|
|
|
|
errors = []
|
|
|
|
|
|
for future in as_completed(futures):
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = future.result(timeout=30) # 设置30秒超时
|
|
|
|
|
|
if not result:
|
|
|
|
|
|
errors.append(1)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"导出错误: {str(e)}")
|
|
|
|
|
|
errors.append(1)
|
|
|
|
|
|
|
|
|
|
|
|
return len(errors) == 0
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _export_single(template, foreground, output_path, format, quality, keep_exif):
|
|
|
|
|
|
"""导出单个前景图"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 加载原图
|
|
|
|
|
|
img = ImageManager.load_image(foreground.file_path)
|
|
|
|
|
|
if img is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# 应用遮罩
|
|
|
|
|
|
masked_img = ImageManager.apply_mask(img, foreground.mask_shape)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用基于顶点的图像变形方法
|
|
|
|
|
|
transformed_img, (x, y) = ImageManager.transform_with_vertices(
|
|
|
|
|
|
masked_img, foreground.vertices
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 创建模板大小的画布
|
|
|
|
|
|
canvas = Image.new('RGBA', (template.width_px, template.height_px),
|
|
|
|
|
|
color=template.bg_color + "FF") # 添加不透明度
|
|
|
|
|
|
|
|
|
|
|
|
# 绘制背景图
|
|
|
|
|
|
if template.bg_image:
|
|
|
|
|
|
# 缩放背景图以适应模板
|
|
|
|
|
|
bg_scaled = template.bg_image.resize(
|
|
|
|
|
|
(template.width_px, template.height_px),
|
|
|
|
|
|
Image.Resampling.LANCZOS
|
|
|
|
|
|
)
|
|
|
|
|
|
canvas.paste(bg_scaled, (0, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# 粘贴前景图
|
|
|
|
|
|
# 确保transformed_img具有RGBA模式
|
|
|
|
|
|
if transformed_img.mode != 'RGBA':
|
|
|
|
|
|
transformed_img = transformed_img.convert('RGBA')
|
|
|
|
|
|
|
|
|
|
|
|
canvas.paste(transformed_img, (int(x), int(y)), transformed_img)
|
|
|
|
|
|
|
|
|
|
|
|
# 保存图像
|
|
|
|
|
|
return ImageManager.save_image(canvas, str(output_path), format, quality, keep_exif)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"导出单个图像失败: {e}")
|
|
|
|
|
|
return False
|