from PIL import Image, ImageSequence
im = Image.open(src)
frames = [f.copy() for f in ImageSequence.Iterator(im)]
for f in frames: f.paste(wm, pos, wm)
frames[0].save(dst, save_all=True, append_images=frames[1:], duration=im.info.get("duration", 100), loop=0)

给 24 帧的 GIF 加水印,输出文件只有第一帧,播放器里完全不动。Pillow 打开时 n_frames 是 24,保存出来再读变成 1,文件体积从 1.8MB 掉到 90KB。先别急着动参数,把报错原文和时间点记下来,顺序反了容易白折腾。

原因不复杂,代码里对 Image.open 的返回值直接 paste 后 save,没有逐帧迭代。Pillow 默认只暴露当前帧,save 也就只写一帧。动图信息在 seek 到后续帧时才加载,不遍历就丢光了。

「GIF 多帧丢失」排查到最后往往是很小的一处差异,前面那些复杂猜测可以先放一放。动手前把原文件复制一份带时间戳,出问题立刻换回去,比现场分析快得多。

一次只改一处,改完立刻验证,几个改动叠在一起,出问题就说不清是谁导致的。先做最小复现,能稳定跑出来的问题,排查成本会低一个数量级。

处理上按这个顺序:用 ImageSequence.Iterator 逐帧处理 每帧都贴上水印;保存时传 save_all=True 和 append_images;保留原图的 duration 和 loop 参数 否则速度会变;处理完读回 n_frames 核对帧数。

改完只确认一件事:输出 GIF 读回 n_frames 仍是 24,播放速度和原图一致,水印在每一帧都出现。

作者 9823189