1/* 2Copyright 2012 The Perkeep Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package images 18 19import ( 20 "bytes" 21 "fmt" 22 "image" 23 "image/jpeg" 24 "io" 25 "os" 26 "path/filepath" 27 "sort" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/rwcarlsen/goexif/exif" 33) 34 35const datadir = "testdata" 36 37func equals(im1, im2 image.Image) bool { 38 if !im1.Bounds().Eq(im2.Bounds()) { 39 return false 40 } 41 for y := 0; y < im1.Bounds().Dy(); y++ { 42 for x := 0; x < im1.Bounds().Dx(); x++ { 43 r1, g1, b1, a1 := im1.At(x, y).RGBA() 44 r2, g2, b2, a2 := im2.At(x, y).RGBA() 45 if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 { 46 return false 47 } 48 } 49 } 50 return true 51} 52 53func straightFImage(t *testing.T) image.Image { 54 g, err := os.Open(filepath.Join(datadir, "f1.jpg")) 55 if err != nil { 56 t.Fatal(err) 57 } 58 defer g.Close() 59 straightF, err := jpeg.Decode(g) 60 if err != nil { 61 t.Fatal(err) 62 } 63 return straightF 64} 65 66func smallStraightFImage(t *testing.T) image.Image { 67 g, err := os.Open(filepath.Join(datadir, "f1-s.jpg")) 68 if err != nil { 69 t.Fatal(err) 70 } 71 defer g.Close() 72 straightF, err := jpeg.Decode(g) 73 if err != nil { 74 t.Fatal(err) 75 } 76 return straightF 77} 78 79func sampleNames(t *testing.T) []string { 80 dir, err := os.Open(datadir) 81 if err != nil { 82 t.Fatal(err) 83 } 84 defer dir.Close() 85 samples, err := dir.Readdirnames(-1) 86 if err != nil { 87 t.Fatal(err) 88 } 89 sort.Strings(samples) 90 return samples 91} 92 93// TestEXIFCorrection tests that the input files with EXIF metadata 94// are correctly automatically rotated/flipped when decoded. 95func TestEXIFCorrection(t *testing.T) { 96 samples := sampleNames(t) 97 straightF := straightFImage(t) 98 for _, v := range samples { 99 if !strings.Contains(v, "exif") || strings.HasSuffix(v, "-s.jpg") { 100 continue 101 } 102 name := filepath.Join(datadir, v) 103 t.Logf("correcting %s with EXIF Orientation", name) 104 f, err := os.Open(name) 105 if err != nil { 106 t.Fatal(err) 107 } 108 defer f.Close() 109 im, _, err := Decode(f, nil) 110 if err != nil { 111 t.Fatal(err) 112 } 113 if !equals(im, straightF) { 114 t.Fatalf("%v not properly corrected with exif", name) 115 } 116 } 117} 118 119// TestForcedCorrection tests that manually specifying the 120// rotation/flipping to be applied when decoding works as 121// expected. 122func TestForcedCorrection(t *testing.T) { 123 samples := sampleNames(t) 124 straightF := straightFImage(t) 125 for _, v := range samples { 126 if strings.HasSuffix(v, "-s.jpg") { 127 continue 128 } 129 if !strings.HasSuffix(v, ".jpg") { 130 continue 131 } 132 name := filepath.Join(datadir, v) 133 t.Logf("forced correction of %s", name) 134 f, err := os.Open(name) 135 if err != nil { 136 t.Fatal(err) 137 } 138 defer f.Close() 139 num := name[10] 140 angle, flipMode := 0, 0 141 switch num { 142 case '1': 143 // nothing to do 144 case '2': 145 flipMode = 2 146 case '3': 147 angle = 180 148 case '4': 149 angle = 180 150 flipMode = 2 151 case '5': 152 angle = -90 153 flipMode = 2 154 case '6': 155 angle = -90 156 case '7': 157 angle = 90 158 flipMode = 2 159 case '8': 160 angle = 90 161 } 162 im, _, err := Decode(f, &DecodeOpts{Rotate: angle, Flip: FlipDirection(flipMode)}) 163 if err != nil { 164 t.Fatal(err) 165 } 166 if !equals(im, straightF) { 167 t.Fatalf("%v not properly corrected", name) 168 } 169 } 170} 171 172// TestRescale verifies that rescaling an image, without 173// any rotation/flipping, produces the expected image. 174func TestRescale(t *testing.T) { 175 name := filepath.Join(datadir, "f1.jpg") 176 t.Logf("rescaling %s with half-width and half-height", name) 177 f, err := os.Open(name) 178 if err != nil { 179 t.Fatal(err) 180 } 181 defer f.Close() 182 rescaledIm, _, err := Decode(f, &DecodeOpts{ScaleWidth: 0.5, ScaleHeight: 0.5}) 183 if err != nil { 184 t.Fatal(err) 185 } 186 187 smallIm := smallStraightFImage(t) 188 189 gotB, wantB := rescaledIm.Bounds(), smallIm.Bounds() 190 if !gotB.Eq(wantB) { 191 t.Errorf("(scale) %v bounds not equal, got %v want %v", name, gotB, wantB) 192 } 193 if !equals(rescaledIm, smallIm) { 194 t.Errorf("(scale) %v pixels not equal", name) 195 } 196 197 _, err = f.Seek(0, io.SeekStart) 198 if err != nil { 199 t.Fatal(err) 200 } 201 202 rescaledIm, _, err = Decode(f, &DecodeOpts{MaxWidth: 2000, MaxHeight: 40}) 203 if err != nil { 204 t.Fatal(err) 205 } 206 gotB = rescaledIm.Bounds() 207 if !gotB.Eq(wantB) { 208 t.Errorf("(max) %v bounds not equal, got %v want %v", name, gotB, wantB) 209 } 210 if !equals(rescaledIm, smallIm) { 211 t.Errorf("(max) %v pixels not equal", name) 212 } 213} 214 215// TestRescaleEXIF verifies that rescaling an image, followed 216// by the automatic EXIF correction (rotation/flipping), 217// produces the expected image. All the possible correction 218// modes are tested. 219func TestRescaleEXIF(t *testing.T) { 220 smallStraightF := smallStraightFImage(t) 221 samples := sampleNames(t) 222 for _, v := range samples { 223 if !strings.Contains(v, "exif") { 224 continue 225 } 226 name := filepath.Join(datadir, v) 227 t.Logf("rescaling %s with half-width and half-height", name) 228 f, err := os.Open(name) 229 if err != nil { 230 t.Fatal(err) 231 } 232 defer f.Close() 233 rescaledIm, _, err := Decode(f, &DecodeOpts{ScaleWidth: 0.5, ScaleHeight: 0.5}) 234 if err != nil { 235 t.Fatal(err) 236 } 237 238 gotB, wantB := rescaledIm.Bounds(), smallStraightF.Bounds() 239 if !gotB.Eq(wantB) { 240 t.Errorf("(scale) %v bounds not equal, got %v want %v", name, gotB, wantB) 241 } 242 if !equals(rescaledIm, smallStraightF) { 243 t.Errorf("(scale) %v pixels not equal", name) 244 } 245 246 _, err = f.Seek(0, io.SeekStart) 247 if err != nil { 248 t.Fatal(err) 249 } 250 rescaledIm, _, err = Decode(f, &DecodeOpts{MaxWidth: 2000, MaxHeight: 40}) 251 if err != nil { 252 t.Fatal(err) 253 } 254 255 gotB = rescaledIm.Bounds() 256 if !gotB.Eq(wantB) { 257 t.Errorf("(max) %v bounds not equal, got %v want %v", name, gotB, wantB) 258 } 259 if !equals(rescaledIm, smallStraightF) { 260 t.Errorf("(max) %v pixels not equal", name) 261 } 262 } 263} 264 265// TestUpscale verifies we don't resize up. 266func TestUpscale(t *testing.T) { 267 b := new(bytes.Buffer) 268 w, h := 64, 48 269 if err := jpeg.Encode(b, image.NewNRGBA(image.Rect(0, 0, w, h)), nil); err != nil { 270 t.Fatal(err) 271 } 272 sizes := []struct { 273 mw, mh int 274 wantW, wantH int 275 }{ 276 {wantW: w, wantH: h}, 277 {mw: w, mh: h, wantW: w, wantH: h}, 278 {mw: w, mh: 2 * h, wantW: w, wantH: h}, 279 {mw: 2 * w, mh: w, wantW: w, wantH: h}, 280 {mw: 2 * w, mh: 2 * h, wantW: w, wantH: h}, 281 {mw: w / 2, mh: h / 2, wantW: w / 2, wantH: h / 2}, 282 {mw: w / 2, mh: 2 * h, wantW: w / 2, wantH: h / 2}, 283 {mw: 2 * w, mh: h / 2, wantW: w / 2, wantH: h / 2}, 284 } 285 for i, size := range sizes { 286 var opts DecodeOpts 287 switch { 288 case size.mw != 0 && size.mh != 0: 289 opts = DecodeOpts{MaxWidth: size.mw, MaxHeight: size.mh} 290 case size.mw != 0: 291 opts = DecodeOpts{MaxWidth: size.mw} 292 case size.mh != 0: 293 opts = DecodeOpts{MaxHeight: size.mh} 294 } 295 im, _, err := Decode(bytes.NewReader(b.Bytes()), &opts) 296 if err != nil { 297 t.Error(i, err) 298 } 299 gotW := im.Bounds().Dx() 300 gotH := im.Bounds().Dy() 301 if gotW != size.wantW || gotH != size.wantH { 302 t.Errorf("%d got %dx%d want %dx%d", i, gotW, gotH, size.wantW, size.wantH) 303 } 304 } 305} 306 307// TODO(mpl): move this test to the goexif lib if/when we contribute 308// back the DateTime stuff to upstream. 309func TestDateTime(t *testing.T) { 310 f, err := os.Open(filepath.Join(datadir, "f1-exif.jpg")) 311 if err != nil { 312 t.Fatal(err) 313 } 314 defer f.Close() 315 ex, err := exif.Decode(f) 316 if err != nil { 317 t.Fatal(err) 318 } 319 got, err := ex.DateTime() 320 if err != nil { 321 t.Fatal(err) 322 } 323 exifTimeLayout := "2006:01:02 15:04:05" 324 want, err := time.ParseInLocation(exifTimeLayout, "2012:11:04 05:42:02", time.Local) 325 if err != nil { 326 t.Fatal(err) 327 } 328 if got != want { 329 t.Fatalf("Creation times differ; got %v, want: %v\n", got, want) 330 } 331} 332 333var issue513tests = []image.Rectangle{ 334 // These test image bounds give a fastjpeg.Factor() result of 1 since 335 // they give dim/max == 1, but require rescaling. 336 image.Rect(0, 0, 500, 500), // The file, bug.jpeg, in issue 315 is a black 500x500. 337 image.Rect(0, 0, 1, 257), 338 image.Rect(0, 0, 1, 511), 339 image.Rect(0, 0, 2001, 1), 340 image.Rect(0, 0, 3999, 1), 341 342 // These test image bounds give either a fastjpeg.Factor() > 1 or 343 // do not require rescaling. 344 image.Rect(0, 0, 1, 256), 345 image.Rect(0, 0, 1, 512), 346 image.Rect(0, 0, 2000, 1), 347 image.Rect(0, 0, 4000, 1), 348} 349 350// Test that decode does not hand off a nil image when using 351// fastjpeg, and fastjpeg.Factor() == 1. 352// See https://perkeep.org/issue/513 353func TestIssue513(t *testing.T) { 354 opts := &DecodeOpts{MaxWidth: 2000, MaxHeight: 256} 355 for _, rect := range issue513tests { 356 buf := &bytes.Buffer{} 357 err := jpeg.Encode(buf, image.NewRGBA(rect), nil) 358 if err != nil { 359 t.Fatalf("Failed to encode test image: %v", err) 360 } 361 func() { 362 defer func() { 363 if r := recover(); r != nil { 364 t.Errorf("Unexpected panic for image size %dx%d: %v", rect.Dx(), rect.Dy(), r) 365 } 366 }() 367 _, format, err, needsRescale := decode(buf, opts, false) 368 if err != nil { 369 t.Errorf("Unexpected error for image size %dx%d: %v", rect.Dx(), rect.Dy(), err) 370 } 371 if format != "jpeg" { 372 t.Errorf("Unexpected format for image size %dx%d: got %q want %q", rect.Dx(), rect.Dy(), format, "jpeg") 373 } 374 if needsRescale != (rect.Dx() > opts.MaxWidth || rect.Dy() > opts.MaxHeight) { 375 t.Errorf("Unexpected rescale for image size %dx%d: needsRescale = %t", rect.Dx(), rect.Dy(), needsRescale) 376 } 377 }() 378 } 379} 380 381func TestHEIFToJPEG(t *testing.T) { 382 filename := filepath.Join("testdata", "IMG_8062.HEIC") 383 f, err := os.Open(filename) 384 if err != nil { 385 t.Fatal(err) 386 } 387 defer f.Close() 388 389 // image is in portrait orientation, so dimensions are swapped 390 wantWidth, wantHeight := 756, 1008 391 max := 1008 392 data, err := HEIFToJPEG(f, &Dimensions{MaxWidth: max, MaxHeight: max}) 393 if err != nil { 394 if _, ok := err.(NoHEICTOJPEGError); ok { 395 t.Skipf("skipping test; missing program: %v", err) 396 } 397 t.Fatal(err) 398 } 399 conf, tp, err := image.DecodeConfig(bytes.NewReader(data)) 400 if err != nil { 401 t.Fatal(err) 402 } 403 if tp != "jpeg" { 404 t.Fatalf("%v not converted into a jpeg", filename) 405 } 406 if conf.Width != wantWidth || conf.Height != wantHeight { 407 t.Fatalf("wrong width or height, wanted (%d, %d), got (%d, %d)", wantWidth, wantHeight, conf.Width, conf.Height) 408 } 409} 410 411func TestDecodeHEIC_WithJPEGInHeader(t *testing.T) { 412 filename := filepath.Join("testdata", "river-truncated.heic") 413 f, err := os.Open(filename) 414 if err != nil { 415 t.Fatal(err) 416 } 417 defer f.Close() 418 419 conf, err := DecodeConfig(f) 420 if err != nil { 421 t.Fatal(err) 422 } 423 got := fmt.Sprintf("Width:%d Height:%d Format:%v HEIC:%d bytes", conf.Width, conf.Height, conf.Format, len(conf.HEICEXIF)) 424 want := "Width:6302 Height:3912 Format:heic HEIC:1046 bytes" 425 if got != want { 426 t.Errorf("Got:\n %s\nWant:\n %s", got, want) 427 } 428} 429