| // 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)) |
| } |
| } |