git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/imaging/io_test.go (about)

     1  package imaging
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"image"
     7  	"image/color"
     8  	"image/color/palette"
     9  	"image/draw"
    10  	"image/png"
    11  	"io"
    12  	"io/ioutil"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"testing"
    17  )
    18  
    19  var (
    20  	errCreate = errors.New("failed to create file")
    21  	errClose  = errors.New("failed to close file")
    22  	errOpen   = errors.New("failed to open file")
    23  )
    24  
    25  type badFS struct{}
    26  
    27  func (badFS) Create(name string) (io.WriteCloser, error) {
    28  	if name == "badFile.jpg" {
    29  		return badFile{ioutil.Discard}, nil
    30  	}
    31  	return nil, errCreate
    32  }
    33  
    34  func (badFS) Open(name string) (io.ReadCloser, error) {
    35  	return nil, errOpen
    36  }
    37  
    38  type badFile struct {
    39  	io.Writer
    40  }
    41  
    42  func (badFile) Close() error {
    43  	return errClose
    44  }
    45  
    46  type quantizer struct {
    47  	palette []color.Color
    48  }
    49  
    50  func (q quantizer) Quantize(p color.Palette, m image.Image) color.Palette {
    51  	pal := make([]color.Color, len(p), cap(p))
    52  	copy(pal, p)
    53  	n := cap(p) - len(p)
    54  	if n > len(q.palette) {
    55  		n = len(q.palette)
    56  	}
    57  	for i := 0; i < n; i++ {
    58  		pal = append(pal, q.palette[i])
    59  	}
    60  	return pal
    61  }
    62  
    63  func TestOpenSave(t *testing.T) {
    64  	imgWithoutAlpha := image.NewNRGBA(image.Rect(0, 0, 4, 6))
    65  	imgWithoutAlpha.Pix = []uint8{
    66  		0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    67  		0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    68  		0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
    69  		0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
    70  		0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x88, 0x88, 0x88, 0xff, 0x88, 0x88, 0x88, 0xff,
    71  		0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x88, 0x88, 0x88, 0xff, 0x88, 0x88, 0x88, 0xff,
    72  	}
    73  	imgWithAlpha := image.NewNRGBA(image.Rect(0, 0, 4, 6))
    74  	imgWithAlpha.Pix = []uint8{
    75  		0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    76  		0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    77  		0xff, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80,
    78  		0xff, 0x00, 0x00, 0x80, 0xff, 0x00, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80, 0x00, 0xff, 0x00, 0x80,
    79  		0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x88, 0x88, 0x88, 0x00, 0x88, 0x88, 0x88, 0x00,
    80  		0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x88, 0x88, 0x88, 0x00, 0x88, 0x88, 0x88, 0x00,
    81  	}
    82  
    83  	options := [][]EncodeOption{
    84  		{
    85  			JPEGQuality(100),
    86  		},
    87  		{
    88  			JPEGQuality(99),
    89  			GIFDrawer(draw.FloydSteinberg),
    90  			GIFNumColors(256),
    91  			GIFQuantizer(quantizer{palette.Plan9}),
    92  			PNGCompressionLevel(png.BestSpeed),
    93  		},
    94  	}
    95  
    96  	dir, err := ioutil.TempDir("", "imaging")
    97  	if err != nil {
    98  		t.Fatalf("failed to create temporary directory: %v", err)
    99  	}
   100  	defer os.RemoveAll(dir)
   101  
   102  	for _, ext := range []string{"jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff"} {
   103  		filename := filepath.Join(dir, "test."+ext)
   104  
   105  		img := imgWithoutAlpha
   106  		if ext == "png" {
   107  			img = imgWithAlpha
   108  		}
   109  
   110  		for _, opts := range options {
   111  			err := Save(img, filename, opts...)
   112  			if err != nil {
   113  				t.Fatalf("failed to save image (%q): %v", filename, err)
   114  			}
   115  
   116  			img2, err := Open(filename)
   117  			if err != nil {
   118  				t.Fatalf("failed to open image (%q): %v", filename, err)
   119  			}
   120  			got := Clone(img2)
   121  
   122  			delta := 0
   123  			if ext == "jpg" || ext == "jpeg" || ext == "gif" {
   124  				delta = 3
   125  			}
   126  
   127  			if !compareNRGBA(got, img, delta) {
   128  				t.Fatalf("bad encode-decode result (ext=%q): got %#v want %#v", ext, got, img)
   129  			}
   130  		}
   131  	}
   132  
   133  	buf := &bytes.Buffer{}
   134  	err = Encode(buf, imgWithAlpha, JPEG)
   135  	if err != nil {
   136  		t.Fatalf("failed to encode alpha to JPEG: %v", err)
   137  	}
   138  
   139  	buf = &bytes.Buffer{}
   140  	err = Encode(buf, imgWithAlpha, Format(100))
   141  	if err != ErrUnsupportedFormat {
   142  		t.Fatalf("got %v want ErrUnsupportedFormat", err)
   143  	}
   144  
   145  	buf = bytes.NewBuffer([]byte("bad data"))
   146  	_, err = Decode(buf)
   147  	if err == nil {
   148  		t.Fatalf("decoding bad data: expected error got nil")
   149  	}
   150  
   151  	err = Save(imgWithAlpha, filepath.Join(dir, "test.unknown"))
   152  	if err != ErrUnsupportedFormat {
   153  		t.Fatalf("got %v want ErrUnsupportedFormat", err)
   154  	}
   155  
   156  	prevFS := fs
   157  	fs = badFS{}
   158  	defer func() { fs = prevFS }()
   159  
   160  	err = Save(imgWithAlpha, "test.jpg")
   161  	if err != errCreate {
   162  		t.Fatalf("got error %v want errCreate", err)
   163  	}
   164  
   165  	err = Save(imgWithAlpha, "badFile.jpg")
   166  	if err != errClose {
   167  		t.Fatalf("got error %v want errClose", err)
   168  	}
   169  
   170  	_, err = Open("test.jpg")
   171  	if err != errOpen {
   172  		t.Fatalf("got error %v want errOpen", err)
   173  	}
   174  }
   175  
   176  func TestFormats(t *testing.T) {
   177  	formatNames := map[Format]string{
   178  		JPEG:       "JPEG",
   179  		PNG:        "PNG",
   180  		GIF:        "GIF",
   181  		BMP:        "BMP",
   182  		TIFF:       "TIFF",
   183  		Format(-1): "",
   184  	}
   185  	for format, name := range formatNames {
   186  		got := format.String()
   187  		if got != name {
   188  			t.Fatalf("got format name %q want %q", got, name)
   189  		}
   190  	}
   191  }
   192  
   193  func TestFormatFromExtension(t *testing.T) {
   194  	testCases := []struct {
   195  		name string
   196  		ext  string
   197  		want Format
   198  		err  error
   199  	}{
   200  		{
   201  			name: "jpg without leading dot",
   202  			ext:  "jpg",
   203  			want: JPEG,
   204  		},
   205  		{
   206  			name: "jpg with leading dot",
   207  			ext:  ".jpg",
   208  			want: JPEG,
   209  		},
   210  		{
   211  			name: "jpg uppercase",
   212  			ext:  ".JPG",
   213  			want: JPEG,
   214  		},
   215  		{
   216  			name: "unsupported",
   217  			ext:  ".unsupportedextension",
   218  			want: -1,
   219  			err:  ErrUnsupportedFormat,
   220  		},
   221  	}
   222  
   223  	for _, tc := range testCases {
   224  		t.Run(tc.name, func(t *testing.T) {
   225  			got, err := FormatFromExtension(tc.ext)
   226  			if err != tc.err {
   227  				t.Errorf("got error %#v want %#v", err, tc.err)
   228  			}
   229  			if got != tc.want {
   230  				t.Errorf("got result %#v want %#v", got, tc.want)
   231  			}
   232  		})
   233  	}
   234  }
   235  
   236  func TestReadOrientation(t *testing.T) {
   237  	testCases := []struct {
   238  		path   string
   239  		orient orientation
   240  	}{
   241  		{"testdata/orientation_0.jpg", 0},
   242  		{"testdata/orientation_1.jpg", 1},
   243  		{"testdata/orientation_2.jpg", 2},
   244  		{"testdata/orientation_3.jpg", 3},
   245  		{"testdata/orientation_4.jpg", 4},
   246  		{"testdata/orientation_5.jpg", 5},
   247  		{"testdata/orientation_6.jpg", 6},
   248  		{"testdata/orientation_7.jpg", 7},
   249  		{"testdata/orientation_8.jpg", 8},
   250  	}
   251  	for _, tc := range testCases {
   252  		f, err := os.Open(tc.path)
   253  		if err != nil {
   254  			t.Fatalf("%q: failed to open: %v", tc.path, err)
   255  		}
   256  		orient := readOrientation(f)
   257  		if orient != tc.orient {
   258  			t.Fatalf("%q: got orientation %d want %d", tc.path, orient, tc.orient)
   259  		}
   260  	}
   261  }
   262  
   263  func TestReadOrientationFails(t *testing.T) {
   264  	testCases := []struct {
   265  		name string
   266  		data string
   267  	}{
   268  		{
   269  			"empty",
   270  			"",
   271  		},
   272  		{
   273  			"missing SOI marker",
   274  			"\xff\xe1",
   275  		},
   276  		{
   277  			"missing APP1 marker",
   278  			"\xff\xd8",
   279  		},
   280  		{
   281  			"short read marker",
   282  			"\xff\xd8\xff",
   283  		},
   284  		{
   285  			"short read block size",
   286  			"\xff\xd8\xff\xe1\x00",
   287  		},
   288  		{
   289  			"invalid marker",
   290  			"\xff\xd8\x00\xe1\x00\x00",
   291  		},
   292  		{
   293  			"block size too small",
   294  			"\xff\xd8\xff\xe0\x00\x01",
   295  		},
   296  		{
   297  			"short read block",
   298  			"\xff\xd8\xff\xe0\x00\x08\x00",
   299  		},
   300  		{
   301  			"missing EXIF header",
   302  			"\xff\xd8\xff\xe1\x00\xff",
   303  		},
   304  		{
   305  			"invalid EXIF header",
   306  			"\xff\xd8\xff\xe1\x00\xff\x00\x00\x00\x00",
   307  		},
   308  		{
   309  			"missing EXIF header tail",
   310  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66",
   311  		},
   312  		{
   313  			"missing byte order tag",
   314  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00",
   315  		},
   316  		{
   317  			"invalid byte order tag",
   318  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x00\x00",
   319  		},
   320  		{
   321  			"missing byte order tail",
   322  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x49\x49",
   323  		},
   324  		{
   325  			"missing exif offset",
   326  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x49\x49\x00\x2a",
   327  		},
   328  		{
   329  			"invalid exif offset",
   330  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x07",
   331  		},
   332  		{
   333  			"read exif offset error",
   334  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x09",
   335  		},
   336  		{
   337  			"missing number of tags",
   338  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08",
   339  		},
   340  		{
   341  			"zero number of tags",
   342  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x00",
   343  		},
   344  		{
   345  			"missing tag",
   346  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01",
   347  		},
   348  		{
   349  			"missing tag offset",
   350  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x00\x00",
   351  		},
   352  		{
   353  			"missing orientation tag",
   354  			"\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",
   355  		},
   356  		{
   357  			"missing orientation tag value offset",
   358  			"\xff\xd8\xff\xe1\x00\xff\x45\x78\x69\x66\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08\x00\x01\x01\x12",
   359  		},
   360  		{
   361  			"missing orientation value",
   362  			"\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",
   363  		},
   364  		{
   365  			"invalid orientation value",
   366  			"\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",
   367  		},
   368  	}
   369  	for _, tc := range testCases {
   370  		t.Run(tc.name, func(t *testing.T) {
   371  			if o := readOrientation(strings.NewReader(tc.data)); o != orientationUnspecified {
   372  				t.Fatalf("got orientation %d want %d", o, orientationUnspecified)
   373  			}
   374  		})
   375  	}
   376  }
   377  
   378  func TestAutoOrientation(t *testing.T) {
   379  	toBW := func(img image.Image) []byte {
   380  		b := img.Bounds()
   381  		data := make([]byte, 0, b.Dx()*b.Dy())
   382  		for x := b.Min.X; x < b.Max.X; x++ {
   383  			for y := b.Min.Y; y < b.Max.Y; y++ {
   384  				c := color.GrayModel.Convert(img.At(x, y)).(color.Gray)
   385  				if c.Y < 128 {
   386  					data = append(data, 1)
   387  				} else {
   388  					data = append(data, 0)
   389  				}
   390  			}
   391  		}
   392  		return data
   393  	}
   394  
   395  	f, err := os.Open("testdata/orientation_0.jpg")
   396  	if err != nil {
   397  		t.Fatalf("os.Open(%q): %v", "testdata/orientation_0.jpg", err)
   398  	}
   399  	orig, _, err := image.Decode(f)
   400  	if err != nil {
   401  		t.Fatalf("image.Decode(%q): %v", "testdata/orientation_0.jpg", err)
   402  	}
   403  	origBW := toBW(orig)
   404  
   405  	testCases := []struct {
   406  		path string
   407  	}{
   408  		{"testdata/orientation_0.jpg"},
   409  		{"testdata/orientation_1.jpg"},
   410  		{"testdata/orientation_2.jpg"},
   411  		{"testdata/orientation_3.jpg"},
   412  		{"testdata/orientation_4.jpg"},
   413  		{"testdata/orientation_5.jpg"},
   414  		{"testdata/orientation_6.jpg"},
   415  		{"testdata/orientation_7.jpg"},
   416  		{"testdata/orientation_8.jpg"},
   417  	}
   418  	for _, tc := range testCases {
   419  		img, err := Open(tc.path, AutoOrientation(true))
   420  		if err != nil {
   421  			t.Fatal(err)
   422  		}
   423  		if img.Bounds() != orig.Bounds() {
   424  			t.Fatalf("%s: got bounds %v want %v", tc.path, img.Bounds(), orig.Bounds())
   425  		}
   426  		imgBW := toBW(img)
   427  		if !bytes.Equal(imgBW, origBW) {
   428  			t.Fatalf("%s: got bw data %v want %v", tc.path, imgBW, origBW)
   429  		}
   430  	}
   431  
   432  	if _, err := Decode(strings.NewReader("invalid data"), AutoOrientation(true)); err == nil {
   433  		t.Fatal("expected error got nil")
   434  	}
   435  }