1// Copyright 2010 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 draw
6
7import (
8	"image"
9	"image/color"
10	"image/png"
11	"os"
12	"testing"
13)
14
15func eq(c0, c1 color.Color) bool {
16	r0, g0, b0, a0 := c0.RGBA()
17	r1, g1, b1, a1 := c1.RGBA()
18	return r0 == r1 && g0 == g1 && b0 == b1 && a0 == a1
19}
20
21func fillBlue(alpha int) image.Image {
22	return image.NewUniform(color.RGBA{0, 0, uint8(alpha), uint8(alpha)})
23}
24
25func fillAlpha(alpha int) image.Image {
26	return image.NewUniform(color.Alpha{uint8(alpha)})
27}
28
29func vgradGreen(alpha int) image.Image {
30	m := image.NewRGBA(image.Rect(0, 0, 16, 16))
31	for y := 0; y < 16; y++ {
32		for x := 0; x < 16; x++ {
33			m.Set(x, y, color.RGBA{0, uint8(y * alpha / 15), 0, uint8(alpha)})
34		}
35	}
36	return m
37}
38
39func vgradAlpha(alpha int) image.Image {
40	m := image.NewAlpha(image.Rect(0, 0, 16, 16))
41	for y := 0; y < 16; y++ {
42		for x := 0; x < 16; x++ {
43			m.Set(x, y, color.Alpha{uint8(y * alpha / 15)})
44		}
45	}
46	return m
47}
48
49func vgradGreenNRGBA(alpha int) image.Image {
50	m := image.NewNRGBA(image.Rect(0, 0, 16, 16))
51	for y := 0; y < 16; y++ {
52		for x := 0; x < 16; x++ {
53			m.Set(x, y, color.RGBA{0, uint8(y * 0x11), 0, uint8(alpha)})
54		}
55	}
56	return m
57}
58
59func vgradCr() image.Image {
60	m := &image.YCbCr{
61		Y:              make([]byte, 16*16),
62		Cb:             make([]byte, 16*16),
63		Cr:             make([]byte, 16*16),
64		YStride:        16,
65		CStride:        16,
66		SubsampleRatio: image.YCbCrSubsampleRatio444,
67		Rect:           image.Rect(0, 0, 16, 16),
68	}
69	for y := 0; y < 16; y++ {
70		for x := 0; x < 16; x++ {
71			m.Cr[y*m.CStride+x] = uint8(y * 0x11)
72		}
73	}
74	return m
75}
76
77func hgradRed(alpha int) Image {
78	m := image.NewRGBA(image.Rect(0, 0, 16, 16))
79	for y := 0; y < 16; y++ {
80		for x := 0; x < 16; x++ {
81			m.Set(x, y, color.RGBA{uint8(x * alpha / 15), 0, 0, uint8(alpha)})
82		}
83	}
84	return m
85}
86
87func gradYellow(alpha int) Image {
88	m := image.NewRGBA(image.Rect(0, 0, 16, 16))
89	for y := 0; y < 16; y++ {
90		for x := 0; x < 16; x++ {
91			m.Set(x, y, color.RGBA{uint8(x * alpha / 15), uint8(y * alpha / 15), 0, uint8(alpha)})
92		}
93	}
94	return m
95}
96
97type drawTest struct {
98	desc     string
99	src      image.Image
100	mask     image.Image
101	op       Op
102	expected color.Color
103}
104
105var drawTests = []drawTest{
106	// Uniform mask (0% opaque).
107	{"nop", vgradGreen(255), fillAlpha(0), Over, color.RGBA{136, 0, 0, 255}},
108	{"clear", vgradGreen(255), fillAlpha(0), Src, color.RGBA{0, 0, 0, 0}},
109	// Uniform mask (100%, 75%, nil) and uniform source.
110	// At (x, y) == (8, 8):
111	// The destination pixel is {136, 0, 0, 255}.
112	// The source pixel is {0, 0, 90, 90}.
113	{"fill", fillBlue(90), fillAlpha(255), Over, color.RGBA{88, 0, 90, 255}},
114	{"fillSrc", fillBlue(90), fillAlpha(255), Src, color.RGBA{0, 0, 90, 90}},
115	{"fillAlpha", fillBlue(90), fillAlpha(192), Over, color.RGBA{100, 0, 68, 255}},
116	{"fillAlphaSrc", fillBlue(90), fillAlpha(192), Src, color.RGBA{0, 0, 68, 68}},
117	{"fillNil", fillBlue(90), nil, Over, color.RGBA{88, 0, 90, 255}},
118	{"fillNilSrc", fillBlue(90), nil, Src, color.RGBA{0, 0, 90, 90}},
119	// Uniform mask (100%, 75%, nil) and variable source.
120	// At (x, y) == (8, 8):
121	// The destination pixel is {136, 0, 0, 255}.
122	// The source pixel is {0, 48, 0, 90}.
123	{"copy", vgradGreen(90), fillAlpha(255), Over, color.RGBA{88, 48, 0, 255}},
124	{"copySrc", vgradGreen(90), fillAlpha(255), Src, color.RGBA{0, 48, 0, 90}},
125	{"copyAlpha", vgradGreen(90), fillAlpha(192), Over, color.RGBA{100, 36, 0, 255}},
126	{"copyAlphaSrc", vgradGreen(90), fillAlpha(192), Src, color.RGBA{0, 36, 0, 68}},
127	{"copyNil", vgradGreen(90), nil, Over, color.RGBA{88, 48, 0, 255}},
128	{"copyNilSrc", vgradGreen(90), nil, Src, color.RGBA{0, 48, 0, 90}},
129	// Uniform mask (100%, 75%, nil) and variable NRGBA source.
130	// At (x, y) == (8, 8):
131	// The destination pixel is {136, 0, 0, 255}.
132	// The source pixel is {0, 136, 0, 90} in NRGBA-space, which is {0, 48, 0, 90} in RGBA-space.
133	// The result pixel is different than in the "copy*" test cases because of rounding errors.
134	{"nrgba", vgradGreenNRGBA(90), fillAlpha(255), Over, color.RGBA{88, 46, 0, 255}},
135	{"nrgbaSrc", vgradGreenNRGBA(90), fillAlpha(255), Src, color.RGBA{0, 46, 0, 90}},
136	{"nrgbaAlpha", vgradGreenNRGBA(90), fillAlpha(192), Over, color.RGBA{100, 34, 0, 255}},
137	{"nrgbaAlphaSrc", vgradGreenNRGBA(90), fillAlpha(192), Src, color.RGBA{0, 34, 0, 68}},
138	{"nrgbaNil", vgradGreenNRGBA(90), nil, Over, color.RGBA{88, 46, 0, 255}},
139	{"nrgbaNilSrc", vgradGreenNRGBA(90), nil, Src, color.RGBA{0, 46, 0, 90}},
140	// Uniform mask (100%, 75%, nil) and variable YCbCr source.
141	// At (x, y) == (8, 8):
142	// The destination pixel is {136, 0, 0, 255}.
143	// The source pixel is {0, 0, 136} in YCbCr-space, which is {11, 38, 0, 255} in RGB-space.
144	{"ycbcr", vgradCr(), fillAlpha(255), Over, color.RGBA{11, 38, 0, 255}},
145	{"ycbcrSrc", vgradCr(), fillAlpha(255), Src, color.RGBA{11, 38, 0, 255}},
146	{"ycbcrAlpha", vgradCr(), fillAlpha(192), Over, color.RGBA{42, 28, 0, 255}},
147	{"ycbcrAlphaSrc", vgradCr(), fillAlpha(192), Src, color.RGBA{8, 28, 0, 192}},
148	{"ycbcrNil", vgradCr(), nil, Over, color.RGBA{11, 38, 0, 255}},
149	{"ycbcrNilSrc", vgradCr(), nil, Src, color.RGBA{11, 38, 0, 255}},
150	// Variable mask and variable source.
151	// At (x, y) == (8, 8):
152	// The destination pixel is {136, 0, 0, 255}.
153	// The source pixel is {0, 0, 255, 255}.
154	// The mask pixel's alpha is 102, or 40%.
155	{"generic", fillBlue(255), vgradAlpha(192), Over, color.RGBA{81, 0, 102, 255}},
156	{"genericSrc", fillBlue(255), vgradAlpha(192), Src, color.RGBA{0, 0, 102, 102}},
157}
158
159func makeGolden(dst image.Image, r image.Rectangle, src image.Image, sp image.Point, mask image.Image, mp image.Point, op Op) image.Image {
160	// Since golden is a newly allocated image, we don't have to check if the
161	// input source and mask images and the output golden image overlap.
162	b := dst.Bounds()
163	sb := src.Bounds()
164	mb := image.Rect(-1e9, -1e9, 1e9, 1e9)
165	if mask != nil {
166		mb = mask.Bounds()
167	}
168	golden := image.NewRGBA(image.Rect(0, 0, b.Max.X, b.Max.Y))
169	for y := r.Min.Y; y < r.Max.Y; y++ {
170		sy := y + sp.Y - r.Min.Y
171		my := y + mp.Y - r.Min.Y
172		for x := r.Min.X; x < r.Max.X; x++ {
173			if !(image.Pt(x, y).In(b)) {
174				continue
175			}
176			sx := x + sp.X - r.Min.X
177			if !(image.Pt(sx, sy).In(sb)) {
178				continue
179			}
180			mx := x + mp.X - r.Min.X
181			if !(image.Pt(mx, my).In(mb)) {
182				continue
183			}
184
185			const M = 1<<16 - 1
186			var dr, dg, db, da uint32
187			if op == Over {
188				dr, dg, db, da = dst.At(x, y).RGBA()
189			}
190			sr, sg, sb, sa := src.At(sx, sy).RGBA()
191			ma := uint32(M)
192			if mask != nil {
193				_, _, _, ma = mask.At(mx, my).RGBA()
194			}
195			a := M - (sa * ma / M)
196			golden.Set(x, y, color.RGBA64{
197				uint16((dr*a + sr*ma) / M),
198				uint16((dg*a + sg*ma) / M),
199				uint16((db*a + sb*ma) / M),
200				uint16((da*a + sa*ma) / M),
201			})
202		}
203	}
204	return golden.SubImage(b)
205}
206
207func TestDraw(t *testing.T) {
208	rr := []image.Rectangle{
209		image.Rect(0, 0, 0, 0),
210		image.Rect(0, 0, 16, 16),
211		image.Rect(3, 5, 12, 10),
212		image.Rect(0, 0, 9, 9),
213		image.Rect(8, 8, 16, 16),
214		image.Rect(8, 0, 9, 16),
215		image.Rect(0, 8, 16, 9),
216		image.Rect(8, 8, 9, 9),
217		image.Rect(8, 8, 8, 8),
218	}
219	for _, r := range rr {
220	loop:
221		for _, test := range drawTests {
222			dst := hgradRed(255).(*image.RGBA).SubImage(r).(Image)
223			// Draw the (src, mask, op) onto a copy of dst using a slow but obviously correct implementation.
224			golden := makeGolden(dst, image.Rect(0, 0, 16, 16), test.src, image.ZP, test.mask, image.ZP, test.op)
225			b := dst.Bounds()
226			if !b.Eq(golden.Bounds()) {
227				t.Errorf("draw %v %s: bounds %v versus %v", r, test.desc, dst.Bounds(), golden.Bounds())
228				continue
229			}
230			// Draw the same combination onto the actual dst using the optimized DrawMask implementation.
231			DrawMask(dst, image.Rect(0, 0, 16, 16), test.src, image.ZP, test.mask, image.ZP, test.op)
232			if image.Pt(8, 8).In(r) {
233				// Check that the resultant pixel at (8, 8) matches what we expect
234				// (the expected value can be verified by hand).
235				if !eq(dst.At(8, 8), test.expected) {
236					t.Errorf("draw %v %s: at (8, 8) %v versus %v", r, test.desc, dst.At(8, 8), test.expected)
237					continue
238				}
239			}
240			// Check that the resultant dst image matches the golden output.
241			for y := b.Min.Y; y < b.Max.Y; y++ {
242				for x := b.Min.X; x < b.Max.X; x++ {
243					if !eq(dst.At(x, y), golden.At(x, y)) {
244						t.Errorf("draw %v %s: at (%d, %d), %v versus golden %v", r, test.desc, x, y, dst.At(x, y), golden.At(x, y))
245						continue loop
246					}
247				}
248			}
249		}
250	}
251}
252
253func TestDrawOverlap(t *testing.T) {
254	for _, op := range []Op{Over, Src} {
255		for yoff := -2; yoff <= 2; yoff++ {
256		loop:
257			for xoff := -2; xoff <= 2; xoff++ {
258				m := gradYellow(127).(*image.RGBA)
259				dst := m.SubImage(image.Rect(5, 5, 10, 10)).(*image.RGBA)
260				src := m.SubImage(image.Rect(5+xoff, 5+yoff, 10+xoff, 10+yoff)).(*image.RGBA)
261				b := dst.Bounds()
262				// Draw the (src, mask, op) onto a copy of dst using a slow but obviously correct implementation.
263				golden := makeGolden(dst, b, src, src.Bounds().Min, nil, image.ZP, op)
264				if !b.Eq(golden.Bounds()) {
265					t.Errorf("drawOverlap xoff=%d,yoff=%d: bounds %v versus %v", xoff, yoff, dst.Bounds(), golden.Bounds())
266					continue
267				}
268				// Draw the same combination onto the actual dst using the optimized DrawMask implementation.
269				DrawMask(dst, b, src, src.Bounds().Min, nil, image.ZP, op)
270				// Check that the resultant dst image matches the golden output.
271				for y := b.Min.Y; y < b.Max.Y; y++ {
272					for x := b.Min.X; x < b.Max.X; x++ {
273						if !eq(dst.At(x, y), golden.At(x, y)) {
274							t.Errorf("drawOverlap xoff=%d,yoff=%d: at (%d, %d), %v versus golden %v", xoff, yoff, x, y, dst.At(x, y), golden.At(x, y))
275							continue loop
276						}
277					}
278				}
279			}
280		}
281	}
282}
283
284// TestNonZeroSrcPt checks drawing with a non-zero src point parameter.
285func TestNonZeroSrcPt(t *testing.T) {
286	a := image.NewRGBA(image.Rect(0, 0, 1, 1))
287	b := image.NewRGBA(image.Rect(0, 0, 2, 2))
288	b.Set(0, 0, color.RGBA{0, 0, 0, 5})
289	b.Set(1, 0, color.RGBA{0, 0, 5, 5})
290	b.Set(0, 1, color.RGBA{0, 5, 0, 5})
291	b.Set(1, 1, color.RGBA{5, 0, 0, 5})
292	Draw(a, image.Rect(0, 0, 1, 1), b, image.Pt(1, 1), Over)
293	if !eq(color.RGBA{5, 0, 0, 5}, a.At(0, 0)) {
294		t.Errorf("non-zero src pt: want %v got %v", color.RGBA{5, 0, 0, 5}, a.At(0, 0))
295	}
296}
297
298func TestFill(t *testing.T) {
299	rr := []image.Rectangle{
300		image.Rect(0, 0, 0, 0),
301		image.Rect(0, 0, 40, 30),
302		image.Rect(10, 0, 40, 30),
303		image.Rect(0, 20, 40, 30),
304		image.Rect(10, 20, 40, 30),
305		image.Rect(10, 20, 15, 25),
306		image.Rect(10, 0, 35, 30),
307		image.Rect(0, 15, 40, 16),
308		image.Rect(24, 24, 25, 25),
309		image.Rect(23, 23, 26, 26),
310		image.Rect(22, 22, 27, 27),
311		image.Rect(21, 21, 28, 28),
312		image.Rect(20, 20, 29, 29),
313	}
314	for _, r := range rr {
315		m := image.NewRGBA(image.Rect(0, 0, 40, 30)).SubImage(r).(*image.RGBA)
316		b := m.Bounds()
317		c := color.RGBA{11, 0, 0, 255}
318		src := &image.Uniform{C: c}
319		check := func(desc string) {
320			for y := b.Min.Y; y < b.Max.Y; y++ {
321				for x := b.Min.X; x < b.Max.X; x++ {
322					if !eq(c, m.At(x, y)) {
323						t.Errorf("%s fill: at (%d, %d), sub-image bounds=%v: want %v got %v", desc, x, y, r, c, m.At(x, y))
324						return
325					}
326				}
327			}
328		}
329		// Draw 1 pixel at a time.
330		for y := b.Min.Y; y < b.Max.Y; y++ {
331			for x := b.Min.X; x < b.Max.X; x++ {
332				DrawMask(m, image.Rect(x, y, x+1, y+1), src, image.ZP, nil, image.ZP, Src)
333			}
334		}
335		check("pixel")
336		// Draw 1 row at a time.
337		c = color.RGBA{0, 22, 0, 255}
338		src = &image.Uniform{C: c}
339		for y := b.Min.Y; y < b.Max.Y; y++ {
340			DrawMask(m, image.Rect(b.Min.X, y, b.Max.X, y+1), src, image.ZP, nil, image.ZP, Src)
341		}
342		check("row")
343		// Draw 1 column at a time.
344		c = color.RGBA{0, 0, 33, 255}
345		src = &image.Uniform{C: c}
346		for x := b.Min.X; x < b.Max.X; x++ {
347			DrawMask(m, image.Rect(x, b.Min.Y, x+1, b.Max.Y), src, image.ZP, nil, image.ZP, Src)
348		}
349		check("column")
350		// Draw the whole image at once.
351		c = color.RGBA{44, 55, 66, 77}
352		src = &image.Uniform{C: c}
353		DrawMask(m, b, src, image.ZP, nil, image.ZP, Src)
354		check("whole")
355	}
356}
357
358// TestFloydSteinbergCheckerboard tests that the result of Floyd-Steinberg
359// error diffusion of a uniform 50% gray source image with a black-and-white
360// palette is a checkerboard pattern.
361func TestFloydSteinbergCheckerboard(t *testing.T) {
362	b := image.Rect(0, 0, 640, 480)
363	// We can't represent 50% exactly, but 0x7fff / 0xffff is close enough.
364	src := &image.Uniform{color.Gray16{0x7fff}}
365	dst := image.NewPaletted(b, color.Palette{color.Black, color.White})
366	FloydSteinberg.Draw(dst, b, src, image.Point{})
367	nErr := 0
368	for y := b.Min.Y; y < b.Max.Y; y++ {
369		for x := b.Min.X; x < b.Max.X; x++ {
370			got := dst.Pix[dst.PixOffset(x, y)]
371			want := uint8(x+y) % 2
372			if got != want {
373				t.Errorf("at (%d, %d): got %d, want %d", x, y, got, want)
374				if nErr++; nErr == 10 {
375					t.Fatal("there may be more errors")
376				}
377			}
378		}
379	}
380}
381
382// embeddedPaletted is an Image that behaves like an *image.Paletted but whose
383// type is not *image.Paletted.
384type embeddedPaletted struct {
385	*image.Paletted
386}
387
388// TestPaletted tests that the drawPaletted function behaves the same
389// regardless of whether dst is an *image.Paletted.
390func TestPaletted(t *testing.T) {
391	f, err := os.Open("../testdata/video-001.png")
392	if err != nil {
393		t.Fatalf("open: %v", err)
394	}
395	defer f.Close()
396	src, err := png.Decode(f)
397	if err != nil {
398		t.Fatalf("decode: %v", err)
399	}
400	b := src.Bounds()
401
402	cgaPalette := color.Palette{
403		color.RGBA{0x00, 0x00, 0x00, 0xff},
404		color.RGBA{0x55, 0xff, 0xff, 0xff},
405		color.RGBA{0xff, 0x55, 0xff, 0xff},
406		color.RGBA{0xff, 0xff, 0xff, 0xff},
407	}
408	drawers := map[string]Drawer{
409		"src":             Src,
410		"floyd-steinberg": FloydSteinberg,
411	}
412
413loop:
414	for dName, d := range drawers {
415		dst0 := image.NewPaletted(b, cgaPalette)
416		dst1 := image.NewPaletted(b, cgaPalette)
417		d.Draw(dst0, b, src, image.Point{})
418		d.Draw(embeddedPaletted{dst1}, b, src, image.Point{})
419		for y := b.Min.Y; y < b.Max.Y; y++ {
420			for x := b.Min.X; x < b.Max.X; x++ {
421				if !eq(dst0.At(x, y), dst1.At(x, y)) {
422					t.Errorf("%s: at (%d, %d), %v versus %v",
423						dName, x, y, dst0.At(x, y), dst1.At(x, y))
424					continue loop
425				}
426			}
427		}
428	}
429}
430