1// Copyright 2014 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 webp
6
7import (
8	"bytes"
9	"fmt"
10	"image"
11	"image/png"
12	"io/ioutil"
13	"os"
14	"strings"
15	"testing"
16)
17
18// hex is like fmt.Sprintf("% x", x) but also inserts dots every 16 bytes, to
19// delineate VP8 macroblock boundaries.
20func hex(x []byte) string {
21	buf := new(bytes.Buffer)
22	for len(x) > 0 {
23		n := len(x)
24		if n > 16 {
25			n = 16
26		}
27		fmt.Fprintf(buf, " . % x", x[:n])
28		x = x[n:]
29	}
30	return buf.String()
31}
32
33func testDecodeLossy(t *testing.T, tc string, withAlpha bool) {
34	webpFilename := "../testdata/" + tc + ".lossy.webp"
35	pngFilename := webpFilename + ".ycbcr.png"
36	if withAlpha {
37		webpFilename = "../testdata/" + tc + ".lossy-with-alpha.webp"
38		pngFilename = webpFilename + ".nycbcra.png"
39	}
40
41	f0, err := os.Open(webpFilename)
42	if err != nil {
43		t.Errorf("%s: Open WEBP: %v", tc, err)
44		return
45	}
46	defer f0.Close()
47	img0, err := Decode(f0)
48	if err != nil {
49		t.Errorf("%s: Decode WEBP: %v", tc, err)
50		return
51	}
52
53	var (
54		m0 *image.YCbCr
55		a0 *image.NYCbCrA
56		ok bool
57	)
58	if withAlpha {
59		a0, ok = img0.(*image.NYCbCrA)
60		if ok {
61			m0 = &a0.YCbCr
62		}
63	} else {
64		m0, ok = img0.(*image.YCbCr)
65	}
66	if !ok || m0.SubsampleRatio != image.YCbCrSubsampleRatio420 {
67		t.Errorf("%s: decoded WEBP image is not a 4:2:0 YCbCr or 4:2:0 NYCbCrA", tc)
68		return
69	}
70	// w2 and h2 are the half-width and half-height, rounded up.
71	w, h := m0.Bounds().Dx(), m0.Bounds().Dy()
72	w2, h2 := int((w+1)/2), int((h+1)/2)
73
74	f1, err := os.Open(pngFilename)
75	if err != nil {
76		t.Errorf("%s: Open PNG: %v", tc, err)
77		return
78	}
79	defer f1.Close()
80	img1, err := png.Decode(f1)
81	if err != nil {
82		t.Errorf("%s: Open PNG: %v", tc, err)
83		return
84	}
85
86	// The split-into-YCbCr-planes golden image is a 2*w2 wide and h+h2 high
87	// (or 2*h+h2 high, if with Alpha) gray image arranged in IMC4 format:
88	//   YYYY
89	//   YYYY
90	//   BBRR
91	//   AAAA
92	// See http://www.fourcc.org/yuv.php#IMC4
93	pngW, pngH := 2*w2, h+h2
94	if withAlpha {
95		pngH += h
96	}
97	if got, want := img1.Bounds(), image.Rect(0, 0, pngW, pngH); got != want {
98		t.Errorf("%s: bounds0: got %v, want %v", tc, got, want)
99		return
100	}
101	m1, ok := img1.(*image.Gray)
102	if !ok {
103		t.Errorf("%s: decoded PNG image is not a Gray", tc)
104		return
105	}
106
107	type plane struct {
108		name     string
109		m0Pix    []uint8
110		m0Stride int
111		m1Rect   image.Rectangle
112	}
113	planes := []plane{
114		{"Y", m0.Y, m0.YStride, image.Rect(0, 0, w, h)},
115		{"Cb", m0.Cb, m0.CStride, image.Rect(0*w2, h, 1*w2, h+h2)},
116		{"Cr", m0.Cr, m0.CStride, image.Rect(1*w2, h, 2*w2, h+h2)},
117	}
118	if withAlpha {
119		planes = append(planes, plane{
120			"A", a0.A, a0.AStride, image.Rect(0, h+h2, w, 2*h+h2),
121		})
122	}
123
124	for _, plane := range planes {
125		dx := plane.m1Rect.Dx()
126		nDiff, diff := 0, make([]byte, dx)
127		for j, y := 0, plane.m1Rect.Min.Y; y < plane.m1Rect.Max.Y; j, y = j+1, y+1 {
128			got := plane.m0Pix[j*plane.m0Stride:][:dx]
129			want := m1.Pix[y*m1.Stride+plane.m1Rect.Min.X:][:dx]
130			if bytes.Equal(got, want) {
131				continue
132			}
133			nDiff++
134			if nDiff > 10 {
135				t.Errorf("%s: %s plane: more rows differ", tc, plane.name)
136				break
137			}
138			for i := range got {
139				diff[i] = got[i] - want[i]
140			}
141			t.Errorf("%s: %s plane: m0 row %d, m1 row %d\ngot %s\nwant%s\ndiff%s",
142				tc, plane.name, j, y, hex(got), hex(want), hex(diff))
143		}
144	}
145}
146
147func TestDecodeVP8(t *testing.T) {
148	testCases := []string{
149		"blue-purple-pink",
150		"blue-purple-pink-large.no-filter",
151		"blue-purple-pink-large.simple-filter",
152		"blue-purple-pink-large.normal-filter",
153		"video-001",
154		"yellow_rose",
155	}
156
157	for _, tc := range testCases {
158		testDecodeLossy(t, tc, false)
159	}
160}
161
162func TestDecodeVP8XAlpha(t *testing.T) {
163	testCases := []string{
164		"yellow_rose",
165	}
166
167	for _, tc := range testCases {
168		testDecodeLossy(t, tc, true)
169	}
170}
171
172func TestDecodeVP8L(t *testing.T) {
173	testCases := []string{
174		"blue-purple-pink",
175		"blue-purple-pink-large",
176		"gopher-doc.1bpp",
177		"gopher-doc.2bpp",
178		"gopher-doc.4bpp",
179		"gopher-doc.8bpp",
180		"tux",
181		"yellow_rose",
182	}
183
184loop:
185	for _, tc := range testCases {
186		f0, err := os.Open("../testdata/" + tc + ".lossless.webp")
187		if err != nil {
188			t.Errorf("%s: Open WEBP: %v", tc, err)
189			continue
190		}
191		defer f0.Close()
192		img0, err := Decode(f0)
193		if err != nil {
194			t.Errorf("%s: Decode WEBP: %v", tc, err)
195			continue
196		}
197		m0, ok := img0.(*image.NRGBA)
198		if !ok {
199			t.Errorf("%s: WEBP image is %T, want *image.NRGBA", tc, img0)
200			continue
201		}
202
203		f1, err := os.Open("../testdata/" + tc + ".png")
204		if err != nil {
205			t.Errorf("%s: Open PNG: %v", tc, err)
206			continue
207		}
208		defer f1.Close()
209		img1, err := png.Decode(f1)
210		if err != nil {
211			t.Errorf("%s: Decode PNG: %v", tc, err)
212			continue
213		}
214		m1, ok := img1.(*image.NRGBA)
215		if !ok {
216			rgba1, ok := img1.(*image.RGBA)
217			if !ok {
218				t.Fatalf("%s: PNG image is %T, want *image.NRGBA", tc, img1)
219				continue
220			}
221			if !rgba1.Opaque() {
222				t.Fatalf("%s: PNG image is non-opaque *image.RGBA, want *image.NRGBA", tc)
223				continue
224			}
225			// The image is fully opaque, so we can re-interpret the RGBA pixels
226			// as NRGBA pixels.
227			m1 = &image.NRGBA{
228				Pix:    rgba1.Pix,
229				Stride: rgba1.Stride,
230				Rect:   rgba1.Rect,
231			}
232		}
233
234		b0, b1 := m0.Bounds(), m1.Bounds()
235		if b0 != b1 {
236			t.Errorf("%s: bounds: got %v, want %v", tc, b0, b1)
237			continue
238		}
239		for i := range m0.Pix {
240			if m0.Pix[i] != m1.Pix[i] {
241				y := i / m0.Stride
242				x := (i - y*m0.Stride) / 4
243				i = 4 * (y*m0.Stride + x)
244				t.Errorf("%s: at (%d, %d):\ngot  %02x %02x %02x %02x\nwant %02x %02x %02x %02x",
245					tc, x, y,
246					m0.Pix[i+0], m0.Pix[i+1], m0.Pix[i+2], m0.Pix[i+3],
247					m1.Pix[i+0], m1.Pix[i+1], m1.Pix[i+2], m1.Pix[i+3],
248				)
249				continue loop
250			}
251		}
252	}
253}
254
255// TestDecodePartitionTooLarge tests that decoding a malformed WEBP image
256// doesn't try to allocate an unreasonable amount of memory. This WEBP image
257// claims a RIFF chunk length of 0x12345678 bytes (291 MiB) compressed,
258// independent of the actual image size (0 pixels wide * 0 pixels high).
259//
260// This is based on golang.org/issue/10790.
261func TestDecodePartitionTooLarge(t *testing.T) {
262	data := "RIFF\xff\xff\xff\x7fWEBPVP8 " +
263		"\x78\x56\x34\x12" + // RIFF chunk length.
264		"\xbd\x01\x00\x14\x00\x00\xb2\x34\x0a\x9d\x01\x2a\x96\x00\x67\x00"
265	_, err := Decode(strings.NewReader(data))
266	if err == nil {
267		t.Fatal("got nil error, want non-nil")
268	}
269	if got, want := err.Error(), "too much data"; !strings.Contains(got, want) {
270		t.Fatalf("got error %q, want something containing %q", got, want)
271	}
272}
273
274func benchmarkDecode(b *testing.B, filename string) {
275	data, err := ioutil.ReadFile("../testdata/blue-purple-pink-large." + filename + ".webp")
276	if err != nil {
277		b.Fatal(err)
278	}
279	s := string(data)
280	cfg, err := DecodeConfig(strings.NewReader(s))
281	if err != nil {
282		b.Fatal(err)
283	}
284	b.SetBytes(int64(cfg.Width * cfg.Height * 4))
285	b.ResetTimer()
286	for i := 0; i < b.N; i++ {
287		Decode(strings.NewReader(s))
288	}
289}
290
291func BenchmarkDecodeVP8NoFilter(b *testing.B)     { benchmarkDecode(b, "no-filter.lossy") }
292func BenchmarkDecodeVP8SimpleFilter(b *testing.B) { benchmarkDecode(b, "simple-filter.lossy") }
293func BenchmarkDecodeVP8NormalFilter(b *testing.B) { benchmarkDecode(b, "normal-filter.lossy") }
294func BenchmarkDecodeVP8L(b *testing.B)            { benchmarkDecode(b, "lossless") }
295