"""Convert GIFs to 360x360 for the JC3636W518 (round 360x360 display). Each frame is scaled so the GIF covers 360x360, then center-cropped. Frame durations and looping are kept. Pillow stores only the changed part of each frame, which keeps files small. Usage: pip install pillow python gif_to_360.py [more ...] -o The device stores one background GIF in an 11.5 MB flash area: bigger results are reported as too large. """ import argparse import os import sys from PIL import Image, ImageSequence SIZE = 360 MAX_BYTES = 11_500_000 def convert(src, dst): im = Image.open(src) w, h = im.size scale = max(SIZE / w, SIZE / h) nw, nh = max(SIZE, round(w * scale)), max(SIZE, round(h * scale)) left, top = (nw - SIZE) // 2, (nh - SIZE) // 2 frames, durations = [], [] for frame in ImageSequence.Iterator(im): durations.append(frame.info.get("duration", im.info.get("duration", 100)) or 100) rgba = frame.convert("RGBA") # fully composited frame if (nw, nh) != (w, h): rgba = rgba.resize((nw, nh), Image.LANCZOS) rgba = rgba.crop((left, top, left + SIZE, top + SIZE)) # the device shows the GIF on black: flatten transparency onto black bg = Image.new("RGBA", rgba.size, (0, 0, 0, 255)) bg.alpha_composite(rgba) frames.append(bg.convert("RGB")) frames[0].save(dst, save_all=True, append_images=frames[1:], duration=durations, loop=im.info.get("loop", 0), optimize=True, disposal=1) size = os.path.getsize(dst) fps = len(durations) * 1000 / max(1, sum(durations)) note = "" if size <= MAX_BYTES else " ** TOO LARGE for the device (max 11.5 MB) **" print(f"{os.path.basename(src)}: {w}x{h}, {len(frames)} frames, {fps:.1f} fps " f"-> {os.path.basename(dst)} {size / 1e6:.2f} MB{note}") def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("inputs", nargs="+", help="GIF files or folders") ap.add_argument("-o", "--out", required=True, help="output folder") args = ap.parse_args() os.makedirs(args.out, exist_ok=True) files = [] for p in args.inputs: if os.path.isdir(p): files += [os.path.join(p, f) for f in sorted(os.listdir(p)) if f.lower().endswith(".gif")] else: files.append(p) if not files: sys.exit("no GIF files found") for f in files: name = os.path.splitext(os.path.basename(f))[0] convert(f, os.path.join(args.out, name + ".gif")) if __name__ == "__main__": main()