Skip to content

Commit db97a9c

Browse files
committed
[feat] Report duration for animated GIF and WebP images. (#204)
1 parent b76996e commit db97a9c

File tree

9 files changed

+172
-19
lines changed

9 files changed

+172
-19
lines changed

giflib.cpp

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,7 +1132,7 @@ int giflib_encoder_get_output_length(giflib_encoder e)
11321132

11331133
struct GifAnimationInfo giflib_decoder_get_animation_info(const giflib_decoder d) {
11341134
// Default to 1 loop (play once) if no NETSCAPE2.0 extension is found
1135-
GifAnimationInfo info = {1, 0, 255, 255, 255, 0}; // loop_count, frame_count, bg_r, bg_g, bg_b, bg_a
1135+
GifAnimationInfo info = {1, 0, 255, 255, 255, 0, 0}; // loop_count, frame_count, bg_r, bg_g, bg_b, bg_a, duration_ms
11361136

11371137
// Create a temporary decoder to read extension blocks
11381138
giflib_decoder loopReader = new struct giflib_decoder_struct();
@@ -1163,18 +1163,28 @@ struct GifAnimationInfo giflib_decoder_get_animation_info(const giflib_decoder d
11631163
int ExtFunction;
11641164

11651165
if (DGifGetExtension(gif, &ExtFunction, &ExtData) == GIF_OK && ExtData != NULL) {
1166-
// Look for GraphicsControlBlock if we haven't found it yet
1167-
if (!found_gcb && ExtFunction == GRAPHICS_EXT_FUNC_CODE) {
1168-
found_gcb = true;
1169-
DGifExtensionToGCB(ExtData[0], &ExtData[1], &gcb);
1170-
// Get background color as soon as we have the GCB
1171-
uint8_t bg_red, bg_green, bg_blue, bg_alpha;
1172-
extract_background_color(gif, &gcb, &bg_red, &bg_green,
1173-
&bg_blue, &bg_alpha);
1174-
info.bg_red = bg_red;
1175-
info.bg_green = bg_green;
1176-
info.bg_blue = bg_blue;
1177-
info.bg_alpha = bg_alpha;
1166+
// Check for GraphicsControlBlock to get frame delay
1167+
if (ExtFunction == GRAPHICS_EXT_FUNC_CODE) {
1168+
GraphicsControlBlock frame_gcb;
1169+
DGifExtensionToGCB(ExtData[0], &ExtData[1], &frame_gcb);
1170+
1171+
// Add frame delay with 20ms minimum for multi-frame GIFs
1172+
int frame_delay_ms = (info.frame_count > 0 && frame_gcb.DelayTime < 2) ?
1173+
20 : frame_gcb.DelayTime * 10;
1174+
info.duration_ms += frame_delay_ms;
1175+
1176+
// If this is first GCB, handle background color
1177+
if (!found_gcb) {
1178+
found_gcb = true;
1179+
gcb = frame_gcb;
1180+
uint8_t bg_red, bg_green, bg_blue, bg_alpha;
1181+
extract_background_color(gif, &gcb, &bg_red, &bg_green,
1182+
&bg_blue, &bg_alpha);
1183+
info.bg_red = bg_red;
1184+
info.bg_green = bg_green;
1185+
info.bg_blue = bg_blue;
1186+
info.bg_alpha = bg_alpha;
1187+
}
11781188
}
11791189
// Look for NETSCAPE2.0 extension
11801190
else if (!found_loop_count &&

giflib.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type gifDecoder struct {
2424
bgGreen uint8
2525
bgBlue uint8
2626
bgAlpha uint8
27+
durationMs int
2728
}
2829

2930
// gifEncoder implements image encoding for GIF format
@@ -125,7 +126,8 @@ func (d *gifDecoder) ICC() []byte {
125126

126127
// Duration returns the total duration of the GIF animation.
127128
func (d *gifDecoder) Duration() time.Duration {
128-
return time.Duration(0)
129+
d.readAnimationInfo()
130+
return time.Duration(d.durationMs) * time.Millisecond
129131
}
130132

131133
// BackgroundColor returns the GIF background color as a 32-bit RGBA value.
@@ -146,6 +148,7 @@ func (d *gifDecoder) readAnimationInfo() {
146148
d.bgGreen = uint8(info.bg_green)
147149
d.bgBlue = uint8(info.bg_blue)
148150
d.bgAlpha = uint8(info.bg_alpha)
151+
d.durationMs = int(info.duration_ms)
149152
}
150153
}
151154

giflib.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ struct GifAnimationInfo {
1414
int bg_green;
1515
int bg_blue;
1616
int bg_alpha;
17+
int duration_ms;
1718
};
1819

1920
#define GIF_DISPOSE_NONE 0

giflib_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package lilliput
2+
3+
import (
4+
"io"
5+
"os"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestGIFOperations(t *testing.T) {
11+
t.Run("GIFDuration", testGIFDuration)
12+
}
13+
14+
func testGIFDuration(t *testing.T) {
15+
testCases := []struct {
16+
name string
17+
filename string
18+
wantLoopCount int
19+
wantFrames int
20+
wantDuration time.Duration
21+
description string
22+
}{
23+
{
24+
name: "Standard animated GIF",
25+
filename: "testdata/party-discord.gif",
26+
wantLoopCount: 0, // infinite loop
27+
wantFrames: 16,
28+
wantDuration: time.Millisecond * 480,
29+
description: "Basic animation with custom delays",
30+
},
31+
{
32+
name: "Static GIF image",
33+
filename: "testdata/ferry_sunset.gif",
34+
wantLoopCount: 1, // play once
35+
wantFrames: 1,
36+
wantDuration: 0,
37+
description: "Static image, no animation",
38+
},
39+
{
40+
name: "Single loop GIF",
41+
filename: "testdata/no-loop.gif",
42+
wantLoopCount: 1, // play once
43+
wantFrames: 44,
44+
wantDuration: time.Millisecond * 4400,
45+
description: "Animation that plays only once",
46+
},
47+
{
48+
name: "Duplicate loop count GIF",
49+
filename: "testdata/duplicate_number_of_loops.gif",
50+
wantLoopCount: 2, // play twice
51+
wantFrames: 2,
52+
wantDuration: 0, // unable to determine duration
53+
description: "Animation with duplicate NETSCAPE2.0 extension blocks",
54+
},
55+
{
56+
name: "Background dispose GIF",
57+
filename: "testdata/dispose_bgnd.gif",
58+
wantLoopCount: 0, // infinite loop
59+
wantFrames: 5,
60+
wantDuration: time.Second * 5,
61+
description: "Animation with background disposal method",
62+
},
63+
}
64+
65+
for _, tc := range testCases {
66+
t.Run(tc.name, func(t *testing.T) {
67+
testGIFImage, err := os.ReadFile(tc.filename)
68+
if err != nil {
69+
t.Fatalf("Failed to read gif image: %v", err)
70+
}
71+
72+
decoder, err := newGifDecoder(testGIFImage)
73+
if err != nil {
74+
t.Fatalf("Failed to create decoder: %v", err)
75+
}
76+
defer decoder.Close()
77+
78+
// Test loop count
79+
if got := decoder.LoopCount(); got != tc.wantLoopCount {
80+
t.Errorf("LoopCount() = %v, want %v", got, tc.wantLoopCount)
81+
}
82+
83+
// Test frame count
84+
if got := decoder.FrameCount(); got != tc.wantFrames {
85+
t.Errorf("FrameCount() = %v, want %v", got, tc.wantFrames)
86+
}
87+
88+
// Test total duration
89+
if got := decoder.Duration(); got != tc.wantDuration {
90+
t.Errorf("Duration() = %v, want %v (%s)", got, tc.wantDuration, tc.description)
91+
}
92+
93+
// Test per-frame durations
94+
header, err := decoder.Header()
95+
if err != nil {
96+
t.Fatalf("Failed to get header: %v", err)
97+
}
98+
99+
framebuffer := NewFramebuffer(header.width, header.height)
100+
defer framebuffer.Close()
101+
102+
var totalDuration time.Duration
103+
frameCount := 0
104+
for {
105+
err = decoder.DecodeTo(framebuffer)
106+
if err == io.EOF {
107+
break
108+
}
109+
if err != nil {
110+
t.Fatalf("DecodeTo failed: %v", err)
111+
}
112+
113+
totalDuration += framebuffer.Duration()
114+
frameCount++
115+
}
116+
117+
// Verify total duration matches sum of frame durations
118+
if totalDuration != tc.wantDuration {
119+
t.Errorf("Sum of frame durations (%v) doesn't match total duration (%v)",
120+
totalDuration, tc.wantDuration)
121+
}
122+
})
123+
}
124+
}

lilliput_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestNewDecoder(t *testing.T) {
6969
sourceFilePath: "testdata/big_buck_bunny_720_5s.webp",
7070
wantWidth: 480,
7171
wantHeight: 270,
72-
wantNegativeDuration: true,
72+
wantNegativeDuration: false,
7373
wantAnimated: true,
7474
},
7575
{

testdata/ferry_sunset.gif

102 KB
Loading

webp.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ struct webp_decoder_struct {
2323
WebPMuxAnimBlend prev_frame_blend;
2424
uint8_t* decode_buffer;
2525
size_t decode_buffer_size;
26+
int total_duration;
2627
};
2728

2829
struct webp_encoder_struct {
@@ -93,10 +94,12 @@ webp_decoder webp_decoder_create(const opencv_mat buf)
9394
return nullptr;
9495
}
9596

96-
// Calculate total frame count
97+
// Calculate total frame count and duration
9798
d->total_frame_count = 0;
99+
d->total_duration = 0;
98100
do {
99101
d->total_frame_count++;
102+
d->total_duration += frame.duration;
100103
WebPDataClear(&frame.bitstream);
101104
} while (WebPMuxGetFrame(mux, d->total_frame_count + 1, &frame) == WEBP_MUX_OK);
102105

@@ -109,6 +112,9 @@ webp_decoder webp_decoder_create(const opencv_mat buf)
109112
d->loop_count = anim_params.loop_count;
110113
}
111114
d->has_animation = true;
115+
} else {
116+
// For static images, ensure duration is 0
117+
d->total_duration = 0;
112118
}
113119

114120
// Pre-allocate decode buffer
@@ -228,6 +234,16 @@ int webp_decoder_get_num_frames(const webp_decoder d)
228234
return d ? d->total_frame_count : 0;
229235
}
230236

237+
/**
238+
* Gets the total duration of the WebP animation in milliseconds.
239+
* @param d The webp_decoder_struct pointer.
240+
* @return The total duration in milliseconds, 0 for static images.
241+
*/
242+
int webp_decoder_get_total_duration(const webp_decoder d)
243+
{
244+
return d ? d->total_duration : 0;
245+
}
246+
231247
/**
232248
* Gets the ICC profile data from the WebP image.
233249
* @param d The webp_decoder_struct pointer.

webp.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ type webpEncoder struct {
2121
encoder C.webp_encoder
2222
dstBuf []byte
2323
icc []byte
24-
isAnimated bool
2524
frameIndex int
2625
hasFlushed bool
2726
}
@@ -74,7 +73,7 @@ func (d *webpDecoder) Description() string {
7473
// Duration returns the total duration of the WebP animation.
7574
// Returns 0 for static images.
7675
func (d *webpDecoder) Duration() time.Duration {
77-
return time.Duration(0)
76+
return time.Duration(C.webp_decoder_get_total_duration(d.decoder)) * time.Millisecond
7877
}
7978

8079
// HasSubtitles returns whether the image contains subtitle data (always false for WebP).
@@ -94,7 +93,6 @@ func (d *webpDecoder) hasReachedEndOfFrames() bool {
9493

9594
// advanceFrameIndex advances the internal frame index for the next decoding call.
9695
func (d *webpDecoder) advanceFrameIndex() {
97-
// Advance the frame index within the C++ decoder
9896
C.webp_decoder_advance_frame(d.decoder)
9997
}
10098

webp.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ int webp_decoder_get_width(const webp_decoder d);
1515
int webp_decoder_get_height(const webp_decoder d);
1616
int webp_decoder_get_pixel_type(const webp_decoder d);
1717
int webp_decoder_get_num_frames(const webp_decoder d);
18+
int webp_decoder_get_total_duration(const webp_decoder d);
1819
int webp_decoder_get_prev_frame_delay(const webp_decoder d);
1920
int webp_decoder_get_prev_frame_dispose(const webp_decoder d);
2021
int webp_decoder_get_prev_frame_blend(const webp_decoder d);

0 commit comments

Comments
 (0)