Skip to content

Commit 2f2bf2a

Browse files
committed
feat(gif): Add GIF animation loop count detection for animated WebP conversion (#199)
1 parent 9604962 commit 2f2bf2a

7 files changed

+123
-5
lines changed

giflib.cpp

+74
Original file line numberDiff line numberDiff line change
@@ -1112,3 +1112,77 @@ int giflib_encoder_get_output_length(giflib_encoder e)
11121112
{
11131113
return e->dst_offset;
11141114
}
1115+
1116+
int giflib_decoder_get_loop_count(const giflib_decoder d) {
1117+
// Default to 1 loop (play once) if no NETSCAPE2.0 extension is found
1118+
int loop_count = 1;
1119+
1120+
// Create a temporary decoder to read extension blocks
1121+
// We need a separate decoder because reading extension blocks modifies decoder state
1122+
giflib_decoder loopReader = new struct giflib_decoder_struct();
1123+
if (!loopReader) {
1124+
return loop_count; // Return default on allocation failure
1125+
}
1126+
1127+
memset(loopReader, 0, sizeof(struct giflib_decoder_struct));
1128+
loopReader->mat = d->mat; // Share the source data
1129+
1130+
int error = 0;
1131+
GifFileType* gif = DGifOpen(loopReader, decode_func, &error);
1132+
if (error) {
1133+
delete loopReader;
1134+
return loop_count;
1135+
}
1136+
1137+
// Read all blocks until we find NETSCAPE2.0 or hit end
1138+
GifRecordType recordType;
1139+
while (DGifGetRecordType(gif, &recordType) == GIF_OK) {
1140+
switch (recordType) {
1141+
case EXTENSION_RECORD_TYPE: {
1142+
GifByteType* ExtData;
1143+
int ExtFunction;
1144+
1145+
if (DGifGetExtension(gif, &ExtFunction, &ExtData) == GIF_OK && ExtData != NULL) {
1146+
// Look for NETSCAPE2.0 extension
1147+
if (ExtFunction == APPLICATION_EXT_FUNC_CODE && ExtData[0] >= 11 &&
1148+
memcmp(ExtData + 1, "NETSCAPE2.0", 11) == 0) {
1149+
// Get the next block with loop count
1150+
if (DGifGetExtensionNext(gif, &ExtData) == GIF_OK &&
1151+
ExtData != NULL &&
1152+
ExtData[0] >= 3 &&
1153+
ExtData[1] == 1) {
1154+
loop_count = ExtData[2] | (ExtData[3] << 8);
1155+
goto cleanup; // Found what we need
1156+
}
1157+
}
1158+
1159+
// Skip any remaining extension blocks
1160+
while (ExtData != NULL) {
1161+
if (DGifGetExtensionNext(gif, &ExtData) != GIF_OK) {
1162+
goto cleanup;
1163+
}
1164+
}
1165+
}
1166+
break;
1167+
}
1168+
1169+
case IMAGE_DESC_RECORD_TYPE:
1170+
// Skip image data
1171+
if (DGifGetImageDesc(gif) != GIF_OK) {
1172+
goto cleanup;
1173+
}
1174+
break;
1175+
1176+
case TERMINATE_RECORD_TYPE:
1177+
goto cleanup;
1178+
1179+
default:
1180+
break;
1181+
}
1182+
}
1183+
1184+
cleanup:
1185+
DGifCloseFile(gif, &error);
1186+
delete loopReader;
1187+
return loop_count;
1188+
}

giflib.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import (
1212
)
1313

1414
type gifDecoder struct {
15-
decoder C.giflib_decoder
16-
mat C.opencv_mat
17-
buf []byte
18-
frameIndex int
15+
decoder C.giflib_decoder
16+
mat C.opencv_mat
17+
buf []byte
18+
frameIndex int
19+
loopCount int
20+
loopCountRead bool
1921
}
2022

2123
type gifEncoder struct {
@@ -114,7 +116,11 @@ func (d *gifDecoder) BackgroundColor() uint32 {
114116
}
115117

116118
func (d *gifDecoder) LoopCount() int {
117-
return 0 // loop indefinitely
119+
if !d.loopCountRead {
120+
d.loopCount = int(C.giflib_decoder_get_loop_count(d.decoder))
121+
d.loopCountRead = true
122+
}
123+
return d.loopCount
118124
}
119125

120126
func (d *gifDecoder) DecodeTo(f *Framebuffer) error {

giflib.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ bool giflib_encoder_encode_frame(giflib_encoder e, const giflib_decoder d, const
3434
bool giflib_encoder_flush(giflib_encoder e, const giflib_decoder d);
3535
void giflib_encoder_release(giflib_encoder e);
3636
int giflib_encoder_get_output_length(giflib_encoder e);
37+
int giflib_decoder_get_loop_count(const giflib_decoder d);
3738

3839
#ifdef __cplusplus
3940
}

testdata/dispose_bgnd.gif

1.42 KB
Loading
1.58 KB
Loading

testdata/no-loop.gif

93.3 KB
Loading

webp_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ func testNewWebpEncoderWithAnimatedGIFSource(t *testing.T) {
391391
height int
392392
quality int
393393
resizeMethod ImageOpsSizeMethod
394+
wantLoops int
394395
}{
395396
{
396397
name: "Animated GIF with alpha channel",
@@ -400,6 +401,37 @@ func testNewWebpEncoderWithAnimatedGIFSource(t *testing.T) {
400401
height: 17,
401402
quality: 80,
402403
resizeMethod: ImageOpsResize,
404+
wantLoops: 0,
405+
},
406+
{
407+
name: "Animated GIF with specific loop count",
408+
inputPath: "testdata/no-loop.gif",
409+
outputPath: "testdata/out/no-loop_out.webp",
410+
width: 200,
411+
height: 200,
412+
quality: 80,
413+
resizeMethod: ImageOpsResize,
414+
wantLoops: 1,
415+
},
416+
{
417+
name: "Animated GIF with duplicate number of loop count, use the first loop count",
418+
inputPath: "testdata/duplicate_number_of_loops.gif",
419+
outputPath: "testdata/out/duplicate_number_of_loops.webp",
420+
width: 200,
421+
height: 200,
422+
quality: 80,
423+
resizeMethod: ImageOpsResize,
424+
wantLoops: 2,
425+
},
426+
{
427+
name: "Animated GIF with multiple extension blocks",
428+
inputPath: "testdata/dispose_bgnd.gif",
429+
outputPath: "testdata/out/dispose_bgnd.webp",
430+
width: 200,
431+
height: 200,
432+
quality: 80,
433+
resizeMethod: ImageOpsResize,
434+
wantLoops: 0,
403435
},
404436
}
405437

@@ -422,6 +454,11 @@ func testNewWebpEncoderWithAnimatedGIFSource(t *testing.T) {
422454
return
423455
}
424456

457+
// Verify loop count
458+
if decoder.LoopCount() != tc.wantLoops {
459+
t.Errorf("Loop count = %d, want %d", decoder.LoopCount(), tc.wantLoops)
460+
}
461+
425462
dstBuf := make([]byte, destinationBufferSize)
426463

427464
options := &ImageOptions{

0 commit comments

Comments
 (0)