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