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 5 package gif 6 7 import ( 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 20 func 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 30 func 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 39 func 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. 49 func 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 var 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 76 func 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 107 func 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. 132 func 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 157 var frames = []string{ 158 "../testdata/video-001.gif", 159 "../testdata/video-005.gray.gif", 160 } 161 162 func 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 263 func TestEncodeAllGo1Dot4(t *testing.T) { testEncodeAll(t, false, false) } 264 func TestEncodeAllGo1Dot5(t *testing.T) { testEncodeAll(t, true, false) } 265 func TestEncodeAllGo1Dot5GlobalColorModel(t *testing.T) { testEncodeAll(t, true, true) } 266 267 func 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 294 func 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 300 func 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 329 func 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 355 func 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 386 func 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 441 func BenchmarkEncode(b *testing.B) { 442 b.StopTimer() 443 444 bo := image.Rect(0, 0, 640, 480) 445 rnd := rand.New(rand.NewSource(123)) 446 447 // Restrict to a 256-color paletted image to avoid quantization path. 448 palette := make(color.Palette, 256) 449 for i := range palette { 450 palette[i] = color.RGBA{ 451 uint8(rnd.Intn(256)), 452 uint8(rnd.Intn(256)), 453 uint8(rnd.Intn(256)), 454 255, 455 } 456 } 457 img := image.NewPaletted(image.Rect(0, 0, 640, 480), palette) 458 for y := bo.Min.Y; y < bo.Max.Y; y++ { 459 for x := bo.Min.X; x < bo.Max.X; x++ { 460 img.Set(x, y, palette[rnd.Intn(256)]) 461 } 462 } 463 464 b.SetBytes(640 * 480 * 4) 465 b.StartTimer() 466 for i := 0; i < b.N; i++ { 467 Encode(ioutil.Discard, img, nil) 468 } 469 } 470 471 func BenchmarkQuantizedEncode(b *testing.B) { 472 b.StopTimer() 473 img := image.NewRGBA(image.Rect(0, 0, 640, 480)) 474 bo := img.Bounds() 475 rnd := rand.New(rand.NewSource(123)) 476 for y := bo.Min.Y; y < bo.Max.Y; y++ { 477 for x := bo.Min.X; x < bo.Max.X; x++ { 478 img.SetRGBA(x, y, color.RGBA{ 479 uint8(rnd.Intn(256)), 480 uint8(rnd.Intn(256)), 481 uint8(rnd.Intn(256)), 482 255, 483 }) 484 } 485 } 486 b.SetBytes(640 * 480 * 4) 487 b.StartTimer() 488 for i := 0; i < b.N; i++ { 489 Encode(ioutil.Discard, img, nil) 490 } 491 } 492