1
0
mirror of https://github.com/golang/go synced 2024-11-19 21:14:43 -07:00

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: 249744f0e2
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>
This commit is contained in:
Peter Teichman 2018-02-12 19:23:31 +00:00 committed by Brad Fitzpatrick
parent caa7d854c8
commit ecba3714a3
3 changed files with 66 additions and 23 deletions

View File

@ -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
@ -566,9 +568,14 @@ func Decode(r io.Reader) (image.Image, error) {
// GIF represents the possibly multiple images stored in a GIF file. // GIF represents the possibly multiple images stored in a GIF file.
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

View File

@ -318,23 +318,62 @@ 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
if err != nil { loopCount int
t.Fatal("DecodeAll:", err) }{
{
"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,
},
} }
w := new(bytes.Buffer)
err = EncodeAll(w, img) for _, tc := range testCases {
if err != nil { t.Run(tc.name, func(t *testing.T) {
t.Fatal("EncodeAll:", err) img, err := DecodeAll(bytes.NewReader(tc.data))
} if err != nil {
img1, err := DecodeAll(w) t.Fatal("DecodeAll:", err)
if err != nil { }
t.Fatal("DecodeAll:", err) w := new(bytes.Buffer)
} err = EncodeAll(w, img)
if img.LoopCount != img1.LoopCount { if err != nil {
t.Errorf("loop count mismatch: %d vs %d", img.LoopCount, img1.LoopCount) 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)
}
})
} }
} }

View File

@ -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 {
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