File size: 6,836 Bytes
ad22797
14c46f5
af3dd3e
 
1898bb9
99c0e27
 
af3dd3e
1898bb9
 
 
 
 
 
 
 
99c0e27
1898bb9
 
 
14df9f4
 
 
 
99c0e27
 
 
14df9f4
 
1898bb9
14c46f5
 
 
1898bb9
14df9f4
 
 
1898bb9
99c0e27
14df9f4
 
 
 
 
4bbef93
af3dd3e
1898bb9
 
99c0e27
 
 
 
 
1898bb9
af3dd3e
1898bb9
 
 
14df9f4
 
 
 
 
 
1898bb9
 
 
99c0e27
1898bb9
 
14df9f4
 
99c0e27
 
1898bb9
 
 
 
 
 
 
99c0e27
1898bb9
 
 
 
 
 
 
14df9f4
 
1898bb9
 
 
99c0e27
 
 
1898bb9
 
 
14df9f4
 
 
1898bb9
 
 
 
14c46f5
4bbef93
 
 
14c46f5
1898bb9
14c46f5
14df9f4
 
 
1898bb9
 
14df9f4
 
1898bb9
14df9f4
 
 
1898bb9
14df9f4
 
1898bb9
99c0e27
14df9f4
1898bb9
14df9f4
 
1898bb9
 
 
14df9f4
1898bb9
14df9f4
 
1898bb9
14df9f4
99c0e27
 
1898bb9
 
14c46f5
 
af3dd3e
1898bb9
99c0e27
 
 
 
1898bb9
14c46f5
99c0e27
14c46f5
99c0e27
 
 
1898bb9
14c46f5
 
 
1898bb9
99c0e27
 
 
 
 
 
 
 
af3dd3e
14df9f4
 
99c0e27
 
 
14df9f4
99c0e27
 
 
14df9f4
99c0e27
14df9f4
4bbef93
99c0e27
af3dd3e
14c46f5
99c0e27
14df9f4
 
14c46f5
14df9f4
99c0e27
 
 
14df9f4
 
 
 
 
 
 
 
 
99c0e27
14df9f4
af3dd3e
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import gradio as gr
from PIL import Image
import io
import zipfile
import random
import tempfile
import os

def random_black_or_white():
    return (0, 0, 0, 255) if random.random() < 0.5 else (255, 255, 255, 255)

def random_non_black_white():
    while True:
        r = random.randint(0, 255)
        g = random.randint(0, 255)
        b = random.randint(0, 255)
        # 不要纯黑纯白
        if not (r == g == b == 0 or r == g == b == 255):
            return (r, g, b, 255)

def limit_2048(img: Image.Image):
    w, h = img.size
    if w > 2048 or h > 2048:
        scale = min(2048 / w, 2048 / h)
        nw = int(w * scale)
        nh = int(h * scale)
        img = img.resize((nw, nh), Image.Resampling.LANCZOS)
    return img

def resize_to_64_multiple(img: Image.Image):
    w, h = img.size
    w64 = max(64, round(w / 64) * 64)
    h64 = max(64, round(h / 64) * 64)
    
    scale = min(w64 / w, h64 / h)
    nw = int(w * scale)
    nh = int(h * scale)
    
    bg_color = random_black_or_white()
    background = Image.new("RGBA", (w64, h64), bg_color)
    scaled = img.resize((nw, nh), Image.Resampling.LANCZOS)
    ox = (w64 - nw) // 2
    oy = (h64 - nh) // 2
    background.paste(scaled, (ox, oy), scaled)
    return background

def make_collage_2x2(images_4):
    w, h = images_4[0].size
    collage = Image.new("RGBA", (2*w, 2*h), (0,0,0,255))
    collage.paste(images_4[0], (0,0), images_4[0])
    collage.paste(images_4[1], (w,0), images_4[1])
    collage.paste(images_4[2], (0,h), images_4[2])
    collage.paste(images_4[3], (w,h), images_4[3])
    return limit_2048(collage)

def make_collage_leftover(images_leftover):
    n = len(images_leftover)
    if n < 1 or n > 3:
        return None
    
    resized_list = [resize_to_64_multiple(img) for img in images_leftover]
    max_w = max(im.size[0] for im in resized_list)
    max_h = max(im.size[1] for im in resized_list)
    
    uniformed = []
    for rimg in resized_list:
        w, h = rimg.size
        if (w,h) == (max_w, max_h):
            uniformed.append(rimg)
        else:
            bg_color = rimg.getpixel((0,0))
            bg = Image.new("RGBA", (max_w, max_h), bg_color)
            offx = (max_w - w)//2
            offy = (max_h - h)//2
            bg.paste(rimg, (offx, offy), rimg)
            uniformed.append(bg)
    
    if n == 1:
        possible_layouts = [(1,1), (1,2), (2,1), (2,2)]
    elif n == 2:
        possible_layouts = [(1,2), (2,1), (2,2)]
    else: # n == 3
        possible_layouts = [(2,2)]
    
    rows, cols = random.choice(possible_layouts)
    big_w = cols * max_w
    big_h = rows * max_h
    collage = Image.new("RGBA", (big_w, big_h), (0,0,0,255))
    
    cells = [(r, c) for r in range(rows) for c in range(cols)]
    random.shuffle(cells)
    
    for i, img_ in enumerate(uniformed):
        r, c = cells[i]
        ox = c * max_w
        oy = r * max_h
        collage.paste(img_, (ox, oy), img_)
    
    leftover_cells = cells[n:]
    for (r, c) in leftover_cells:
        color_ = random_non_black_white()
        rect = Image.new("RGBA", (max_w, max_h), color_)
        collage.paste(rect, (c*max_w, r*max_h), rect)
    
    return limit_2048(collage)

def process_images(uploaded_files):
    pil_images = []
    for f in uploaded_files:
        if f is not None:
            img = Image.open(f.name).convert("RGBA")
            pil_images.append(img)
    
    results = []
    total = len(pil_images)
    groups_4 = total // 4
    leftover = total % 4
    
    idx = 0
    for _ in range(groups_4):
        group_4 = pil_images[idx:idx+4]
        idx += 4
        resized_4 = [resize_to_64_multiple(im) for im in group_4]
        max_w = max(im.size[0] for im in resized_4)
        max_h = max(im.size[1] for im in resized_4)
        
        final_4 = []
        for rimg in resized_4:
            w, h = rimg.size
            if (w,h) == (max_w, max_h):
                final_4.append(rimg)
            else:
                bg_color = rimg.getpixel((0,0))
                bg = Image.new("RGBA", (max_w, max_h), bg_color)
                offx = (max_w - w)//2
                offy = (max_h - h)//2
                bg.paste(rimg, (offx, offy), rimg)
                final_4.append(bg)
        
        collage_2x2 = make_collage_2x2(final_4)
        results.append(collage_2x2)
    
    if leftover > 0:
        leftover_imgs = pil_images[idx:]
        collage_left = make_collage_leftover(leftover_imgs)
        if collage_left is not None:
            results.append(collage_left)
    
    return results

def make_zip(uploaded_files):
    """
    真正构建ZIP,并返回临时文件路径(不是BytesIO),
    以防 Gradio 把 BytesIO 当成字符串解析而报错。
    """
    collages = process_images(uploaded_files)
    if not collages:
        return None  # 让前端做判断
    
    # 在内存里打包
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
        for i, img in enumerate(collages, start=1):
            img_bytes = io.BytesIO()
            img.save(img_bytes, format="PNG")
            img_bytes.seek(0)
            zf.writestr(f"collage_{i}.png", img_bytes.read())
    zip_buffer.seek(0)
    
    # 将 BytesIO 写入临时文件,再返回临时文件路径
    with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
        tmp.write(zip_buffer.getvalue())
        tmp_path = tmp.name
    
    return tmp_path

def on_zip_click(files):
    """
    返回 (zip_file_path, message) 给两个 output:
    1. gr.File 要求返回一个str路径或None
    2. gr.Textbox 用来输出提示信息
    """
    path = make_zip(files)
    if path is None:
        return (None, "无可下载内容 - 可能没上传图片或无法拼接")
    else:
        return (path, "打包完成!点击上方链接下载ZIP")

with gr.Blocks() as demo:
    gr.Markdown("## 2×2 拼接小工具(兼容不足4张、随机填充、保留透明)")
    with gr.Row():
        with gr.Column():
            file_input = gr.Files(label="上传多张图片(可多选)", file_types=["image"])
            preview_btn = gr.Button("生成预览")
            zip_btn = gr.Button("打包下载 ZIP")
        with gr.Column():
            gallery_out = gr.Gallery(label="拼接结果预览", columns=2)
            # 注意:必须 visible=True,这样点击后可以直接更新
            zip_file_out = gr.File(label="下载拼接结果 ZIP", visible=True, interactive=False)
            msg_out = gr.Textbox(label="提示信息", interactive=False)
    
    preview_btn.click(
        fn=process_images,
        inputs=[file_input],
        outputs=[gallery_out]
    )
    zip_btn.click(
        fn=on_zip_click,
        inputs=[file_input],
        outputs=[zip_file_out, msg_out]
    )

demo.launch()