]> Cypherpunks repositories - gostls13.git/commitdiff
image/gif: support non-looping animated gifs (LoopCount=-1)
authorPeter Teichman <pteichman@fastly.com>
Mon, 12 Feb 2018 19:23:31 +0000 (19:23 +0000)
committerBrad Fitzpatrick <bradfitz@golang.org>
Tue, 13 Feb 2018 20:25:49 +0000 (20:25 +0000)
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 #15768

GitHub-Last-Rev: 249744f0e28ef8907aa876070a102cb5493f5084
GitHub-Pull-Request: golang/go#23761
Change-Id: Ic915268505bf12bdad690b59148983a7d78d693b
Reviewed-on: https://go-review.googlesource.com/93076
Reviewed-by: Andrew Bonventre <andybons@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Andrew Bonventre <andybons@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>

src/image/gif/reader.go
src/image/gif/reader_test.go
src/image/gif/writer.go

index c1c9562067d0d7d71095f36bcf9e07fc910583bc..763146ecc4cf653a5e622917ee126d744f68cf3b 100644 (file)
@@ -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
@@ -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
index 220e8f52d48430823d70f969b0667ecb2de80ab8..29f47b6c081d13f5072d17c70be55a151bb5af07 100644 (file)
@@ -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)
+                       }
+               })
        }
 }
 
index f26af8be478be253d8aa9e13462cbdb53f13cb61..5e80feb33f08812767e0175cf5e892b79167baae 100644 (file)
@@ -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.
@@ -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