#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 图片拼接工具 - 针对GPT优化 将多张图片缩放后拼接,生成GPT可读取的最大尺寸图片 """ import os import sys import json from pathlib import Path from PIL import Image # GPT图片尺寸限制(根据实际API限制调整) # 为了减小base64编码后的体积,使用较小的尺寸 # 2048x2048 对于黑白漫画来说足够清晰,AI可以识别文字和内容 # 这样可以显著减小文件大小(约减少75%的体积) GPT_MAX_WIDTH = 2048 GPT_MAX_HEIGHT = 2048 GPT_MAX_PIXELS = GPT_MAX_WIDTH * GPT_MAX_HEIGHT # 约4.2MP # 为了保证文字清晰,单张图片的最小尺寸(针对黑白漫画优化) MIN_IMAGE_WIDTH = 600 # 最小宽度,保证文字可读(黑白漫画可以稍小) MIN_IMAGE_HEIGHT = 450 # 最小高度 def calculate_grid_layout(images, max_width=GPT_MAX_WIDTH, max_height=GPT_MAX_HEIGHT): """ 计算网格布局参数,最大化利用空间(确保单张网格图不超过限制) 参数: images: 图片列表 max_width: 最大宽度 max_height: 最大高度 返回: (cols, rows, scale_factor, single_width, single_height) """ if not images: return 1, 1, 1.0, 0, 0 # 获取单张图片的尺寸(假设所有图片尺寸相同) single_width = images[0].width single_height = images[0].height # 尝试不同的行列组合,找到能放最多图片且不超过限制的组合 best_cols = 1 best_rows = 1 best_scale = 1.0 max_images_per_grid = 0 # 遍历可能的列数(从1开始,最多到能放下的最大列数) max_possible_cols = min(int(max_width / MIN_IMAGE_WIDTH), len(images)) for cols in range(1, max_possible_cols + 1): # 计算需要的宽度 needed_width = single_width * cols # 如果宽度超过限制,需要缩放 if needed_width > max_width: scale_w = max_width / needed_width else: scale_w = 1.0 # 计算缩放后单张图片的宽度 scaled_single_width = single_width * scale_w # 确保缩放后宽度不小于最小值 if scaled_single_width < MIN_IMAGE_WIDTH: continue # 计算缩放后单张图片的高度(保持宽高比) scaled_single_height = single_height * scale_w # 计算能放多少行 max_rows = int(max_height / scaled_single_height) if max_rows < 1: continue # 计算这个布局能放多少张图片 images_per_grid = cols * max_rows # 如果这个布局能放更多图片,记录它 if images_per_grid > max_images_per_grid: max_images_per_grid = images_per_grid best_cols = cols best_rows = max_rows best_scale = scale_w # 如果没找到合适的,使用保守方案 if max_images_per_grid == 0: # 计算最小缩放因子,确保单张图片能放下 scale_w = min(max_width / single_width, max_height / single_height) # 确保缩放后不小于最小值 if single_width * scale_w < MIN_IMAGE_WIDTH: scale_w = MIN_IMAGE_WIDTH / single_width if single_height * scale_w < MIN_IMAGE_HEIGHT: scale_w = MIN_IMAGE_HEIGHT / single_height best_scale = scale_w scaled_single_width = single_width * best_scale scaled_single_height = single_height * best_scale best_cols = max(1, int(max_width / scaled_single_width)) best_rows = max(1, int(max_height / scaled_single_height)) max_images_per_grid = best_cols * best_rows return best_cols, best_rows, best_scale, single_width, single_height def resize_image(img, scale_factor): """缩放图片""" # 如果缩放因子大于等于1.0,不放大(只缩小) if scale_factor >= 1.0: return img new_width = int(img.width * scale_factor) new_height = int(img.height * scale_factor) # 确保尺寸至少为1 new_width = max(1, new_width) new_height = max(1, new_height) # 使用高质量缩放算法(LANCZOS) return img.resize((new_width, new_height), Image.Resampling.LANCZOS) def merge_images_for_gpt(image_dir, output_path, max_width=GPT_MAX_WIDTH, max_height=GPT_MAX_HEIGHT): """ 将目录下的所有图片缩放后拼接,生成GPT可读取的图片 参数: image_dir: 图片目录(包含单页图片) output_path: 输出文件路径 max_width: 单张图片最大宽度 max_height: 单张图片最大高度 """ image_dir = Path(image_dir) if not image_dir.exists(): print(f"❌ 错误:目录不存在 - {image_dir}") return False # 获取所有图片文件(按文件名排序) image_files = sorted(image_dir.glob("*.jpeg")) + sorted(image_dir.glob("*.jpg")) + \ sorted(image_dir.glob("*.png")) # 过滤掉已拼接的图片 image_files = [f for f in image_files if 'merged' not in f.name.lower() and 'part' not in f.name.lower()] if not image_files: print(f"❌ 在 {image_dir} 中未找到图片文件") return False print(f"📚 找到 {len(image_files)} 张图片") print(f"📁 图片目录: {image_dir}") print(f"📁 输出路径: {output_path}") print(f"🖼️ GPT最大尺寸: {max_width} x {max_height} 像素") print("-" * 60) # 加载所有图片 print("📖 正在加载图片...") images = [] for i, img_path in enumerate(image_files, 1): try: img = Image.open(img_path) # 转换为RGB模式(如果是灰度,保持灰度) if img.mode not in ('L', 'RGB'): img = img.convert('RGB') images.append(img) if i % 50 == 0: print(f" 已加载 {i}/{len(image_files)} 张...") except Exception as e: print(f" ⚠️ 跳过 {img_path.name}: {e}") if not images: print("❌ 没有成功加载的图片") return False print(f"✅ 成功加载 {len(images)} 张图片") # 检查所有图片尺寸是否一致 first_width = images[0].width first_height = images[0].height all_same_size = all(img.width == first_width and img.height == first_height for img in images) if not all_same_size: print(f"⚠️ 警告:图片尺寸不一致,将使用第一张图片的尺寸作为基准") print(f" 第一张: {first_width}x{first_height}, 其他图片将被缩放") print(f"📊 单张图片尺寸: {first_width} x {first_height} 像素") # 计算最佳网格布局 print("🔍 正在计算最佳网格布局(从右到左、从上到下,日式漫画阅读顺序)...") cols, rows, scale_factor, single_width, single_height = calculate_grid_layout(images, max_width, max_height) scaled_single_width = int(single_width * scale_factor) scaled_single_height = int(single_height * scale_factor) grid_width = scaled_single_width * cols grid_height = scaled_single_height * rows print(f"📐 布局方案: {cols}列 x {rows}行") print(f"📉 缩放比例: {scale_factor:.2%}") print(f"📊 单张图片缩放后: {scaled_single_width} x {scaled_single_height} 像素") print(f"📊 网格总尺寸: {grid_width} x {grid_height} 像素 ({grid_width * grid_height/1e6:.2f} MP)") print(f"📈 每张网格图可容纳: {cols * rows} 张原图") # 缩放所有图片 print("🔄 正在缩放图片...") scaled_images = [] for i, img in enumerate(images, 1): # 如果尺寸不一致,需要缩放 if img.width != single_width or img.height != single_height: # 先缩放到标准尺寸 img = img.resize((single_width, single_height), Image.Resampling.LANCZOS) # 应用缩放因子 scaled_img = resize_image(img, scale_factor) scaled_images.append(scaled_img) if i % 50 == 0: print(f" 已缩放 {i}/{len(images)} 张...") # 创建输出子文件夹 output_dir = Path(output_path).parent # 如果output_path已经在gpt_merged文件夹中,需要回到上一级 if output_dir.name == "gpt_merged": output_dir = output_dir.parent output_folder_name = "gpt_merged" output_folder = output_dir / output_folder_name output_folder.mkdir(parents=True, exist_ok=True) output_stem = "gpt_merged" output_suffix = Path(output_path).suffix if Path(output_path).suffix else ".jpg" # 计算需要多少张网格图 images_per_grid = cols * rows total_grids = (len(scaled_images) + images_per_grid - 1) // images_per_grid print(f"\n📦 需要生成 {total_grids} 张网格图片(每张包含最多 {images_per_grid} 张原图)") merged_files = [] for grid_num in range(total_grids): start_idx = grid_num * images_per_grid end_idx = min(start_idx + images_per_grid, len(scaled_images)) grid_images = scaled_images[start_idx:end_idx] # 计算当前网格的实际行数 current_rows = (len(grid_images) + cols - 1) // cols current_grid_height = scaled_single_height * current_rows # 创建网格图(从右到左、从上到下排列,符合日式漫画阅读顺序) print(f"\n🔗 正在生成第 {grid_num + 1}/{total_grids} 张网格图({cols}列 x {current_rows}行,包含{len(grid_images)}张原图,从右到左排列)...") grid_image = Image.new('L' if grid_images[0].mode == 'L' else 'RGB', (grid_width, current_grid_height)) for idx, img in enumerate(grid_images): row = idx // cols # 从右到左排列(日式漫画阅读顺序):第一张图片在最右边,然后向左排列 col = (cols - 1) - (idx % cols) x = col * scaled_single_width y = row * scaled_single_height grid_image.paste(img, (x, y)) if (idx + 1) % 50 == 0 or (idx + 1) == len(grid_images): print(f" 已拼接 {idx + 1}/{len(grid_images)} 张...") # 如果是最后一张图片且图片数量不足以填满整行,裁剪掉左侧空白 is_last_grid = (grid_num == total_grids - 1) current_grid_width = grid_width if is_last_grid and len(grid_images) % cols != 0: # 由于是从右到左排列,空白在左侧 # 计算最后一行的实际列数 actual_cols_in_last_row = len(grid_images) % cols # 从右到左排列时,最后一行的图片在右侧,左侧是空白 # 需要裁剪掉的左侧空白宽度 = (cols - actual_cols_in_last_row) * scaled_single_width left_blank_width = (cols - actual_cols_in_last_row) * scaled_single_width if left_blank_width > 0 and left_blank_width < grid_width: # 裁剪掉左侧空白 actual_width = grid_width - left_blank_width cropped_image = grid_image.crop((left_blank_width, 0, grid_width, current_grid_height)) grid_image = cropped_image current_grid_width = actual_width print(f" ✂️ 裁剪左侧空白: {left_blank_width}px,实际尺寸: {current_grid_width}x{current_grid_height}") # 保存 if total_grids == 1: part_path = output_folder / f"{output_stem}{output_suffix}" else: part_path = output_folder / f"{output_stem}_part{grid_num + 1:02d}{output_suffix}" grid_image.save(part_path, 'JPEG', quality=85, optimize=True, progressive=True) file_size = part_path.stat().st_size / 1024 / 1024 merged_files.append(part_path.name) print(f" ✅ 已保存: {part_path.name} ({current_grid_width}x{current_grid_height}, {file_size:.2f}MB)") print(f"\n✅ 拼接完成!共生成 {len(merged_files)} 张网格图片") return merged_files def main(): """主函数""" # 获取项目根目录 project_root = Path(__file__).parent.parent if len(sys.argv) > 1: # 如果提供了参数,使用指定的目录 image_dir = Path(sys.argv[1]) if len(sys.argv) > 2: output_path = Path(sys.argv[2]) else: # 在图片目录下创建gpt_merged子文件夹 output_folder = image_dir / "gpt_merged" output_folder.mkdir(parents=True, exist_ok=True) output_path = output_folder / "gpt_merged.jpg" else: # 默认使用漫画图片目录 image_dir = project_root / "static" / "漫画" / "image" / "金-田-一-少-年-之-事-件-簿-日-文-版-第001卷" # 输出到子文件夹 output_folder = image_dir / "gpt_merged" output_folder.mkdir(parents=True, exist_ok=True) output_path = output_folder / "gpt_merged.jpg" print("📖 图片拼接工具 - GPT优化版") print("=" * 60) result = merge_images_for_gpt(image_dir, output_path) if result: print("\n" + "=" * 60) print("✅ 处理完成!") if isinstance(result, list): print(f"📊 共生成 {len(result)} 张图片") for name in result: print(f" - {name}") else: print(f"📊 生成图片: {result}") else: print("\n❌ 处理失败") if __name__ == "__main__": main()