-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathimageConverter.py
220 lines (194 loc) · 7.26 KB
/
imageConverter.py
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
214
215
216
217
218
219
220
import argparse
from PIL import Image, ImageSequence
# Pillow resampling fallback for older versions
try:
from PIL.Image import Resampling
RESAMPLE_FILTER = Resampling.LANCZOS
except ImportError:
from PIL import Image
RESAMPLE_FILTER = Image.ANTIALIAS
def guess_best_mode(frame_count):
"""
Auto-select mode based on the total number of frames:
- <= 4 -> 2x2 (capacity=4)
- <= 16 -> 4x4 (capacity=16)
- else -> 8x8 (capacity=64)
"""
if frame_count <= 4:
return "2x2"
elif frame_count <= 16:
return "4x4"
else:
return "8x8"
def divisors_of(n):
"""
Return all divisors of n in ascending order.
Example: divisors_of(16) -> [1, 2, 4, 8, 16]
"""
result = []
for i in range(1, int(n**0.5) + 1):
if n % i == 0:
result.append(i)
if i != n // i:
result.append(n // i)
return sorted(result)
def sample_frames(frames, keep_count, method):
"""
Pick exactly 'keep_count' frames from 'frames' using:
- 'consecutive': the first keep_count frames
- 'percentage': evenly spaced frames across the entire length
"""
frame_count = len(frames)
if frame_count <= keep_count:
# No need to sample
return frames[:keep_count]
if method == "consecutive":
return frames[:keep_count]
else: # "percentage"
step = frame_count / float(keep_count)
indices = [int(i * step) for i in range(keep_count)]
return [frames[i] for i in indices]
def letterbox_frame(frame, target_w, target_h):
"""
Resizes 'frame' to fit within (target_w x target_h)
while preserving aspect ratio; the rest is transparent.
"""
original_w, original_h = frame.size
aspect_frame = original_w / float(original_h)
aspect_target = target_w / float(target_h)
if aspect_frame > aspect_target:
# Width-limited
new_w = target_w
new_h = int(target_w / aspect_frame)
else:
# Height-limited
new_h = target_h
new_w = int(target_h * aspect_frame)
resized = frame.resize((new_w, new_h), RESAMPLE_FILTER)
# Create transparent background of the cell size
background = Image.new("RGBA", (target_w, target_h), (0, 0, 0, 0))
offset_x = (target_w - new_w) // 2
offset_y = (target_h - new_h) // 2
background.paste(resized, (offset_x, offset_y))
return background
def create_spritesheet(gif_path, layout_mode, method, output_path):
"""
Creates a 1024x1024 PNG spritesheet from a GIF with:
- auto layout selection (2x2, 4x4, 8x8),
- 'consecutive' or 'percentage' selection when discarding frames,
- largest-divisor logic when fewer frames than capacity,
- letterboxing for each frame tile.
"""
# 1) Read GIF frames
gif = Image.open(gif_path)
frames = [f.copy().convert("RGBA") for f in ImageSequence.Iterator(gif)]
frame_count = len(frames)
# 2) Resolve layout mode (auto or user-chosen)
if layout_mode == "auto":
layout_mode = guess_best_mode(frame_count)
layout_map = {
'2x2': (2, 2, 512, 512), # capacity=4
'4x4': (4, 4, 256, 256), # capacity=16
'8x8': (8, 8, 128, 128) # capacity=64
}
if layout_mode not in layout_map:
raise ValueError("Invalid mode. Choose '2x2', '4x4', '8x8', or 'auto'.")
grid_w, grid_h, tile_w, tile_h = layout_map[layout_mode]
total_slots = grid_w * grid_h
print(f"\nSelected mode: {layout_mode}")
print(f"Total frames in GIF: {frame_count}, Layout capacity: {total_slots}")
print(f"Method for discarding frames (if needed): {method}\n")
# 3) If the GIF has *more* frames than capacity
if frame_count > total_slots:
# We'll just keep 'total_slots' worth of frames,
# but do we want to ensure it's a multiple? Up to you.
# This example just picks total_slots frames directly.
print("More frames than capacity -> sampling down.")
frames = sample_frames(frames, total_slots, method)
frame_count = len(frames)
print(f"Now using {frame_count} frames.\n")
# 4) If the GIF has *fewer* frames than capacity
elif frame_count < total_slots:
divs = divisors_of(total_slots) # e.g., [1,2,4,8,16, ...]
valid_divs = [d for d in divs if d <= frame_count]
if valid_divs:
# Largest divisor <= frame_count
d = max(valid_divs)
# We pick 'd' frames from the original, using the method
if d < frame_count:
# Discard some frames to get exactly d
leftover = frame_count - d
print(f"Fewer frames than capacity -> largest divisor is {d}.")
print(f"Discarding {leftover} frames via '{method}' sampling to keep {d}.")
frames = sample_frames(frames, d, method)
frame_count = len(frames)
# Now replicate these d frames
repeats = total_slots // d
repeated_frames = []
print(f"Repeating these {d} frames {repeats} times to fill {total_slots}.")
for _ in range(repeats):
repeated_frames.extend(frames)
frames = repeated_frames
frame_count = len(frames)
else:
# No divisor found other than possibly 1
print("No valid divisor found (other than 1). We'll repeat all frames.")
repeated_frames = []
idx = 0
for _ in range(total_slots):
repeated_frames.append(frames[idx])
idx = (idx + 1) % frame_count
frames = repeated_frames
frame_count = len(frames)
print(f"\nNow using {frame_count} frames.\n")
else:
print("Exact match of frames to capacity. No sampling needed.\n")
# 5) Create a blank 1024x1024 canvas
spritesheet = Image.new("RGBA", (1024, 1024), (0, 0, 0, 0))
# 6) Letterbox each frame, then paste
index = 0
for row in range(grid_h):
for col in range(grid_w):
letterboxed = letterbox_frame(frames[index], tile_w, tile_h)
pos_x = col * tile_w
pos_y = row * tile_h
spritesheet.paste(letterboxed, (pos_x, pos_y))
index += 1
# 7) Save
spritesheet.save(output_path, "PNG")
print(f"Spritesheet saved to: {output_path}\n")
def main():
parser = argparse.ArgumentParser(
description="Convert a GIF into a 1024x1024 PNG spritesheet with letterboxing."
)
parser.add_argument(
"--input",
required=True,
help="Path to the input GIF file."
)
parser.add_argument(
"--mode",
choices=["2x2", "4x4", "8x8", "auto"],
default="auto",
help="Spritesheet layout: 2x2, 4x4, 8x8, or auto."
)
parser.add_argument(
"--method",
choices=["consecutive", "percentage"],
default="consecutive",
help="How to handle discarding frames: consecutive or percentage."
)
parser.add_argument(
"--output",
default="spritesheet.png",
help="Path to the output PNG file."
)
args = parser.parse_args()
create_spritesheet(
gif_path=args.input,
layout_mode=args.mode,
method=args.method,
output_path=args.output
)
if __name__ == "__main__":
main()