#2070

slack-gif-creator: add_frame retains mutable NumPy arrays, replacing earlier animation frames

OpenBichengWangopened 7d ago

GIFBuilder.add_frame() stores a same-sized NumPy array by reference. Reusing a canvas to build an animation changes all previously added frames, so saving a red/green/blue sequence produces one blue frame. PIL inputs and arrays that need resizing are copied, making the behavior depend on the input type and dimensions.

Reproduced at dbd4588f9e1033efb41dad4bef2f7947c8993d44 with Python 3.11.17, NumPy 2.4.6, Pillow 13.0.0 and imageio 2.38.1. Run from skills/slack-gif-creator:

import tempfile
from pathlib import Path

import numpy as np
from PIL import Image, ImageSequence
from core.gif_builder import GIFBuilder

builder = GIFBuilder(width=128, height=128, fps=10)
canvas = np.zeros((128, 128, 3), dtype=np.uint8)
for color in [(255, 0, 0), (0, 255, 0), (0, 0, 255)]:
    canvas[:] = color
    builder.add_frame(canvas)

print([frame[0, 0].tolist() for frame in builder.frames])
with tempfile.TemporaryDirectory() as directory:
    output = Path(directory) / "animation.gif"
    builder.save(output)
    with Image.open(output) as image:
        print(image.n_frames)
        print([f.convert("RGB").getpixel((0, 0))
               for f in ImageSequence.Iterator(image)])

Actual: stored pixels are [[0, 0, 255], [0, 0, 255], [0, 0, 255]]; the saved GIF has one frame, colored blue. Expected: the three colors remain in order after adding each frame, and the GIF contains all three.

As controls, passing canvas.copy() produces the expected three-frame GIF, as does reusing a PIL image or a 64×64 array that the builder resizes. This uses RGB arrays and leaves duplicate removal disabled, so it is separate from #1886's RGBA/grayscale conversion failure and #1901's duplicate-removal timing change. Copying the array when accepting a frame would give these input paths consistent snapshot behavior; canvas.copy() is a caller-side workaround.

Activity

1
ashwinruke6d ago

Hi @BichengWang, thanks for the clear write-up and reproduction. I'd like to work on this if it's still available.

I plan to copy the NumPy array in GIFBuilder.add_frame() so the same-size array path behaves like the PIL and resize paths, and add a regression test based on your red/green/blue example. I'll keep the change separate from #1886 and #1901.

If anyone is already working on this, or the maintainers would prefer a different approach, please let me know. Otherwise I'll open a PR soon.