Commit ecba3714 authored by Peter Teichman's avatar Peter Teichman Committed by Brad Fitzpatrick

image/gif: support non-looping animated gifs (LoopCount=-1)

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/93076Reviewed-by: default avatarAndrew Bonventre <andybons@golang.org>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Andrew Bonventre <andybons@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent caa7d854
...@@ -224,6 +224,8 @@ func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error { ...@@ -224,6 +224,8 @@ func (d *decoder) decode(r io.Reader, configOnly, keepAllFrames bool) error {
d.r = bufio.NewReader(r) d.r = bufio.NewReader(r)
} }
d.loopCount = -1
err := d.readHeaderAndScreenDescriptor() err := d.readHeaderAndScreenDescriptor()
if err != nil { if err != nil {
return err return err
...@@ -568,7 +570,12 @@ func Decode(r io.Reader) (image.Image, error) { ...@@ -568,7 +570,12 @@ func Decode(r io.Reader) (image.Image, error) {
type GIF struct { type GIF struct {
Image []*image.Paletted // The successive images. Image []*image.Paletted // The successive images.
Delay []int // The successive delay times, one per frame, in 100ths of a second. Delay []int // The successive delay times, one per frame, in 100ths of a second.
LoopCount int // The loop count. // 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 // Disposal is the successive disposal methods, one per frame. For
// backwards compatibility, a nil Disposal is valid to pass to EncodeAll, // backwards compatibility, a nil Disposal is valid to pass to EncodeAll,
// and implies that each frame's disposal method is 0 (no disposal // and implies that each frame's disposal method is 0 (no disposal
......
...@@ -318,9 +318,43 @@ func TestTransparentPixelOutsidePaletteRange(t *testing.T) { ...@@ -318,9 +318,43 @@ func TestTransparentPixelOutsidePaletteRange(t *testing.T) {
} }
func TestLoopCount(t *testing.T) { func TestLoopCount(t *testing.T) {
data := []byte("GIF89a000\x00000,0\x00\x00\x00\n\x00" + testCases := []struct {
"\n\x00\x80000000\x02\b\xf01u\xb9\xfdal\x05\x00;") name string
img, err := DecodeAll(bytes.NewReader(data)) 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,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
img, err := DecodeAll(bytes.NewReader(tc.data))
if err != nil { if err != nil {
t.Fatal("DecodeAll:", err) t.Fatal("DecodeAll:", err)
} }
...@@ -333,8 +367,13 @@ func TestLoopCount(t *testing.T) { ...@@ -333,8 +367,13 @@ func TestLoopCount(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("DecodeAll:", err) 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 { if img.LoopCount != img1.LoopCount {
t.Errorf("loop count mismatch: %d vs %d", img.LoopCount, img1.LoopCount) t.Errorf("loop count failed round-trip: %d vs %d", img.LoopCount, img1.LoopCount)
}
})
} }
} }
......
...@@ -178,7 +178,7 @@ func (e *encoder) writeHeader() { ...@@ -178,7 +178,7 @@ func (e *encoder) writeHeader() {
} }
// Add animation info if necessary. // 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[0] = 0x21 // Extension Introducer.
e.buf[1] = 0xff // Application Label. e.buf[1] = 0xff // Application Label.
e.buf[2] = 0x0b // Block Size. e.buf[2] = 0x0b // Block Size.
...@@ -377,9 +377,6 @@ func EncodeAll(w io.Writer, g *GIF) error { ...@@ -377,9 +377,6 @@ func EncodeAll(w io.Writer, g *GIF) error {
if len(g.Image) != len(g.Delay) { if len(g.Image) != len(g.Delay) {
return errors.New("gif: mismatched image and delay lengths") return errors.New("gif: mismatched image and delay lengths")
} }
if g.LoopCount < 0 {
g.LoopCount = 0
}
e := encoder{g: *g} e := encoder{g: *g}
// The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added // The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment