Skip to content

Commit

Permalink
image/gif: support non-looping animated gifs (LoopCount=-1)
Browse files Browse the repository at this point in the history
The Netscape looping application extension encodes how many
times the animation should restart, and if it's present
there is no way to signal that a GIF should play only once.

Use LoopCount=-1 to signal when a decoded GIF had no looping
extension, and update the encoder to omit that extension
block when LoopCount=-1.

Fixes golang#15768

GitHub-Last-Rev: 249744f
GitHub-Pull-Request: golang#23761
Change-Id: Ic915268505bf12bdad690b59148983a7d78d693b
Reviewed-on: https://go-review.googlesource.com/93076
Reviewed-by: Andrew Bonventre <[email protected]>
Reviewed-by: Brad Fitzpatrick <[email protected]>
Run-TryBot: Andrew Bonventre <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
  • Loading branch information
pteichman authored and bradfitz committed Feb 13, 2018
1 parent caa7d85 commit ecba371
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 23 deletions.
13 changes: 10 additions & 3 deletions src/image/gif/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
d.r = bufio.NewReader(r)
}

d.loopCount = -1

err := d.readHeaderAndScreenDescriptor()
if err != nil {
return err
Expand Down Expand Up @@ -566,9 +568,14 @@ func Decode(r io.Reader) (image.Image, error) {

// GIF represents the possibly multiple images stored in a GIF file.
type GIF struct {
Image []*image.Paletted // The successive images.
Delay []int // The successive delay times, one per frame, in 100ths of a second.
LoopCount int // The loop count.
Image []*image.Paletted // The successive images.
Delay []int // The successive delay times, one per frame, in 100ths of a second.
// LoopCount controls the number of times an animation will be
// restarted during display.
// A LoopCount of 0 means to loop forever.
// A LoopCount of -1 means to show each frame only once.
// Otherwise, the animation is looped LoopCount+1 times.
LoopCount int
// Disposal is the successive disposal methods, one per frame. For
// backwards compatibility, a nil Disposal is valid to pass to EncodeAll,
// and implies that each frame's disposal method is 0 (no disposal
Expand Down
71 changes: 55 additions & 16 deletions src/image/gif/reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,23 +318,62 @@ func TestTransparentPixelOutsidePaletteRange(t *testing.T) {
}

func TestLoopCount(t *testing.T) {
data := []byte("GIF89a000\x00000,0\x00\x00\x00\n\x00" +
"\n\x00\x80000000\x02\b\xf01u\xb9\xfdal\x05\x00;")
img, err := DecodeAll(bytes.NewReader(data))
if err != nil {
t.Fatal("DecodeAll:", err)
}
w := new(bytes.Buffer)
err = EncodeAll(w, img)
if err != nil {
t.Fatal("EncodeAll:", err)
}
img1, err := DecodeAll(w)
if err != nil {
t.Fatal("DecodeAll:", err)
testCases := []struct {
name string
data []byte
loopCount int
}{
{
"loopcount-missing",
[]byte("GIF89a000\x00000" +
",0\x00\x00\x00\n\x00\n\x00\x80000000" + // image 0 descriptor & color table
"\x02\b\xf01u\xb9\xfdal\x05\x00;"), // image 0 image data & trailer
-1,
},
{
"loopcount-0",
[]byte("GIF89a000\x00000" +
"!\xff\vNETSCAPE2.0\x03\x01\x00\x00\x00" + // loop count = 0
",0\x00\x00\x00\n\x00\n\x00\x80000000" + // image 0 descriptor & color table
"\x02\b\xf01u\xb9\xfdal\x05\x00" + // image 0 image data
",0\x00\x00\x00\n\x00\n\x00\x80000000" + // image 1 descriptor & color table
"\x02\b\xf01u\xb9\xfdal\x05\x00;"), // image 1 image data & trailer
0,
},
{
"loopcount-1",
[]byte("GIF89a000\x00000" +
"!\xff\vNETSCAPE2.0\x03\x01\x01\x00\x00" + // loop count = 1
",0\x00\x00\x00\n\x00\n\x00\x80000000" + // image 0 descriptor & color table
"\x02\b\xf01u\xb9\xfdal\x05\x00" + // image 0 image data
",0\x00\x00\x00\n\x00\n\x00\x80000000" + // image 1 descriptor & color table
"\x02\b\xf01u\xb9\xfdal\x05\x00;"), // image 1 image data & trailer
1,
},
}
if img.LoopCount != img1.LoopCount {
t.Errorf("loop count mismatch: %d vs %d", img.LoopCount, img1.LoopCount)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
img, err := DecodeAll(bytes.NewReader(tc.data))
if err != nil {
t.Fatal("DecodeAll:", err)
}
w := new(bytes.Buffer)
err = EncodeAll(w, img)
if err != nil {
t.Fatal("EncodeAll:", err)
}
img1, err := DecodeAll(w)
if err != nil {
t.Fatal("DecodeAll:", err)
}
if img.LoopCount != tc.loopCount {
t.Errorf("loop count mismatch: %d vs %d", img.LoopCount, tc.loopCount)
}
if img.LoopCount != img1.LoopCount {
t.Errorf("loop count failed round-trip: %d vs %d", img.LoopCount, img1.LoopCount)
}
})
}
}

Expand Down
5 changes: 1 addition & 4 deletions src/image/gif/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (e *encoder) writeHeader() {
}

// Add animation info if necessary.
if len(e.g.Image) > 1 {
if len(e.g.Image) > 1 && e.g.LoopCount >= 0 {
e.buf[0] = 0x21 // Extension Introducer.
e.buf[1] = 0xff // Application Label.
e.buf[2] = 0x0b // Block Size.
Expand Down Expand Up @@ -377,9 +377,6 @@ func EncodeAll(w io.Writer, g *GIF) error {
if len(g.Image) != len(g.Delay) {
return errors.New("gif: mismatched image and delay lengths")
}
if g.LoopCount < 0 {
g.LoopCount = 0
}

e := encoder{g: *g}
// The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added
Expand Down

0 comments on commit ecba371

Please sign in to comment.