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