Skip to content

Commit

Permalink
feat(gif): Add GIF animation loop count detection for animated WebP c…
Browse files Browse the repository at this point in the history
…onversion (#199)
  • Loading branch information
skidder authored Nov 7, 2024
1 parent 9ccf8e8 commit 2a8aa7e
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 5 deletions.
74 changes: 74 additions & 0 deletions giflib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1112,3 +1112,77 @@ int giflib_encoder_get_output_length(giflib_encoder e)
{
return e->dst_offset;
}

int giflib_decoder_get_loop_count(const giflib_decoder d) {
// Default to 1 loop (play once) if no NETSCAPE2.0 extension is found
int loop_count = 1;

// Create a temporary decoder to read extension blocks
// We need a separate decoder because reading extension blocks modifies decoder state
giflib_decoder loopReader = new struct giflib_decoder_struct();
if (!loopReader) {
return loop_count; // Return default on allocation failure
}

memset(loopReader, 0, sizeof(struct giflib_decoder_struct));
loopReader->mat = d->mat; // Share the source data

int error = 0;
GifFileType* gif = DGifOpen(loopReader, decode_func, &error);
if (error) {
delete loopReader;
return loop_count;
}

// Read all blocks until we find NETSCAPE2.0 or hit end
GifRecordType recordType;
while (DGifGetRecordType(gif, &recordType) == GIF_OK) {
switch (recordType) {
case EXTENSION_RECORD_TYPE: {
GifByteType* ExtData;
int ExtFunction;

if (DGifGetExtension(gif, &ExtFunction, &ExtData) == GIF_OK && ExtData != NULL) {
// Look for NETSCAPE2.0 extension
if (ExtFunction == APPLICATION_EXT_FUNC_CODE && ExtData[0] >= 11 &&
memcmp(ExtData + 1, "NETSCAPE2.0", 11) == 0) {
// Get the next block with loop count
if (DGifGetExtensionNext(gif, &ExtData) == GIF_OK &&
ExtData != NULL &&
ExtData[0] >= 3 &&
ExtData[1] == 1) {
loop_count = ExtData[2] | (ExtData[3] << 8);
goto cleanup; // Found what we need
}
}

// Skip any remaining extension blocks
while (ExtData != NULL) {
if (DGifGetExtensionNext(gif, &ExtData) != GIF_OK) {
goto cleanup;
}
}
}
break;
}

case IMAGE_DESC_RECORD_TYPE:
// Skip image data
if (DGifGetImageDesc(gif) != GIF_OK) {
goto cleanup;
}
break;

case TERMINATE_RECORD_TYPE:
goto cleanup;

default:
break;
}
}

cleanup:
DGifCloseFile(gif, &error);
delete loopReader;
return loop_count;
}
16 changes: 11 additions & 5 deletions giflib.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (
)

type gifDecoder struct {
decoder C.giflib_decoder
mat C.opencv_mat
buf []byte
frameIndex int
decoder C.giflib_decoder
mat C.opencv_mat
buf []byte
frameIndex int
loopCount int
loopCountRead bool
}

type gifEncoder struct {
Expand Down Expand Up @@ -114,7 +116,11 @@ func (d *gifDecoder) BackgroundColor() uint32 {
}

func (d *gifDecoder) LoopCount() int {
return 0 // loop indefinitely
if !d.loopCountRead {
d.loopCount = int(C.giflib_decoder_get_loop_count(d.decoder))
d.loopCountRead = true
}
return d.loopCount
}

func (d *gifDecoder) DecodeTo(f *Framebuffer) error {
Expand Down
1 change: 1 addition & 0 deletions giflib.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ bool giflib_encoder_encode_frame(giflib_encoder e, const giflib_decoder d, const
bool giflib_encoder_flush(giflib_encoder e, const giflib_decoder d);
void giflib_encoder_release(giflib_encoder e);
int giflib_encoder_get_output_length(giflib_encoder e);
int giflib_decoder_get_loop_count(const giflib_decoder d);

#ifdef __cplusplus
}
Expand Down
Binary file added testdata/dispose_bgnd.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/duplicate_number_of_loops.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/no-loop.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions webp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ func testNewWebpEncoderWithAnimatedGIFSource(t *testing.T) {
height int
quality int
resizeMethod ImageOpsSizeMethod
wantLoops int
}{
{
name: "Animated GIF with alpha channel",
Expand All @@ -400,6 +401,37 @@ func testNewWebpEncoderWithAnimatedGIFSource(t *testing.T) {
height: 17,
quality: 80,
resizeMethod: ImageOpsResize,
wantLoops: 0,
},
{
name: "Animated GIF with specific loop count",
inputPath: "testdata/no-loop.gif",
outputPath: "testdata/out/no-loop_out.webp",
width: 200,
height: 200,
quality: 80,
resizeMethod: ImageOpsResize,
wantLoops: 1,
},
{
name: "Animated GIF with duplicate number of loop count, use the first loop count",
inputPath: "testdata/duplicate_number_of_loops.gif",
outputPath: "testdata/out/duplicate_number_of_loops.webp",
width: 200,
height: 200,
quality: 80,
resizeMethod: ImageOpsResize,
wantLoops: 2,
},
{
name: "Animated GIF with multiple extension blocks",
inputPath: "testdata/dispose_bgnd.gif",
outputPath: "testdata/out/dispose_bgnd.webp",
width: 200,
height: 200,
quality: 80,
resizeMethod: ImageOpsResize,
wantLoops: 0,
},
}

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

// Verify loop count
if decoder.LoopCount() != tc.wantLoops {
t.Errorf("Loop count = %d, want %d", decoder.LoopCount(), tc.wantLoops)
}

dstBuf := make([]byte, destinationBufferSize)

options := &ImageOptions{
Expand Down

0 comments on commit 2a8aa7e

Please sign in to comment.