Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gif): Add GIF animation loop count detection for animated WebP conversion #199

Merged
merged 2 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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