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