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
5package gif
6
7import (
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
20func 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
30func 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
39func 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.
49func 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.
69var _ writer = blockWriter{}
70
71var 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
80func 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
111func 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.
136func 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
161var frames = []string{
162	"../testdata/video-001.gif",
163	"../testdata/video-005.gray.gif",
164}
165
166func 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
267func TestEncodeAllGo1Dot4(t *testing.T)                 { testEncodeAll(t, false, false) }
268func TestEncodeAllGo1Dot5(t *testing.T)                 { testEncodeAll(t, true, false) }
269func TestEncodeAllGo1Dot5GlobalColorModel(t *testing.T) { testEncodeAll(t, true, true) }
270
271func 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
298func 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
304func 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
333func 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
359func 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
390func 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
445func 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
478func 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
507func 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
535func 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
564func 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