blob: f0f3768ffaed3a5349166c1fe420c07cf0ac96e9 [file] [log] [blame]
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")
}
}