import gradio as gr from PIL import Image import io import zipfile def resize_image_to_multiple_of_64_rgba(img: Image.Image): w, h = img.size # 计算最接近的 64 倍数(保底64) w64 = max(64, round(w / 64) * 64) h64 = max(64, round(h / 64) * 64) # 按最小缩放比,使原图能贴进 w64 x h64 scale = min(w64 / w, h64 / h) new_width = int(w * scale) new_height = int(h * scale) # 用 RGBA、黑色背景(全不透明) background = Image.new("RGBA", (w64, h64), (0, 0, 0, 255)) # 缩放原图到 new_width, new_height scaled_img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) # 贴到背景中央,保留其透明度 offset_x = (w64 - new_width) // 2 offset_y = (h64 - new_height) // 2 background.paste(scaled_img, (offset_x, offset_y), scaled_img) return background def make_collage_2x2(four_rgba_images): # 4 张图,均已是 RGBA、同尺寸 w, h = four_rgba_images[0].size # 新画布:2x2 => 宽度 2*w,高度 2*h,背景黑 collage = Image.new("RGBA", (2 * w, 2 * h), (0, 0, 0, 255)) collage.paste(four_rgba_images[0], (0, 0), four_rgba_images[0]) collage.paste(four_rgba_images[1], (w, 0), four_rgba_images[1]) collage.paste(four_rgba_images[2], (0, h), four_rgba_images[2]) collage.paste(four_rgba_images[3], (w, h), four_rgba_images[3]) cw, ch = collage.size # 若合并后任意边 > 2048,则等比例缩小 if cw > 2048 or ch > 2048: scale = min(2048 / cw, 2048 / ch) new_cw = int(cw * scale) new_ch = int(ch * scale) collage = collage.resize((new_cw, new_ch), Image.Resampling.LANCZOS) return collage def process_images_for_preview(uploaded_files): pil_images = [] for f in uploaded_files: if f is not None: # 以 RGBA 读图,保证可保留透明通道 img = Image.open(f.name).convert("RGBA") pil_images.append(img) results = [] # 每 4 张为一组,不足 4 张跳过 for i in range(0, len(pil_images), 4): group = pil_images[i : i + 4] if len(group) < 4: break # 每张做64倍数 resize + 黑色背景填充 resized = [resize_image_to_multiple_of_64_rgba(im) for im in group] # 再 2×2 拼接 collage = make_collage_2x2(resized) results.append(collage) return results def process_and_zip_for_download(uploaded_files): collages = process_images_for_preview(uploaded_files) if not collages: # 没有任何拼接图,就返回 None,让界面不显示可下载链接 return None buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: for idx, img in enumerate(collages): # 转字节并写入 zip img_bytes = io.BytesIO() # 以 PNG 格式保存,带 RGBA img.save(img_bytes, format="PNG") img_bytes.seek(0) zf.writestr(f"collage_{idx+1}.png", img_bytes.read()) buf.seek(0) return buf with gr.Blocks() as demo: gr.Markdown("## 图片 2×2 拼接小工具") gr.Markdown( "1. 一次可上传多张图片,每 4 张为一组。\n" "2. 每组里 4 张图会先缩放到 64 的倍数、空余处用黑色填充,然后 2×2 拼接。\n" "3. 拼接结果若超过 2048×2048,则缩小到不超过 2048。\n" "4. 不足 4 张的剩余图片忽略。\n" "5. 支持保留 PNG 透明度,拼图时使用黑色背景。" ) with gr.Row(): with gr.Column(): file_input = gr.Files(label="上传图片(可多选)", file_types=["image"]) preview_btn = gr.Button("生成预览") download_btn = gr.Button("打包下载 ZIP") with gr.Column(): # Gallery 构造上不使用 .style() 以兼容老版本 gallery_output = gr.Gallery(label="拼接结果预览", columns=2) zip_output = gr.File(label="下载拼接结果 ZIP", interactive=False, visible=False) # 点击“生成预览” -> 返回拼接图列表 preview_btn.click( fn=process_images_for_preview, inputs=[file_input], outputs=[gallery_output] ) # 点击“打包下载 ZIP” -> 生成 zip download_btn.click( fn=process_and_zip_for_download, inputs=[file_input], outputs=[zip_output] ) demo.launch()