summaryrefslogtreecommitdiffstats
path: root/src/image/gif/reader_test.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/image/gif/reader_test.go441
1 files changed, 441 insertions, 0 deletions
diff --git a/src/image/gif/reader_test.go b/src/image/gif/reader_test.go
new file mode 100644
index 0000000..a7f943a
--- /dev/null
+++ b/src/image/gif/reader_test.go
@@ -0,0 +1,441 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gif
+
+import (
+ "bytes"
+ "compress/lzw"
+ "image"
+ "image/color"
+ "image/color/palette"
+ "io"
+ "os"
+ "reflect"
+ "runtime"
+ "runtime/debug"
+ "strings"
+ "testing"
+)
+
+// header, palette and trailer are parts of a valid 2x1 GIF image.
+const (
+ headerStr = "GIF89a" +
+ "\x02\x00\x01\x00" + // width=2, height=1
+ "\x80\x00\x00" // headerFields=(a color table of 2 pixels), backgroundIndex, aspect
+ paletteStr = "\x10\x20\x30\x40\x50\x60" // the color table, also known as a palette
+ trailerStr = "\x3b"
+)
+
+// lzw.NewReader wants a io.ByteReader, this ensures we're compatible.
+var _ io.ByteReader = (*blockReader)(nil)
+
+// lzwEncode returns an LZW encoding (with 2-bit literals) of in.
+func lzwEncode(in []byte) []byte {
+ b := &bytes.Buffer{}
+ w := lzw.NewWriter(b, lzw.LSB, 2)
+ if _, err := w.Write(in); err != nil {
+ panic(err)
+ }
+ if err := w.Close(); err != nil {
+ panic(err)
+ }
+ return b.Bytes()
+}
+
+func TestDecode(t *testing.T) {
+ // extra contains superfluous bytes to inject into the GIF, either at the end
+ // of an existing data sub-block (past the LZW End of Information code) or in
+ // a separate data sub-block. The 0x02 values are arbitrary.
+ const extra = "\x02\x02\x02\x02"
+
+ testCases := []struct {
+ nPix int // The number of pixels in the image data.
+ // If non-zero, write this many extra bytes inside the data sub-block
+ // containing the LZW end code.
+ extraExisting int
+ // If non-zero, write an extra block of this many bytes.
+ extraSeparate int
+ wantErr error
+ }{
+ {0, 0, 0, errNotEnough},
+ {1, 0, 0, errNotEnough},
+ {2, 0, 0, nil},
+ // An extra data sub-block after the compressed section with 1 byte which we
+ // silently skip.
+ {2, 0, 1, nil},
+ // An extra data sub-block after the compressed section with 2 bytes. In
+ // this case we complain that there is too much data.
+ {2, 0, 2, errTooMuch},
+ // Too much pixel data.
+ {3, 0, 0, errTooMuch},
+ // An extra byte after LZW data, but inside the same data sub-block.
+ {2, 1, 0, nil},
+ // Two extra bytes after LZW data, but inside the same data sub-block.
+ {2, 2, 0, nil},
+ // Extra data exists in the final sub-block with LZW data, AND there is
+ // a bogus sub-block following.
+ {2, 1, 1, errTooMuch},
+ }
+ for _, tc := range testCases {
+ b := &bytes.Buffer{}
+ b.WriteString(headerStr)
+ b.WriteString(paletteStr)
+ // Write an image with bounds 2x1 but tc.nPix pixels. If tc.nPix != 2
+ // then this should result in an invalid GIF image. First, write a
+ // magic 0x2c (image descriptor) byte, bounds=(0,0)-(2,1), a flags
+ // byte, and 2-bit LZW literals.
+ b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02")
+ if tc.nPix > 0 {
+ enc := lzwEncode(make([]byte, tc.nPix))
+ if len(enc)+tc.extraExisting > 0xff {
+ t.Errorf("nPix=%d, extraExisting=%d, extraSeparate=%d: compressed length %d is too large",
+ tc.nPix, tc.extraExisting, tc.extraSeparate, len(enc))
+ continue
+ }
+
+ // Write the size of the data sub-block containing the LZW data.
+ b.WriteByte(byte(len(enc) + tc.extraExisting))
+
+ // Write the LZW data.
+ b.Write(enc)
+
+ // Write extra bytes inside the same data sub-block where LZW data
+ // ended. Each arbitrarily 0x02.
+ b.WriteString(extra[:tc.extraExisting])
+ }
+
+ if tc.extraSeparate > 0 {
+ // Data sub-block size. This indicates how many extra bytes follow.
+ b.WriteByte(byte(tc.extraSeparate))
+ b.WriteString(extra[:tc.extraSeparate])
+ }
+ b.WriteByte(0x00) // An empty block signifies the end of the image data.
+ b.WriteString(trailerStr)
+
+ got, err := Decode(b)
+ if err != tc.wantErr {
+ t.Errorf("nPix=%d, extraExisting=%d, extraSeparate=%d\ngot %v\nwant %v",
+ tc.nPix, tc.extraExisting, tc.extraSeparate, err, tc.wantErr)
+ }
+
+ if tc.wantErr != nil {
+ continue
+ }
+ want := &image.Paletted{
+ Pix: []uint8{0, 0},
+ Stride: 2,
+ Rect: image.Rect(0, 0, 2, 1),
+ Palette: color.Palette{
+ color.RGBA{0x10, 0x20, 0x30, 0xff},
+ color.RGBA{0x40, 0x50, 0x60, 0xff},
+ },
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("nPix=%d, extraExisting=%d, extraSeparate=%d\ngot %v\nwant %v",
+ tc.nPix, tc.extraExisting, tc.extraSeparate, got, want)
+ }
+ }
+}
+
+func TestTransparentIndex(t *testing.T) {
+ b := &bytes.Buffer{}
+ b.WriteString(headerStr)
+ b.WriteString(paletteStr)
+ for transparentIndex := 0; transparentIndex < 3; transparentIndex++ {
+ if transparentIndex < 2 {
+ // Write the graphic control for the transparent index.
+ b.WriteString("\x21\xf9\x04\x01\x00\x00")
+ b.WriteByte(byte(transparentIndex))
+ b.WriteByte(0)
+ }
+ // Write an image with bounds 2x1, as per TestDecode.
+ b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02")
+ enc := lzwEncode([]byte{0x00, 0x00})
+ if len(enc) > 0xff {
+ t.Fatalf("compressed length %d is too large", len(enc))
+ }
+ b.WriteByte(byte(len(enc)))
+ b.Write(enc)
+ b.WriteByte(0x00)
+ }
+ b.WriteString(trailerStr)
+
+ g, err := DecodeAll(b)
+ if err != nil {
+ t.Fatalf("DecodeAll: %v", err)
+ }
+ c0 := color.RGBA{paletteStr[0], paletteStr[1], paletteStr[2], 0xff}
+ c1 := color.RGBA{paletteStr[3], paletteStr[4], paletteStr[5], 0xff}
+ cz := color.RGBA{}
+ wants := []color.Palette{
+ {cz, c1},
+ {c0, cz},
+ {c0, c1},
+ }
+ if len(g.Image) != len(wants) {
+ t.Fatalf("got %d images, want %d", len(g.Image), len(wants))
+ }
+ for i, want := range wants {
+ got := g.Image[i].Palette
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("palette #%d:\ngot %v\nwant %v", i, got, want)
+ }
+ }
+}
+
+// testGIF is a simple GIF that we can modify to test different scenarios.
+var testGIF = []byte{
+ 'G', 'I', 'F', '8', '9', 'a',
+ 1, 0, 1, 0, // w=1, h=1 (6)
+ 128, 0, 0, // headerFields, bg, aspect (10)
+ 0, 0, 0, 1, 1, 1, // color table and graphics control (13)
+ 0x21, 0xf9, 0x04, 0x00, 0x00, 0x00, 0xff, 0x00, // (19)
+ // frame 1 (0,0 - 1,1)
+ 0x2c,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0x00, // (32)
+ 0x00,
+ 0x02, 0x02, 0x4c, 0x01, 0x00, // lzw pixels
+ // trailer
+ 0x3b,
+}
+
+func try(t *testing.T, b []byte, want string) {
+ _, err := DecodeAll(bytes.NewReader(b))
+ var got string
+ if err != nil {
+ got = err.Error()
+ }
+ if got != want {
+ t.Fatalf("got %v, want %v", got, want)
+ }
+}
+
+func TestBounds(t *testing.T) {
+ // Make a local copy of testGIF.
+ gif := make([]byte, len(testGIF))
+ copy(gif, testGIF)
+ // Make the bounds too big, just by one.
+ gif[32] = 2
+ want := "gif: frame bounds larger than image bounds"
+ try(t, gif, want)
+
+ // Make the bounds too small; does not trigger bounds
+ // check, but now there's too much data.
+ gif[32] = 0
+ want = "gif: too much image data"
+ try(t, gif, want)
+ gif[32] = 1
+
+ // Make the bounds really big, expect an error.
+ want = "gif: frame bounds larger than image bounds"
+ for i := 0; i < 4; i++ {
+ gif[32+i] = 0xff
+ }
+ try(t, gif, want)
+}
+
+func TestNoPalette(t *testing.T) {
+ b := &bytes.Buffer{}
+
+ // Manufacture a GIF with no palette, so any pixel at all
+ // will be invalid.
+ b.WriteString(headerStr[:len(headerStr)-3])
+ b.WriteString("\x00\x00\x00") // No global palette.
+
+ // Image descriptor: 2x1, no local palette, and 2-bit LZW literals.
+ b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02")
+
+ // Encode the pixels: neither is in range, because there is no palette.
+ enc := lzwEncode([]byte{0x00, 0x03})
+ b.WriteByte(byte(len(enc)))
+ b.Write(enc)
+ b.WriteByte(0x00) // An empty block signifies the end of the image data.
+
+ b.WriteString(trailerStr)
+
+ try(t, b.Bytes(), "gif: no color table")
+}
+
+func TestPixelOutsidePaletteRange(t *testing.T) {
+ for _, pval := range []byte{0, 1, 2, 3} {
+ b := &bytes.Buffer{}
+
+ // Manufacture a GIF with a 2 color palette.
+ b.WriteString(headerStr)
+ b.WriteString(paletteStr)
+
+ // Image descriptor: 2x1, no local palette, and 2-bit LZW literals.
+ b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02")
+
+ // Encode the pixels; some pvals trigger the expected error.
+ enc := lzwEncode([]byte{pval, pval})
+ b.WriteByte(byte(len(enc)))
+ b.Write(enc)
+ b.WriteByte(0x00) // An empty block signifies the end of the image data.
+
+ b.WriteString(trailerStr)
+
+ // No error expected, unless the pixels are beyond the 2 color palette.
+ want := ""
+ if pval >= 2 {
+ want = "gif: invalid pixel value"
+ }
+ try(t, b.Bytes(), want)
+ }
+}
+
+func TestTransparentPixelOutsidePaletteRange(t *testing.T) {
+ b := &bytes.Buffer{}
+
+ // Manufacture a GIF with a 2 color palette.
+ b.WriteString(headerStr)
+ b.WriteString(paletteStr)
+
+ // Graphic Control Extension: transparency, transparent color index = 3.
+ //
+ // This index, 3, is out of range of the global palette and there is no
+ // local palette in the subsequent image descriptor. This is an error
+ // according to the spec, but Firefox and Google Chrome seem OK with this.
+ //
+ // See golang.org/issue/15059.
+ b.WriteString("\x21\xf9\x04\x01\x00\x00\x03\x00")
+
+ // Image descriptor: 2x1, no local palette, and 2-bit LZW literals.
+ b.WriteString("\x2c\x00\x00\x00\x00\x02\x00\x01\x00\x00\x02")
+
+ // Encode the pixels.
+ enc := lzwEncode([]byte{0x03, 0x03})
+ b.WriteByte(byte(len(enc)))
+ b.Write(enc)
+ b.WriteByte(0x00) // An empty block signifies the end of the image data.
+
+ b.WriteString(trailerStr)
+
+ try(t, b.Bytes(), "")
+}
+
+func TestLoopCount(t *testing.T) {
+ 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,
+ },
+ }
+
+ 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)
+ }
+ })
+ }
+}
+
+func TestUnexpectedEOF(t *testing.T) {
+ for i := len(testGIF) - 1; i >= 0; i-- {
+ _, err := DecodeAll(bytes.NewReader(testGIF[:i]))
+ if err == errNotEnough {
+ continue
+ }
+ text := ""
+ if err != nil {
+ text = err.Error()
+ }
+ if !strings.HasPrefix(text, "gif:") || !strings.HasSuffix(text, ": unexpected EOF") {
+ t.Errorf("Decode(testGIF[:%d]) = %v, want gif: ...: unexpected EOF", i, err)
+ }
+ }
+}
+
+// See golang.org/issue/22237
+func TestDecodeMemoryConsumption(t *testing.T) {
+ const frames = 3000
+ img := image.NewPaletted(image.Rectangle{Max: image.Point{1, 1}}, palette.WebSafe)
+ hugeGIF := &GIF{
+ Image: make([]*image.Paletted, frames),
+ Delay: make([]int, frames),
+ Disposal: make([]byte, frames),
+ }
+ for i := 0; i < frames; i++ {
+ hugeGIF.Image[i] = img
+ hugeGIF.Delay[i] = 60
+ }
+ buf := new(bytes.Buffer)
+ if err := EncodeAll(buf, hugeGIF); err != nil {
+ t.Fatal("EncodeAll:", err)
+ }
+ s0, s1 := new(runtime.MemStats), new(runtime.MemStats)
+ runtime.GC()
+ defer debug.SetGCPercent(debug.SetGCPercent(5))
+ runtime.ReadMemStats(s0)
+ if _, err := Decode(buf); err != nil {
+ t.Fatal("Decode:", err)
+ }
+ runtime.ReadMemStats(s1)
+ if heapDiff := int64(s1.HeapAlloc - s0.HeapAlloc); heapDiff > 30<<20 {
+ t.Fatalf("Decode of %d frames increased heap by %dMB", frames, heapDiff>>20)
+ }
+}
+
+func BenchmarkDecode(b *testing.B) {
+ data, err := os.ReadFile("../testdata/video-001.gif")
+ if err != nil {
+ b.Fatal(err)
+ }
+ cfg, err := DecodeConfig(bytes.NewReader(data))
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.SetBytes(int64(cfg.Width * cfg.Height))
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ Decode(bytes.NewReader(data))
+ }
+}