From 7eb53a10e1090cd3551361d2b5bfab274d73078e Mon Sep 17 00:00:00 2001
From: sam <sam@cinelerra-gg.org>
Date: Wed, 14 Oct 2026 21:39:49 +0200
Subject: [PATCH] GIF: write a delay per image and a loop block into animations

FileGIF wrote animated GIFs without a graphics control extension, so
every image had a delay of 0. ffprobe saw a single image in a 50 image
file and viewers raced through the rest or showed only the first one.
Upstream writes the same file.

An animation (FileGIF on a path, not a frame of a GIF list) is now
GIF89a with a NETSCAPE2.0 block that loops forever and a delay per
image. The delay follows the running time in hundredths of a second:
25 fps gives 4 each, 30 fps the pattern 3,4,3. Delays under 2 are
raised to 2, since viewers slow those down.

Checked: 25 fps renders 50 images of 4/100 s, ffprobe reads 50 frames
at 25 fps and 2.0 s. A project with this GIF as its source renders
frame 25 identical to ffmpeg's frame 25. GIF list frames are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
 cinelerra-5.1/cinelerra/filegif.C | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/cinelerra-5.1/cinelerra/filegif.C b/cinelerra-5.1/cinelerra/filegif.C
index e6a54cac..0738f2aa 100644
--- a/cinelerra-5.1/cinelerra/filegif.C
+++ b/cinelerra-5.1/cinelerra/filegif.C
@@ -694,9 +694,40 @@ int FileGIF::write_frame(VFrame *frame)
 				result = 1;
 		}
 	}
+// An animation (written to a path, not a frame of a GIF list) needs GIF89a,
+// a loop block and a delay per image.  Without them every image had a delay
+// of 0: viewers raced through it or showed the first image only.
+	int anim = fd >= 0;
+	if( !result && !writes && anim )
+		EGifSetGifVersion(gif_file, true);
 	if( !result && !writes &&
 	    EGifPutScreenDesc(gif_file, w, h, depth, 0, 0) == GIF_ERROR )
 		result = 1;
+	if( !result && !writes && anim ) {
+// NETSCAPE2.0 block, loop count 0: repeat forever
+		static const GifByteType loop[] = { 1, 0, 0 };
+		if( EGifPutExtensionLeader(gif_file, APPLICATION_EXT_FUNC_CODE) == GIF_ERROR ||
+		    EGifPutExtensionBlock(gif_file, 11, "NETSCAPE2.0") == GIF_ERROR ||
+		    EGifPutExtensionBlock(gif_file, sizeof(loop), loop) == GIF_ERROR ||
+		    EGifPutExtensionTrailer(gif_file) == GIF_ERROR )
+			result = 1;
+	}
+	if( !result && anim ) {
+// The delay is in 1/100 s, taken from the running time: 30 fps gives 3,4,3.
+// Viewers slow down delays under 2, so faster rates play at 50 fps.
+		double rate = asset->frame_rate > 0 ? asset->frame_rate : 10;
+		int delay = (int)(100 * (writes + 1) / rate + 0.5) -
+			(int)(100 * writes / rate + 0.5);
+		GraphicsControlBlock gcb;
+		gcb.DisposalMode = DISPOSAL_UNSPECIFIED;
+		gcb.UserInputFlag = false;
+		gcb.DelayTime = delay < 2 ? 2 : delay;
+		gcb.TransparentColor = NO_TRANSPARENT_COLOR;
+		GifByteType ext[4];
+		size_t ext_len = EGifGCBToExtension(&gcb, ext);
+		if( EGifPutExtension(gif_file, GRAPHICS_EXT_FUNC_CODE, ext_len, ext) == GIF_ERROR )
+			result = 1;
+	}
 	if( !result &&
 	    EGifPutImageDesc(gif_file, 0, 0, w, h, 0, cmap) == GIF_ERROR )
 		result = 1;
-- 
2.43.0

