package imaging import ( "bytes" "errors" "image" "image/color" "image/color/palette" "image/draw" "image/png" "io" "io/ioutil" "os" "path/filepath" "strings" "testing" ) var ( errCreate = errors.New("failed to create file") errClose = errors.New("failed to close file") errOpen = errors.New("failed to open file") ) type badFS struct{} func (badFS) Create(name string) (io.WriteCloser, error) { if name == "badFile.jpg" { return badFile{ioutil.Discard}, nil } return nil, errCreate } func (badFS) Open(name string) (io.ReadCloser, error) { return nil, errOpen } type badFile struct { io.Writer } func (badFile) Close() error { return errClose } type quantizer struct { palette []color.Color } func (q quantizer) Quantize(p color.Palette, m image.Image) color.Palette { pal := make([]color.Color, len(p), cap(p)) copy(pal, p) n := cap(p) - len(p) if n > len(q.palette) { n = len(q.palette) } for i := 0; i < n; i++ { pal = append(pal, q.palette[i]) } return pal } func TestOpenSave(t *testing.T) { imgWithoutAlpha := image.NewNRGBA(image.Rect(0, 0, 4, 6)) imgWithoutAlpha.Pix = []uint8{ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x88, 0x88, 0x88, 0xff, 0x88, 0x88, 0x88, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x88, 0x88, 0x88, 0xff, 0x88, 0x88, 0x88, 0xff, } imgWithAlpha := image.NewNRGBA(image.Rect(0, 0, 4, 6)) imgWithAlpha.Pix = []uint8{ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x88, 0x88, 0x88, 0x00, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x88, 0x88, 0x88, 0x00, 0x88, 0x88, 0x88, 0x00, } options := [][]EncodeOption{ { JPEGQuality(100), }, { JPEGQuality(99), GIFDrawer(draw.FloydSteinberg), GIFNumColors(256), GIFQuantizer(quantizer{palette.Plan9}), PNGCompressionLevel(png.BestSpeed), }, } dir, err := ioutil.TempDir("", "imaging") if err != nil { t.Fatalf("failed to create temporary directory: %v", err) } defer os.RemoveAll(dir) for _, ext := range []string{"jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff"} { filename := filepath.Join(dir, "test."+ext) img := imgWithoutAlpha if ext == "png" { img = imgWithAlpha } for _, opts := range options { err := Save(img, filename, opts...) if err != nil { t.Fatalf("failed to save image (%q): %v", filename, err) } img2, err := Open(filename) if err != nil { t.Fatalf("failed to open image (%q): %v", filename, err) } got := Clone(img2) delta := 0 if ext == "jpg" || ext == "jpeg" || ext == "gif" { delta = 3 } if !compareNRGBA(got, img, delta) { t.Fatalf("bad encode-decode result (ext=%q): got %#v want %#v", ext, got, img) } } } buf := &bytes.Buffer{} err = Encode(buf, imgWithAlpha, JPEG) if err != nil { t.Fatalf("failed to encode alpha to JPEG: %v", err) } buf = &bytes.Buffer{} err = Encode(buf, imgWithAlpha, Format(100)) if err != ErrUnsupportedFormat { t.Fatalf("got %v want ErrUnsupportedFormat", err) } buf = bytes.NewBuffer([]byte("bad data")) _, err = Decode(buf) if err == nil { t.Fatalf("decoding bad data: expected error got nil") } err = Save(imgWithAlpha, filepath.Join(dir, "test.unknown")) if err != ErrUnsupportedFormat { t.Fatalf("got %v want ErrUnsupportedFormat", err) } prevFS := fs fs = badFS{} defer func() { fs = prevFS }() err = Save(imgWithAlpha, "test.jpg") if err != errCreate { t.Fatalf("got error %v want errCreate", err) } err = Save(imgWithAlpha, "badFile.jpg") if err != errClose { t.Fatalf("got error %v want errClose", err) } _, err = Open("test.jpg") if err != errOpen { t.Fatalf("got error %v want errOpen", err) } } func TestFormats(t *testing.T) { formatNames := map[Format]string{ JPEG: "JPEG", PNG: "PNG", GIF: "GIF", BMP: "BMP", TIFF: "TIFF", Format(-1): "", } for format, name := range formatNames { got := format.String() if got != name { t.Fatalf("got format name %q want %q", got, name) } } } func TestFormatFromExtension(t *testing.T) { testCases := []struct { name string ext string want Format err error }{ { name: "jpg without leading dot", ext: "jpg", want: JPEG, }, { name: "jpg with leading dot", ext: ".jpg", want: JPEG, }, { name: "jpg uppercase", ext: ".JPG", want: JPEG, }, { name: "unsupported", ext: ".unsupportedextension", want: -1, err: ErrUnsupportedFormat, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got, err := FormatFromExtension(tc.ext) if err != tc.err { t.Errorf("got error %#v want %#v", err, tc.err) } if got != tc.want { t.Errorf("got result %#v want %#v", got, tc.want) } }) } } func TestReadOrientation(t *testing.T) { testCases := []struct { path string orient orientation }{ {"testdata/orientation_0.jpg", 0}, {"testdata/orientation_1.jpg", 1}, {"testdata/orientation_2.jpg", 2}, {"testdata/orientation_3.jpg", 3}, {"testdata/orientation_4.jpg", 4}, {"testdata/orientation_5.jpg", 5}, {"testdata/orientation_6.jpg", 6}, {"testdata/orientation_7.jpg", 7}, {"testdata/orientation_8.jpg", 8}, } for _, tc := range testCases { f, err := os.Open(tc.path) if err != nil { t.Fatalf("%q: failed to open: %v", tc.path, err) } orient := readOrientation(f) if orient != tc.orient { t.Fatalf("%q: got orientation %d want %d", tc.path, orient, tc.orient) } } } func TestReadOrientationFails(t *testing.T) { testCases := []struct { name string data string }{ { "empty", "", }, { "missing SOI marker", "\xff\xe1", }, { "missing APP1 marker", "\xff\xd8", }, { "short read marker", "\xff\xd8\xff", }, { "short read block size", "\xff\xd8\xff\xe1\x00", }, { "invalid marker", "\xff\xd8\x00\xe1\x00\x00", }, { "block size too small", "\xff\xd8\xff\xe0\x00\x01", }, { "short read block", "\xff\xd8\xff\xe0\x00\x08\x00", }, { "missing EXIF header", "\xff\xd8\xff\xe1\x00\xff", }, { "invalid EXIF header", "\xff\xd8\xff\xe1\x00\xff\x00\x00\x00\x00", }, { "missing EXIF header tail", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66", }, { "missing byte order tag", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00", }, { "invalid byte order tag", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x00\x00", }, { "missing byte order tail", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x49\x49", }, { "missing exif offset", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x49\x49\x00\x2a", }, { "invalid exif offset", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x07", }, { "read exif offset error", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x09", }, { "missing number of tags", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08", }, { "zero number of tags", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x00", }, { "missing tag", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01", }, { "missing tag offset", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x00\x00", }, { "missing orientation tag", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", }, { "missing orientation tag value offset", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x01\x12", }, { "missing orientation value", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x01\x12\x00\x03\x00\x00\x00\x01", }, { "invalid orientation value", "\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x01\x12\x00\x03\x00\x00\x00\x01\x00\x09", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if o := readOrientation(strings.NewReader(tc.data)); o != orientationUnspecified { t.Fatalf("got orientation %d want %d", o, orientationUnspecified) } }) } } func TestAutoOrientation(t *testing.T) { toBW := func(img image.Image) []byte { b := img.Bounds() data := make([]byte, 0, b.Dx()*b.Dy()) for x := b.Min.X; x < b.Max.X; x++ { for y := b.Min.Y; y < b.Max.Y; y++ { c := color.GrayModel.Convert(img.At(x, y)).(color.Gray) if c.Y < 128 { data = append(data, 1) } else { data = append(data, 0) } } } return data } f, err := os.Open("testdata/orientation_0.jpg") if err != nil { t.Fatalf("os.Open(%q): %v", "testdata/orientation_0.jpg", err) } orig, _, err := image.Decode(f) if err != nil { t.Fatalf("image.Decode(%q): %v", "testdata/orientation_0.jpg", err) } origBW := toBW(orig) testCases := []struct { path string }{ {"testdata/orientation_0.jpg"}, {"testdata/orientation_1.jpg"}, {"testdata/orientation_2.jpg"}, {"testdata/orientation_3.jpg"}, {"testdata/orientation_4.jpg"}, {"testdata/orientation_5.jpg"}, {"testdata/orientation_6.jpg"}, {"testdata/orientation_7.jpg"}, {"testdata/orientation_8.jpg"}, } for _, tc := range testCases { img, err := Open(tc.path, AutoOrientation(true)) if err != nil { t.Fatal(err) } if img.Bounds() != orig.Bounds() { t.Fatalf("%s: got bounds %v want %v", tc.path, img.Bounds(), orig.Bounds()) } imgBW := toBW(img) if !bytes.Equal(imgBW, origBW) { t.Fatalf("%s: got bw data %v want %v", tc.path, imgBW, origBW) } } if _, err := Decode(strings.NewReader("invalid data"), AutoOrientation(true)); err == nil { t.Fatal("expected error got nil") } }