Home | History | Annotate | Download | only in gif
      1 // Copyright 2013 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package gif
      6 
      7 import (
      8 	"bytes"
      9 	"image"
     10 	"image/color"
     11 	"image/color/palette"
     12 	_ "image/png"
     13 	"io/ioutil"
     14 	"math/rand"
     15 	"os"
     16 	"reflect"
     17 	"testing"
     18 )
     19 
     20 func readImg(filename string) (image.Image, error) {
     21 	f, err := os.Open(filename)
     22 	if err != nil {
     23 		return nil, err
     24 	}
     25 	defer f.Close()
     26 	m, _, err := image.Decode(f)
     27 	return m, err
     28 }
     29 
     30 func readGIF(filename string) (*GIF, error) {
     31 	f, err := os.Open(filename)
     32 	if err != nil {
     33 		return nil, err
     34 	}
     35 	defer f.Close()
     36 	return DecodeAll(f)
     37 }
     38 
     39 func delta(u0, u1 uint32) int64 {
     40 	d := int64(u0) - int64(u1)
     41 	if d < 0 {
     42 		return -d
     43 	}
     44 	return d
     45 }
     46 
     47 // averageDelta returns the average delta in RGB space. The two images must
     48 // have the same bounds.
     49 func averageDelta(m0, m1 image.Image) int64 {
     50 	b := m0.Bounds()
     51 	var sum, n int64
     52 	for y := b.Min.Y; y < b.Max.Y; y++ {
     53 		for x := b.Min.X; x < b.Max.X; x++ {
     54 			c0 := m0.At(x, y)
     55 			c1 := m1.At(x, y)
     56 			r0, g0, b0, _ := c0.RGBA()
     57 			r1, g1, b1, _ := c1.RGBA()
     58 			sum += delta(r0, r1)
     59 			sum += delta(g0, g1)
     60 			sum += delta(b0, b1)
     61 			n += 3
     62 		}
     63 	}
     64 	return sum / n
     65 }
     66 
     67 // lzw.NewWriter wants an interface which is basically the same thing as gif's
     68 // writer interface.  This ensures we're compatible.
     69 var _ writer = blockWriter{}
     70 
     71 var testCase = []struct {
     72 	filename  string
     73 	tolerance int64
     74 }{
     75 	{"../testdata/video-001.png", 1 << 12},
     76 	{"../testdata/video-001.gif", 0},
     77 	{"../testdata/video-001.interlaced.gif", 0},
     78 }
     79 
     80 func TestWriter(t *testing.T) {
     81 	for _, tc := range testCase {
     82 		m0, err := readImg(tc.filename)
     83 		if err != nil {
     84 			t.Error(tc.filename, err)
     85 			continue
     86 		}
     87 		var buf bytes.Buffer
     88 		err = Encode(&buf, m0, nil)
     89 		if err != nil {
     90 			t.Error(tc.filename, err)
     91 			continue
     92 		}
     93 		m1, err := Decode(&buf)
     94 		if err != nil {
     95 			t.Error(tc.filename, err)
     96 			continue
     97 		}
     98 		if m0.Bounds() != m1.Bounds() {
     99 			t.Errorf("%s, bounds differ: %v and %v", tc.filename, m0.Bounds(), m1.Bounds())
    100 			continue
    101 		}
    102 		// Compare the average delta to the tolerance level.
    103 		avgDelta := averageDelta(m0, m1)
    104 		if avgDelta > tc.tolerance {
    105 			t.Errorf("%s: average delta is too high. expected: %d, got %d", tc.filename, tc.tolerance, avgDelta)
    106 			continue
    107 		}
    108 	}
    109 }
    110 
    111 func TestSubImage(t *testing.T) {
    112 	m0, err := readImg("../testdata/video-001.gif")
    113 	if err != nil {
    114 		t.Fatalf("readImg: %v", err)
    115 	}
    116 	m0 = m0.(*image.Paletted).SubImage(image.Rect(0, 0, 50, 30))
    117 	var buf bytes.Buffer
    118 	err = Encode(&buf, m0, nil)
    119 	if err != nil {
    120 		t.Fatalf("Encode: %v", err)
    121 	}
    122 	m1, err := Decode(&buf)
    123 	if err != nil {
    124 		t.Fatalf("Decode: %v", err)
    125 	}
    126 	if m0.Bounds() != m1.Bounds() {
    127 		t.Fatalf("bounds differ: %v and %v", m0.Bounds(), m1.Bounds())
    128 	}
    129 	if averageDelta(m0, m1) != 0 {
    130 		t.Fatalf("images differ")
    131 	}
    132 }
    133 
    134 // palettesEqual reports whether two color.Palette values are equal, ignoring
    135 // any trailing opaque-black palette entries.
    136 func palettesEqual(p, q color.Palette) bool {
    137 	n := len(p)
    138 	if n > len(q) {
    139 		n = len(q)
    140 	}
    141 	for i := 0; i < n; i++ {
    142 		if p[i] != q[i] {
    143 			return false
    144 		}
    145 	}
    146 	for i := n; i < len(p); i++ {
    147 		r, g, b, a := p[i].RGBA()
    148 		if r != 0 || g != 0 || b != 0 || a != 0xffff {
    149 			return false
    150 		}
    151 	}
    152 	for i := n; i < len(q); i++ {
    153 		r, g, b, a := q[i].RGBA()
    154 		if r != 0 || g != 0 || b != 0 || a != 0xffff {
    155 			return false
    156 		}
    157 	}
    158 	return true
    159 }
    160 
    161 var frames = []string{
    162 	"../testdata/video-001.gif",
    163 	"../testdata/video-005.gray.gif",
    164 }
    165 
    166 func testEncodeAll(t *testing.T, go1Dot5Fields bool, useGlobalColorModel bool) {
    167 	const width, height = 150, 103
    168 
    169 	g0 := &GIF{
    170 		Image:     make([]*image.Paletted, len(frames)),
    171 		Delay:     make([]int, len(frames)),
    172 		LoopCount: 5,
    173 	}
    174 	for i, f := range frames {
    175 		g, err := readGIF(f)
    176 		if err != nil {
    177 			t.Fatal(f, err)
    178 		}
    179 		m := g.Image[0]
    180 		if m.Bounds().Dx() != width || m.Bounds().Dy() != height {
    181 			t.Fatalf("frame %d had unexpected bounds: got %v, want width/height = %d/%d",
    182 				i, m.Bounds(), width, height)
    183 		}
    184 		g0.Image[i] = m
    185 	}
    186 	// The GIF.Disposal, GIF.Config and GIF.BackgroundIndex fields were added
    187 	// in Go 1.5. Valid Go 1.4 or earlier code should still produce valid GIFs.
    188 	//
    189 	// On the following line, color.Model is an interface type, and
    190 	// color.Palette is a concrete (slice) type.
    191 	globalColorModel, backgroundIndex := color.Model(color.Palette(nil)), uint8(0)
    192 	if useGlobalColorModel {
    193 		globalColorModel, backgroundIndex = color.Palette(palette.WebSafe), uint8(1)
    194 	}
    195 	if go1Dot5Fields {
    196 		g0.Disposal = make([]byte, len(g0.Image))
    197 		for i := range g0.Disposal {
    198 			g0.Disposal[i] = DisposalNone
    199 		}
    200 		g0.Config = image.Config{
    201 			ColorModel: globalColorModel,
    202 			Width:      width,
    203 			Height:     height,
    204 		}
    205 		g0.BackgroundIndex = backgroundIndex
    206 	}
    207 
    208 	var buf bytes.Buffer
    209 	if err := EncodeAll(&buf, g0); err != nil {
    210 		t.Fatal("EncodeAll:", err)
    211 	}
    212 	encoded := buf.Bytes()
    213 	config, err := DecodeConfig(bytes.NewReader(encoded))
    214 	if err != nil {
    215 		t.Fatal("DecodeConfig:", err)
    216 	}
    217 	g1, err := DecodeAll(bytes.NewReader(encoded))
    218 	if err != nil {
    219 		t.Fatal("DecodeAll:", err)
    220 	}
    221 
    222 	if !reflect.DeepEqual(config, g1.Config) {
    223 		t.Errorf("DecodeConfig inconsistent with DecodeAll")
    224 	}
    225 	if !palettesEqual(g1.Config.ColorModel.(color.Palette), globalColorModel.(color.Palette)) {
    226 		t.Errorf("unexpected global color model")
    227 	}
    228 	if w, h := g1.Config.Width, g1.Config.Height; w != width || h != height {
    229 		t.Errorf("got config width * height = %d * %d, want %d * %d", w, h, width, height)
    230 	}
    231 
    232 	if g0.LoopCount != g1.LoopCount {
    233 		t.Errorf("loop counts differ: %d and %d", g0.LoopCount, g1.LoopCount)
    234 	}
    235 	if backgroundIndex != g1.BackgroundIndex {
    236 		t.Errorf("background indexes differ: %d and %d", backgroundIndex, g1.BackgroundIndex)
    237 	}
    238 	if len(g0.Image) != len(g1.Image) {
    239 		t.Fatalf("image lengths differ: %d and %d", len(g0.Image), len(g1.Image))
    240 	}
    241 	if len(g1.Image) != len(g1.Delay) {
    242 		t.Fatalf("image and delay lengths differ: %d and %d", len(g1.Image), len(g1.Delay))
    243 	}
    244 	if len(g1.Image) != len(g1.Disposal) {
    245 		t.Fatalf("image and disposal lengths differ: %d and %d", len(g1.Image), len(g1.Disposal))
    246 	}
    247 
    248 	for i := range g0.Image {
    249 		m0, m1 := g0.Image[i], g1.Image[i]
    250 		if m0.Bounds() != m1.Bounds() {
    251 			t.Errorf("frame %d: bounds differ: %v and %v", i, m0.Bounds(), m1.Bounds())
    252 		}
    253 		d0, d1 := g0.Delay[i], g1.Delay[i]
    254 		if d0 != d1 {
    255 			t.Errorf("frame %d: delay values differ: %d and %d", i, d0, d1)
    256 		}
    257 		p0, p1 := uint8(0), g1.Disposal[i]
    258 		if go1Dot5Fields {
    259 			p0 = DisposalNone
    260 		}
    261 		if p0 != p1 {
    262 			t.Errorf("frame %d: disposal values differ: %d and %d", i, p0, p1)
    263 		}
    264 	}
    265 }
    266 
    267 func TestEncodeAllGo1Dot4(t *testing.T)                 { testEncodeAll(t, false, false) }
    268 func TestEncodeAllGo1Dot5(t *testing.T)                 { testEncodeAll(t, true, false) }
    269 func TestEncodeAllGo1Dot5GlobalColorModel(t *testing.T) { testEncodeAll(t, true, true) }
    270 
    271 func TestEncodeMismatchDelay(t *testing.T) {
    272 	images := make([]*image.Paletted, 2)
    273 	for i := range images {
    274 		images[i] = image.NewPaletted(image.Rect(0, 0, 5, 5), palette.Plan9)
    275 	}
    276 
    277 	g0 := &GIF{
    278 		Image: images,
    279 		Delay: make([]int, 1),
    280 	}
    281 	if err := EncodeAll(ioutil.Discard, g0); err == nil {
    282 		t.Error("expected error from mismatched delay and image slice lengths")
    283 	}
    284 
    285 	g1 := &GIF{
    286 		Image:    images,
    287 		Delay:    make([]int, len(images)),
    288 		Disposal: make([]byte, 1),
    289 	}
    290 	for i := range g1.Disposal {
    291 		g1.Disposal[i] = DisposalNone
    292 	}
    293 	if err := EncodeAll(ioutil.Discard, g1); err == nil {
    294 		t.Error("expected error from mismatched disposal and image slice lengths")
    295 	}
    296 }
    297 
    298 func TestEncodeZeroGIF(t *testing.T) {
    299 	if err := EncodeAll(ioutil.Discard, &GIF{}); err == nil {
    300 		t.Error("expected error from providing empty gif")
    301 	}
    302 }
    303 
    304 func TestEncodeAllFramesOutOfBounds(t *testing.T) {
    305 	images := []*image.Paletted{
    306 		image.NewPaletted(image.Rect(0, 0, 5, 5), palette.Plan9),
    307 		image.NewPaletted(image.Rect(2, 2, 8, 8), palette.Plan9),
    308 		image.NewPaletted(image.Rect(3, 3, 4, 4), palette.Plan9),
    309 	}
    310 	for _, upperBound := range []int{6, 10} {
    311 		g := &GIF{
    312 			Image:    images,
    313 			Delay:    make([]int, len(images)),
    314 			Disposal: make([]byte, len(images)),
    315 			Config: image.Config{
    316 				Width:  upperBound,
    317 				Height: upperBound,
    318 			},
    319 		}
    320 		err := EncodeAll(ioutil.Discard, g)
    321 		if upperBound >= 8 {
    322 			if err != nil {
    323 				t.Errorf("upperBound=%d: %v", upperBound, err)
    324 			}
    325 		} else {
    326 			if err == nil {
    327 				t.Errorf("upperBound=%d: got nil error, want non-nil", upperBound)
    328 			}
    329 		}
    330 	}
    331 }
    332 
    333 func TestEncodeNonZeroMinPoint(t *testing.T) {
    334 	points := []image.Point{
    335 		{-8, -9},
    336 		{-4, -4},
    337 		{-3, +3},
    338 		{+0, +0},
    339 		{+2, +2},
    340 	}
    341 	for _, p := range points {
    342 		src := image.NewPaletted(image.Rectangle{Min: p, Max: p.Add(image.Point{6, 6})}, palette.Plan9)
    343 		var buf bytes.Buffer
    344 		if err := Encode(&buf, src, nil); err != nil {
    345 			t.Errorf("p=%v: Encode: %v", p, err)
    346 			continue
    347 		}
    348 		m, err := Decode(&buf)
    349 		if err != nil {
    350 			t.Errorf("p=%v: Decode: %v", p, err)
    351 			continue
    352 		}
    353 		if got, want := m.Bounds(), image.Rect(0, 0, 6, 6); got != want {
    354 			t.Errorf("p=%v: got %v, want %v", p, got, want)
    355 		}
    356 	}
    357 }
    358 
    359 func TestEncodeImplicitConfigSize(t *testing.T) {
    360 	// For backwards compatibility for Go 1.4 and earlier code, the Config
    361 	// field is optional, and if zero, the width and height is implied by the
    362 	// first (and in this case only) frame's width and height.
    363 	//
    364 	// A Config only specifies a width and height (two integers) while an
    365 	// image.Image's Bounds method returns an image.Rectangle (four integers).
    366 	// For a gif.GIF, the overall bounds' top-left point is always implicitly
    367 	// (0, 0), and any frame whose bounds have a negative X or Y will be
    368 	// outside those overall bounds, so encoding should fail.
    369 	for _, lowerBound := range []int{-1, 0, 1} {
    370 		images := []*image.Paletted{
    371 			image.NewPaletted(image.Rect(lowerBound, lowerBound, 4, 4), palette.Plan9),
    372 		}
    373 		g := &GIF{
    374 			Image: images,
    375 			Delay: make([]int, len(images)),
    376 		}
    377 		err := EncodeAll(ioutil.Discard, g)
    378 		if lowerBound >= 0 {
    379 			if err != nil {
    380 				t.Errorf("lowerBound=%d: %v", lowerBound, err)
    381 			}
    382 		} else {
    383 			if err == nil {
    384 				t.Errorf("lowerBound=%d: got nil error, want non-nil", lowerBound)
    385 			}
    386 		}
    387 	}
    388 }
    389 
    390 func TestEncodePalettes(t *testing.T) {
    391 	const w, h = 5, 5
    392 	pals := []color.Palette{{
    393 		color.RGBA{0x00, 0x00, 0x00, 0xff},
    394 		color.RGBA{0x01, 0x00, 0x00, 0xff},
    395 		color.RGBA{0x02, 0x00, 0x00, 0xff},
    396 	}, {
    397 		color.RGBA{0x00, 0x00, 0x00, 0xff},
    398 		color.RGBA{0x00, 0x01, 0x00, 0xff},
    399 	}, {
    400 		color.RGBA{0x00, 0x00, 0x03, 0xff},
    401 		color.RGBA{0x00, 0x00, 0x02, 0xff},
    402 		color.RGBA{0x00, 0x00, 0x01, 0xff},
    403 		color.RGBA{0x00, 0x00, 0x00, 0xff},
    404 	}, {
    405 		color.RGBA{0x10, 0x07, 0xf0, 0xff},
    406 		color.RGBA{0x20, 0x07, 0xf0, 0xff},
    407 		color.RGBA{0x30, 0x07, 0xf0, 0xff},
    408 		color.RGBA{0x40, 0x07, 0xf0, 0xff},
    409 		color.RGBA{0x50, 0x07, 0xf0, 0xff},
    410 	}}
    411 	g0 := &GIF{
    412 		Image: []*image.Paletted{
    413 			image.NewPaletted(image.Rect(0, 0, w, h), pals[0]),
    414 			image.NewPaletted(image.Rect(0, 0, w, h), pals[1]),
    415 			image.NewPaletted(image.Rect(0, 0, w, h), pals[2]),
    416 			image.NewPaletted(image.Rect(0, 0, w, h), pals[3]),
    417 		},
    418 		Delay:    make([]int, len(pals)),
    419 		Disposal: make([]byte, len(pals)),
    420 		Config: image.Config{
    421 			ColorModel: pals[2],
    422 			Width:      w,
    423 			Height:     h,
    424 		},
    425 	}
    426 
    427 	var buf bytes.Buffer
    428 	if err := EncodeAll(&buf, g0); err != nil {
    429 		t.Fatalf("EncodeAll: %v", err)
    430 	}
    431 	g1, err := DecodeAll(&buf)
    432 	if err != nil {
    433 		t.Fatalf("DecodeAll: %v", err)
    434 	}
    435 	if len(g0.Image) != len(g1.Image) {
    436 		t.Fatalf("image lengths differ: %d and %d", len(g0.Image), len(g1.Image))
    437 	}
    438 	for i, m := range g1.Image {
    439 		if got, want := m.Palette, pals[i]; !palettesEqual(got, want) {
    440 			t.Errorf("frame %d:\ngot  %v\nwant %v", i, got, want)
    441 		}
    442 	}
    443 }
    444 
    445 func TestEncodeBadPalettes(t *testing.T) {
    446 	const w, h = 5, 5
    447 	for _, n := range []int{256, 257} {
    448 		for _, nilColors := range []bool{false, true} {
    449 			pal := make(color.Palette, n)
    450 			if !nilColors {
    451 				for i := range pal {
    452 					pal[i] = color.Black
    453 				}
    454 			}
    455 
    456 			err := EncodeAll(ioutil.Discard, &GIF{
    457 				Image: []*image.Paletted{
    458 					image.NewPaletted(image.Rect(0, 0, w, h), pal),
    459 				},
    460 				Delay:    make([]int, 1),
    461 				Disposal: make([]byte, 1),
    462 				Config: image.Config{
    463 					ColorModel: pal,
    464 					Width:      w,
    465 					Height:     h,
    466 				},
    467 			})
    468 
    469 			got := err != nil
    470 			want := n > 256 || nilColors
    471 			if got != want {
    472 				t.Errorf("n=%d, nilColors=%t: err != nil: got %t, want %t", n, nilColors, got, want)
    473 			}
    474 		}
    475 	}
    476 }
    477 
    478 func TestColorTablesMatch(t *testing.T) {
    479 	const trIdx = 100
    480 	global := color.Palette(palette.Plan9)
    481 	if rgb := global[trIdx].(color.RGBA); rgb.R == 0 && rgb.G == 0 && rgb.B == 0 {
    482 		t.Fatalf("trIdx (%d) is already black", trIdx)
    483 	}
    484 
    485 	// Make a copy of the palette, substituting trIdx's slot with transparent,
    486 	// just like decoder.decode.
    487 	local := append(color.Palette(nil), global...)
    488 	local[trIdx] = color.RGBA{}
    489 
    490 	const testLen = 3 * 256
    491 	const padded = 7
    492 	e := new(encoder)
    493 	if l, err := encodeColorTable(e.globalColorTable[:], global, padded); err != nil || l != testLen {
    494 		t.Fatalf("Failed to encode global color table: got %d, %v; want nil, %d", l, err, testLen)
    495 	}
    496 	if l, err := encodeColorTable(e.localColorTable[:], local, padded); err != nil || l != testLen {
    497 		t.Fatalf("Failed to encode local color table: got %d, %v; want nil, %d", l, err, testLen)
    498 	}
    499 	if bytes.Equal(e.globalColorTable[:testLen], e.localColorTable[:testLen]) {
    500 		t.Fatal("Encoded color tables are equal, expected mismatch")
    501 	}
    502 	if !e.colorTablesMatch(len(local), trIdx) {
    503 		t.Fatal("colorTablesMatch() == false, expected true")
    504 	}
    505 }
    506 
    507 func TestEncodeCroppedSubImages(t *testing.T) {
    508 	// This test means to ensure that Encode honors the Bounds and Strides of
    509 	// images correctly when encoding.
    510 	whole := image.NewPaletted(image.Rect(0, 0, 100, 100), palette.Plan9)
    511 	subImages := []image.Rectangle{
    512 		image.Rect(0, 0, 50, 50),
    513 		image.Rect(50, 0, 100, 50),
    514 		image.Rect(0, 50, 50, 50),
    515 		image.Rect(50, 50, 100, 100),
    516 		image.Rect(25, 25, 75, 75),
    517 		image.Rect(0, 0, 100, 50),
    518 		image.Rect(0, 50, 100, 100),
    519 		image.Rect(0, 0, 50, 100),
    520 		image.Rect(50, 0, 100, 100),
    521 	}
    522 	for _, sr := range subImages {
    523 		si := whole.SubImage(sr)
    524 		buf := bytes.NewBuffer(nil)
    525 		if err := Encode(buf, si, nil); err != nil {
    526 			t.Errorf("Encode: sr=%v: %v", sr, err)
    527 			continue
    528 		}
    529 		if _, err := Decode(buf); err != nil {
    530 			t.Errorf("Decode: sr=%v: %v", sr, err)
    531 		}
    532 	}
    533 }
    534 
    535 func BenchmarkEncode(b *testing.B) {
    536 	bo := image.Rect(0, 0, 640, 480)
    537 	rnd := rand.New(rand.NewSource(123))
    538 
    539 	// Restrict to a 256-color paletted image to avoid quantization path.
    540 	palette := make(color.Palette, 256)
    541 	for i := range palette {
    542 		palette[i] = color.RGBA{
    543 			uint8(rnd.Intn(256)),
    544 			uint8(rnd.Intn(256)),
    545 			uint8(rnd.Intn(256)),
    546 			255,
    547 		}
    548 	}
    549 	img := image.NewPaletted(image.Rect(0, 0, 640, 480), palette)
    550 	for y := bo.Min.Y; y < bo.Max.Y; y++ {
    551 		for x := bo.Min.X; x < bo.Max.X; x++ {
    552 			img.Set(x, y, palette[rnd.Intn(256)])
    553 		}
    554 	}
    555 
    556 	b.SetBytes(640 * 480 * 4)
    557 	b.ReportAllocs()
    558 	b.ResetTimer()
    559 	for i := 0; i < b.N; i++ {
    560 		Encode(ioutil.Discard, img, nil)
    561 	}
    562 }
    563 
    564 func BenchmarkQuantizedEncode(b *testing.B) {
    565 	img := image.NewRGBA(image.Rect(0, 0, 640, 480))
    566 	bo := img.Bounds()
    567 	rnd := rand.New(rand.NewSource(123))
    568 	for y := bo.Min.Y; y < bo.Max.Y; y++ {
    569 		for x := bo.Min.X; x < bo.Max.X; x++ {
    570 			img.SetRGBA(x, y, color.RGBA{
    571 				uint8(rnd.Intn(256)),
    572 				uint8(rnd.Intn(256)),
    573 				uint8(rnd.Intn(256)),
    574 				255,
    575 			})
    576 		}
    577 	}
    578 	b.SetBytes(640 * 480 * 4)
    579 	b.ReportAllocs()
    580 	b.ResetTimer()
    581 	for i := 0; i < b.N; i++ {
    582 		Encode(ioutil.Discard, img, nil)
    583 	}
    584 }
    585