File size: 13,947 Bytes
144bf8c 9c11a59 144bf8c 6e6cb4c 144bf8c fbd7e7e 144bf8c 1259cb7 144bf8c |
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 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
#!/usr/bin/env python3
"""
png2gif.py βΒ Turn a directory of .png frames into an animated .gif
Usage:
python png2gif.py --src ./frames --out animation.gif --fps 12
"""
import argparse
from pathlib import Path
from PIL import Image
import re
import gradio as gr
import os
import tempfile
import zipfile
import shutil
import datetime
def natural_key(s):
"""Sort 'frame_2.png' before 'frame_10.png'."""
return [int(t) if t.isdigit() else t.lower() for t in re.split(r'(\d+)', s.name)]
def gather_frames(src_dir):
pngs = sorted(Path(src_dir).glob("*.png"), key=natural_key)
if not pngs:
return []
return pngs
def build_gif(pngs, out_file, fps, loop):
ms_per_frame = int(1000 / fps)
frames = [Image.open(png).convert("RGBA") for png in pngs]
# Warn if frame sizes differ
w, h = frames[0].size
if any(im.size != (w, h) for im in frames):
print("β οΈ Frame dimensions differ; resizing to first frameβs size")
frames = [im.resize((w, h), Image.LANCZOS) for im in frames]
frames[0].save(
out_file,
save_all=True,
append_images=frames[1:],
duration=ms_per_frame,
loop=loop,
disposal=2, # clears frame before drawing next (good for transparency)
optimize=True, # basic optimization
)
print(f"β
GIF saved to {out_file}")
def process_batch_directories(parent_dir, fps, loop_count):
parent_path = Path(parent_dir)
if not parent_path.exists():
print(f"β Parent directory not found: {parent_dir}")
return
if not parent_path.is_dir():
print(f"β Path is not a directory: {parent_dir}")
return
# Find all subdirectories
subdirs = [d for d in parent_path.iterdir() if d.is_dir()]
if not subdirs:
print(f"β No subdirectories found in {parent_dir}")
return
print(f"π Found {len(subdirs)} subdirectories to process")
successful = 0
failed = 0
for subdir in subdirs:
print(f"\nπ Processing: {subdir.name}")
# Gather PNG frames from this subdirectory
pngs = gather_frames(subdir)
if not pngs:
print(f"β οΈ No PNG files found in {subdir.name}, skipping")
continue
# Create GIF filename based on subdirectory name
gif_name = f"{subdir.name}.gif"
output_path = subdir / gif_name
try:
build_gif(pngs, output_path, fps, loop_count)
print(f"β
Created {gif_name} with {len(pngs)} frames")
successful += 1
except Exception as e:
print(f"β Failed to create GIF for {subdir.name}: {e}")
failed += 1
# Summary
print(f"\nπ Batch processing complete:")
print(f" β
Successful: {successful}")
print(f" β Failed: {failed}")
print(f" π Total processed: {successful + failed}")
def process_uploaded_files(files, fps, loop_count):
if not files:
return None, None, "Please upload PNG files"
with tempfile.TemporaryDirectory() as temp_dir:
png_files = []
for file in files:
if not file.name.lower().endswith('.png'):
continue
# Gradio files have a .name attribute that contains the file path
source_path = Path(file.name)
dest_path = Path(temp_dir) / source_path.name
# Copy the file from the temporary location Gradio created
shutil.copy2(source_path, dest_path)
png_files.append(dest_path)
if not png_files:
return None, None, "No valid PNG files found"
png_files = sorted(png_files, key=natural_key)
temp_output = Path(temp_dir) / "animation.gif"
try:
build_gif(png_files, temp_output, fps, loop_count)
# Create a persistent temporary file for Gradio to access
persistent_output = tempfile.NamedTemporaryFile(suffix=".gif", delete=False)
persistent_output.close()
# Copy the generated GIF to the persistent location
shutil.copy2(temp_output, persistent_output.name)
# Optional: build filename for display only
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename_for_display = f"animation_{timestamp}.gif"
return persistent_output.name, persistent_output.name, f"β
GIF created successfully with {len(png_files)} frames\nπ Filename: {filename_for_display}"
except Exception as e:
return None, None, f"β Error creating GIF: {str(e)}"
def process_batch_zip(zip_file, fps, loop_count):
if not zip_file:
return "No ZIP file uploaded", [], None
with tempfile.TemporaryDirectory() as temp_dir:
try:
# Extract ZIP file
zip_path = Path(zip_file.name)
extract_dir = Path(temp_dir) / "extracted"
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
# Find subdirectories containing PNG files
subdirs = []
for item in extract_dir.rglob("*"):
if item.is_dir():
pngs = list(item.glob("*.png"))
if pngs:
subdirs.append(item)
if not subdirs:
return "β No subdirectories with PNG files found in ZIP", [], None
progress_msg = f"π Found {len(subdirs)} directories to process"
generated_gifs = []
results_dir = Path(temp_dir) / "results"
results_dir.mkdir()
successful = 0
failed = 0
for i, subdir in enumerate(subdirs, 1):
progress_msg += f"\nπ Processing {i}/{len(subdirs)}: {subdir.name}"
# Gather PNG frames
pngs = gather_frames(subdir)
if not pngs:
progress_msg += f"\nβ οΈ No PNG files found in {subdir.name}, skipping"
continue
# Create GIF
gif_name = f"{subdir.name}.gif"
gif_path = results_dir / gif_name
try:
build_gif(pngs, gif_path, fps, loop_count)
# Create persistent copy for gallery display
persistent_gif = tempfile.NamedTemporaryFile(suffix=".gif", delete=False)
persistent_gif.close()
shutil.copy2(gif_path, persistent_gif.name)
generated_gifs.append(persistent_gif.name)
progress_msg += f"\nβ
Created {gif_name} with {len(pngs)} frames"
successful += 1
except Exception as e:
progress_msg += f"\nβ Failed to create GIF for {subdir.name}: {e}"
failed += 1
# Create results ZIP
if generated_gifs:
results_zip = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
results_zip.close()
with zipfile.ZipFile(results_zip.name, 'w') as zip_out:
for gif_path in results_dir.glob("*.gif"):
zip_out.write(gif_path, gif_path.name)
progress_msg += f"\n\nπ Batch processing complete:"
progress_msg += f"\n β
Successful: {successful}"
progress_msg += f"\n β Failed: {failed}"
progress_msg += f"\n π¦ Results ZIP created with {len(generated_gifs)} GIFs"
return progress_msg, generated_gifs, results_zip.name
else:
return "β No GIFs were successfully created", [], None
except Exception as e:
return f"β Error processing ZIP file: {str(e)}", [], None
def toggle_mode(mode):
if mode == "Single Sequence":
return (
gr.update(visible=True), # single_group
gr.update(visible=False), # batch_group
gr.update(visible=True), # single_output
gr.update(visible=False) # batch_output
)
else:
return (
gr.update(visible=False), # single_group
gr.update(visible=True), # batch_group
gr.update(visible=False), # single_output
gr.update(visible=True) # batch_output
)
def process_conversion(mode, files, zip_file, fps, loop_count):
if mode == "Single Sequence":
if files:
preview, download, status = process_uploaded_files(files, fps, loop_count)
return preview, download, status, "", [], None
else:
return None, None, "Please upload PNG files", "", [], None
else: # Batch Processing
if zip_file:
progress, gallery, zip_download = process_batch_zip(zip_file, fps, loop_count)
return None, None, "", progress, gallery, zip_download
else:
return None, None, "", "Please upload a ZIP file", [], None
def create_gradio_interface():
with gr.Blocks(title="PNG to GIF Converter") as demo:
gr.Markdown("# PNG to GIF Converter")
gr.Markdown("Create animated GIFs from PNG sequences")
# Mode selector
mode_selector = gr.Radio(
choices=["Single Sequence", "Batch Processing"],
value="Single Sequence",
label="Processing Mode"
)
gr.Markdown("**Single**: Upload individual PNG files | **Batch**: Upload ZIP with subdirectories")
with gr.Row():
with gr.Column():
# Single mode components
with gr.Group(visible=True) as single_group:
files_input = gr.File(
label="Upload PNG Files",
file_count="multiple",
file_types=[".png"]
)
# Batch mode components
with gr.Group(visible=False) as batch_group:
gr.Markdown("ZIP should contain subdirectories, each with PNG files")
zip_input = gr.File(
label="Upload ZIP File",
file_count="single",
file_types=[".zip"]
)
# Common controls
with gr.Row():
fps_input = gr.Slider(
minimum=1,
maximum=60,
value=12,
step=1,
label="Frames Per Second (FPS)"
)
loop_input = gr.Slider(
minimum=0,
maximum=10,
value=0,
step=1,
label="Loop Count (0 = infinite)"
)
convert_btn = gr.Button("Convert to GIF", variant="primary")
with gr.Column():
# Single mode outputs
with gr.Group(visible=True) as single_output:
gif_preview = gr.Image(label="GIF Preview", type="filepath")
output_file = gr.File(label="Download GIF")
# Batch mode outputs
with gr.Group(visible=False) as batch_output:
batch_progress = gr.Textbox(label="Progress", interactive=False)
gif_gallery = gr.Gallery(label="Generated GIFs", columns=3, rows=2)
batch_download = gr.File(label="Download All GIFs (ZIP)")
status_text = gr.Textbox(label="Status", interactive=False)
# Mode switching logic
mode_selector.change(
fn=toggle_mode,
inputs=[mode_selector],
outputs=[single_group, batch_group, single_output, batch_output]
)
# Routing function to handle both modes
convert_btn.click(
fn=process_conversion,
inputs=[mode_selector, files_input, zip_input, fps_input, loop_input],
outputs=[gif_preview, output_file, status_text, batch_progress, gif_gallery, batch_download]
)
return demo
def main():
# Check if running in a Hugging Face Space
if os.getenv("SYSTEM") == "spaces":
demo = create_gradio_interface()
demo.launch()
return
ap = argparse.ArgumentParser()
ap.add_argument("--src", default=".", help="Folder with PNG frames or parent directory for batch mode")
ap.add_argument("--out", default="animation.gif", help="Output GIF file")
ap.add_argument("--fps", type=int, default=12, help="Frames per second")
ap.add_argument("--loop", type=int, default=0, help="Loop count (0 = infinite)")
ap.add_argument("--web", action="store_true", help="Launch web interface")
ap.add_argument("--batch", action="store_true", help="Batch process subdirectories")
args = ap.parse_args()
if args.web:
demo = create_gradio_interface()
demo.launch(share=True)
elif args.batch:
process_batch_directories(args.src, args.fps, args.loop)
else:
pngs = gather_frames(args.src)
if not pngs:
print(f"β No PNG files found in {args.src}")
return
build_gif(pngs, args.out, args.fps, args.loop)
if __name__ == "__main__":
main()
|