diff --git a/helpers.go b/helpers.go deleted file mode 100644 index 2381a07..0000000 --- a/helpers.go +++ /dev/null @@ -1,278 +0,0 @@ -package imaging - -import ( - "bytes" - "errors" - "image" - "image/color" - "image/draw" - "image/gif" - "image/jpeg" - "image/png" - "io" - "os" - "path/filepath" - "strings" - - "golang.org/x/image/bmp" - "golang.org/x/image/tiff" -) - -// Format is an image file format. -type Format int - -// Image file formats. -const ( - JPEG Format = iota - PNG - GIF - TIFF - BMP -) - -func (f Format) String() string { - switch f { - case JPEG: - return "JPEG" - case PNG: - return "PNG" - case GIF: - return "GIF" - case TIFF: - return "TIFF" - case BMP: - return "BMP" - default: - return "Unsupported" - } -} - -var formatFromExt = map[string]Format{ - "jpg": JPEG, - "jpeg": JPEG, - "png": PNG, - "tif": TIFF, - "tiff": TIFF, - "bmp": BMP, - "gif": GIF, -} - -// FormatFromExtension parses image format from extension: -// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. -func FormatFromExtension(ext string) (Format, error) { - if f, ok := formatFromExt[strings.ToLower(strings.TrimPrefix(ext, "."))]; ok { - return f, nil - } - return -1, ErrUnsupportedFormat -} - -// FormatFromFilename parses image format from filename extension: -// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. -func FormatFromFilename(filename string) (Format, error) { - ext := filepath.Ext(filename) - return FormatFromExtension(ext) -} - -var ( - // ErrUnsupportedFormat means the given image format (or file extension) is unsupported. - ErrUnsupportedFormat = errors.New("imaging: unsupported image format") -) - -type fileSystem interface { - Create(string) (io.WriteCloser, error) - Open(string) (io.ReadCloser, error) -} - -type localFS struct{} - -func (localFS) Create(name string) (io.WriteCloser, error) { return os.Create(name) } -func (localFS) Open(name string) (io.ReadCloser, error) { return os.Open(name) } - -var fs fileSystem = localFS{} - -// Decode reads an image from r. -func Decode(r io.Reader) (image.Image, error) { - img, _, err := image.Decode(r) - return img, err -} - -// Open loads an image from file -func Open(filename string) (image.Image, error) { - file, err := fs.Open(filename) - if err != nil { - return nil, err - } - defer file.Close() - return Decode(file) -} - -type encodeConfig struct { - jpegQuality int - gifNumColors int - gifQuantizer draw.Quantizer - gifDrawer draw.Drawer - pngCompressionLevel png.CompressionLevel -} - -var defaultEncodeConfig = encodeConfig{ - jpegQuality: 95, - gifNumColors: 256, - gifQuantizer: nil, - gifDrawer: nil, - pngCompressionLevel: png.DefaultCompression, -} - -// EncodeOption sets an optional parameter for the Encode and Save functions. -type EncodeOption func(*encodeConfig) - -// JPEGQuality returns an EncodeOption that sets the output JPEG quality. -// Quality ranges from 1 to 100 inclusive, higher is better. Default is 95. -func JPEGQuality(quality int) EncodeOption { - return func(c *encodeConfig) { - c.jpegQuality = quality - } -} - -// GIFNumColors returns an EncodeOption that sets the maximum number of colors -// used in the GIF-encoded image. It ranges from 1 to 256. Default is 256. -func GIFNumColors(numColors int) EncodeOption { - return func(c *encodeConfig) { - c.gifNumColors = numColors - } -} - -// GIFQuantizer returns an EncodeOption that sets the quantizer that is used to produce -// a palette of the GIF-encoded image. -func GIFQuantizer(quantizer draw.Quantizer) EncodeOption { - return func(c *encodeConfig) { - c.gifQuantizer = quantizer - } -} - -// GIFDrawer returns an EncodeOption that sets the drawer that is used to convert -// the source image to the desired palette of the GIF-encoded image. -func GIFDrawer(drawer draw.Drawer) EncodeOption { - return func(c *encodeConfig) { - c.gifDrawer = drawer - } -} - -// PNGCompressionLevel returns an EncodeOption that sets the compression level -// of the PNG-encoded image. Default is png.DefaultCompression. -func PNGCompressionLevel(level png.CompressionLevel) EncodeOption { - return func(c *encodeConfig) { - c.pngCompressionLevel = level - } -} - -// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP). -func Encode(w io.Writer, img image.Image, format Format, opts ...EncodeOption) error { - cfg := defaultEncodeConfig - for _, option := range opts { - option(&cfg) - } - - var err error - switch format { - case JPEG: - var rgba *image.RGBA - if nrgba, ok := img.(*image.NRGBA); ok { - if nrgba.Opaque() { - rgba = &image.RGBA{ - Pix: nrgba.Pix, - Stride: nrgba.Stride, - Rect: nrgba.Rect, - } - } - } - if rgba != nil { - err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: cfg.jpegQuality}) - } else { - err = jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality}) - } - - case PNG: - enc := png.Encoder{CompressionLevel: cfg.pngCompressionLevel} - err = enc.Encode(w, img) - - case GIF: - err = gif.Encode(w, img, &gif.Options{ - NumColors: cfg.gifNumColors, - Quantizer: cfg.gifQuantizer, - Drawer: cfg.gifDrawer, - }) - - case TIFF: - err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true}) - - case BMP: - err = bmp.Encode(w, img) - - default: - err = ErrUnsupportedFormat - } - return err -} - -// Save saves the image to file with the specified filename. -// The format is determined from the filename extension: "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. -// -// Examples: -// -// // Save the image as PNG. -// err := imaging.Save(img, "out.png") -// -// // Save the image as JPEG with optional quality parameter set to 80. -// err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80)) -// -func Save(img image.Image, filename string, opts ...EncodeOption) (err error) { - f, err := FormatFromFilename(filename) - if err != nil { - return err - } - file, err := fs.Create(filename) - if err != nil { - return err - } - - defer func() { - cerr := file.Close() - if err == nil { - err = cerr - } - }() - - return Encode(file, img, f, opts...) -} - -// New creates a new image with the specified width and height, and fills it with the specified color. -func New(width, height int, fillColor color.Color) *image.NRGBA { - if width <= 0 || height <= 0 { - return &image.NRGBA{} - } - - c := color.NRGBAModel.Convert(fillColor).(color.NRGBA) - if (c == color.NRGBA{0, 0, 0, 0}) { - return image.NewNRGBA(image.Rect(0, 0, width, height)) - } - - return &image.NRGBA{ - Pix: bytes.Repeat([]byte{c.R, c.G, c.B, c.A}, width*height), - Stride: 4 * width, - Rect: image.Rect(0, 0, width, height), - } -} - -// Clone returns a copy of the given image. -func Clone(img image.Image) *image.NRGBA { - src := newScanner(img) - dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) - size := src.w * 4 - parallel(0, src.h, func(ys <-chan int) { - for y := range ys { - i := y * dst.Stride - src.scan(0, y, src.w, y+1, dst.Pix[i:i+size]) - } - }) - return dst -} diff --git a/helpers_test.go b/helpers_test.go deleted file mode 100644 index c8939d7..0000000 --- a/helpers_test.go +++ /dev/null @@ -1,547 +0,0 @@ -package imaging - -import ( - "bytes" - "errors" - "image" - "image/color" - "image/color/palette" - "image/draw" - "image/png" - "io" - "io/ioutil" - "os" - "path/filepath" - "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 TestNew(t *testing.T) { - testCases := []struct { - name string - w, h int - c color.Color - dstBounds image.Rectangle - dstPix []uint8 - }{ - { - "New 1x1 transparent", - 1, 1, - color.Transparent, - image.Rect(0, 0, 1, 1), - []uint8{0x00, 0x00, 0x00, 0x00}, - }, - { - "New 1x2 red", - 1, 2, - color.RGBA{255, 0, 0, 255}, - image.Rect(0, 0, 1, 2), - []uint8{0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff}, - }, - { - "New 2x1 white", - 2, 1, - color.White, - image.Rect(0, 0, 2, 1), - []uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, - }, - { - "New 3x3 with alpha", - 3, 3, - color.NRGBA{0x01, 0x23, 0x45, 0x67}, - image.Rect(0, 0, 3, 3), - []uint8{ - 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, - 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, - 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, - }, - }, - { - "New 0x0 white", - 0, 0, - color.White, - image.Rect(0, 0, 0, 0), - nil, - }, - { - "New 800x600 custom", - 800, 600, - color.NRGBA{1, 2, 3, 4}, - image.Rect(0, 0, 800, 600), - bytes.Repeat([]byte{1, 2, 3, 4}, 800*600), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := New(tc.w, tc.h, tc.c) - want := image.NewNRGBA(tc.dstBounds) - want.Pix = tc.dstPix - if !compareNRGBA(got, want, 0) { - t.Fatalf("got result %#v want %#v", got, want) - } - }) - } -} - -func BenchmarkNew(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - New(1024, 1024, color.White) - } -} - -func TestFormats(t *testing.T) { - formatNames := map[Format]string{ - JPEG: "JPEG", - PNG: "PNG", - GIF: "GIF", - BMP: "BMP", - TIFF: "TIFF", - Format(-1): "Unsupported", - } - for format, name := range formatNames { - got := format.String() - if got != name { - t.Fatalf("got format name %q want %q", got, name) - } - } -} - -func TestClone(t *testing.T) { - testCases := []struct { - name string - src image.Image - want *image.NRGBA - }{ - { - "Clone NRGBA", - &image.NRGBA{ - Rect: image.Rect(-1, -1, 0, 1), - Stride: 1 * 4, - Pix: []uint8{0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 1, 2), - Stride: 1 * 4, - Pix: []uint8{0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, - }, - }, - { - "Clone NRGBA64", - &image.NRGBA64{ - Rect: image.Rect(-1, -1, 0, 1), - Stride: 1 * 8, - Pix: []uint8{ - 0x00, 0x00, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, - 0xcc, 0xcc, 0xdd, 0xdd, 0xee, 0xee, 0xff, 0xff, - }, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 1, 2), - Stride: 1 * 4, - Pix: []uint8{0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, - }, - }, - { - "Clone RGBA", - &image.RGBA{ - Rect: image.Rect(-1, -1, 0, 2), - Stride: 1 * 4, - Pix: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 1, 3), - Stride: 1 * 4, - Pix: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0x33, 0xcc, 0xdd, 0xee, 0xff}, - }, - }, - { - "Clone RGBA64", - &image.RGBA64{ - Rect: image.Rect(-1, -1, 0, 2), - Stride: 1 * 8, - Pix: []uint8{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, - 0xcc, 0xcc, 0xdd, 0xdd, 0xee, 0xee, 0xff, 0xff, - }, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 1, 3), - Stride: 1 * 4, - Pix: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0x33, 0xcc, 0xdd, 0xee, 0xff}, - }, - }, - { - "Clone Gray", - &image.Gray{ - Rect: image.Rect(-1, -1, 0, 1), - Stride: 1 * 1, - Pix: []uint8{0x11, 0xee}, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 1, 2), - Stride: 1 * 4, - Pix: []uint8{0x11, 0x11, 0x11, 0xff, 0xee, 0xee, 0xee, 0xff}, - }, - }, - { - "Clone Gray16", - &image.Gray16{ - Rect: image.Rect(-1, -1, 0, 1), - Stride: 1 * 2, - Pix: []uint8{0x11, 0x11, 0xee, 0xee}, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 1, 2), - Stride: 1 * 4, - Pix: []uint8{0x11, 0x11, 0x11, 0xff, 0xee, 0xee, 0xee, 0xff}, - }, - }, - { - "Clone Alpha", - &image.Alpha{ - Rect: image.Rect(-1, -1, 0, 1), - Stride: 1 * 1, - Pix: []uint8{0x11, 0xee}, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 1, 2), - Stride: 1 * 4, - Pix: []uint8{0xff, 0xff, 0xff, 0x11, 0xff, 0xff, 0xff, 0xee}, - }, - }, - { - "Clone YCbCr", - &image.YCbCr{ - Rect: image.Rect(-1, -1, 5, 0), - SubsampleRatio: image.YCbCrSubsampleRatio444, - YStride: 6, - CStride: 6, - Y: []uint8{0x00, 0xff, 0x7f, 0x26, 0x4b, 0x0e}, - Cb: []uint8{0x80, 0x80, 0x80, 0x6b, 0x56, 0xc0}, - Cr: []uint8{0x80, 0x80, 0x80, 0xc0, 0x4b, 0x76}, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 6, 1), - Stride: 6 * 4, - Pix: []uint8{ - 0x00, 0x00, 0x00, 0xff, - 0xff, 0xff, 0xff, 0xff, - 0x7f, 0x7f, 0x7f, 0xff, - 0x7f, 0x00, 0x00, 0xff, - 0x00, 0x7f, 0x00, 0xff, - 0x00, 0x00, 0x7f, 0xff, - }, - }, - }, - { - "Clone YCbCr 444", - &image.YCbCr{ - Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, - Cb: []uint8{0x55, 0xd4, 0xff, 0x8e, 0x2c, 0x01, 0x6b, 0xaa, 0xc0, 0x95, 0x56, 0x40, 0x80, 0x80, 0x80, 0x80}, - Cr: []uint8{0xff, 0xeb, 0x6b, 0x36, 0x15, 0x95, 0xc0, 0xb5, 0x76, 0x41, 0x4b, 0x8c, 0x80, 0x80, 0x80, 0x80}, - YStride: 4, - CStride: 4, - SubsampleRatio: image.YCbCrSubsampleRatio444, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - &image.NRGBA{ - Pix: []uint8{0xff, 0x0, 0x0, 0xff, 0xff, 0x0, 0xff, 0xff, 0x0, 0x0, 0xff, 0xff, 0x49, 0xe1, 0xca, 0xff, 0x0, 0xff, 0x0, 0xff, 0xff, 0xff, 0x0, 0xff, 0x7f, 0x0, 0x0, 0xff, 0x7f, 0x0, 0x7f, 0xff, 0x0, 0x0, 0x7f, 0xff, 0x0, 0x7f, 0x7f, 0xff, 0x0, 0x7f, 0x0, 0xff, 0x82, 0x7f, 0x0, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, - Stride: 16, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - }, - { - "Clone YCbCr 440", - &image.YCbCr{ - Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, - Cb: []uint8{0x2c, 0x01, 0x6b, 0xaa, 0x80, 0x80, 0x80, 0x80}, - Cr: []uint8{0x15, 0x95, 0xc0, 0xb5, 0x80, 0x80, 0x80, 0x80}, - YStride: 4, - CStride: 4, - SubsampleRatio: image.YCbCrSubsampleRatio440, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - &image.NRGBA{ - Pix: []uint8{0x0, 0xb5, 0x0, 0xff, 0x86, 0x86, 0x0, 0xff, 0x77, 0x0, 0x0, 0xff, 0xfb, 0x7d, 0xfb, 0xff, 0x0, 0xff, 0x1, 0xff, 0xff, 0xff, 0x1, 0xff, 0x80, 0x0, 0x1, 0xff, 0x7e, 0x0, 0x7e, 0xff, 0xe, 0xe, 0xe, 0xff, 0x59, 0x59, 0x59, 0xff, 0x4b, 0x4b, 0x4b, 0xff, 0x71, 0x71, 0x71, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, - Stride: 16, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - }, - { - "Clone YCbCr 422", - &image.YCbCr{ - Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, - Cb: []uint8{0xd4, 0x8e, 0x01, 0xaa, 0x95, 0x40, 0x80, 0x80}, - Cr: []uint8{0xeb, 0x36, 0x95, 0xb5, 0x41, 0x8c, 0x80, 0x80}, - YStride: 4, - CStride: 2, - SubsampleRatio: image.YCbCrSubsampleRatio422, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - &image.NRGBA{ - Pix: []uint8{0xe2, 0x0, 0xe1, 0xff, 0xff, 0x0, 0xfe, 0xff, 0x0, 0x4d, 0x36, 0xff, 0x49, 0xe1, 0xca, 0xff, 0xb3, 0xb3, 0x0, 0xff, 0xff, 0xff, 0x1, 0xff, 0x70, 0x0, 0x70, 0xff, 0x7e, 0x0, 0x7e, 0xff, 0x0, 0x34, 0x33, 0xff, 0x1, 0x7f, 0x7e, 0xff, 0x5c, 0x58, 0x0, 0xff, 0x82, 0x7e, 0x0, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, - Stride: 16, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - }, - { - "Clone YCbCr 420", - &image.YCbCr{ - Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, - Cb: []uint8{0x01, 0xaa, 0x80, 0x80}, - Cr: []uint8{0x95, 0xb5, 0x80, 0x80}, - YStride: 4, CStride: 2, - SubsampleRatio: image.YCbCrSubsampleRatio420, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - &image.NRGBA{ - Pix: []uint8{0x69, 0x69, 0x0, 0xff, 0x86, 0x86, 0x0, 0xff, 0x67, 0x0, 0x67, 0xff, 0xfb, 0x7d, 0xfb, 0xff, 0xb3, 0xb3, 0x0, 0xff, 0xff, 0xff, 0x1, 0xff, 0x70, 0x0, 0x70, 0xff, 0x7e, 0x0, 0x7e, 0xff, 0xe, 0xe, 0xe, 0xff, 0x59, 0x59, 0x59, 0xff, 0x4b, 0x4b, 0x4b, 0xff, 0x71, 0x71, 0x71, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, - Stride: 16, - Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, - }, - }, - { - "Clone Paletted", - &image.Paletted{ - Rect: image.Rect(-1, -1, 5, 0), - Stride: 6 * 1, - Palette: color.Palette{ - color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, - color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, - color.NRGBA{R: 0x7f, G: 0x7f, B: 0x7f, A: 0xff}, - color.NRGBA{R: 0x7f, G: 0x00, B: 0x00, A: 0xff}, - color.NRGBA{R: 0x00, G: 0x7f, B: 0x00, A: 0xff}, - color.NRGBA{R: 0x00, G: 0x00, B: 0x7f, A: 0xff}, - }, - Pix: []uint8{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}, - }, - &image.NRGBA{ - Rect: image.Rect(0, 0, 6, 1), - Stride: 6 * 4, - Pix: []uint8{ - 0x00, 0x00, 0x00, 0xff, - 0xff, 0xff, 0xff, 0xff, - 0x7f, 0x7f, 0x7f, 0xff, - 0x7f, 0x00, 0x00, 0xff, - 0x00, 0x7f, 0x00, 0xff, - 0x00, 0x00, 0x7f, 0xff, - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := Clone(tc.src) - delta := 0 - if _, ok := tc.src.(*image.YCbCr); ok { - delta = 1 - } - if !compareNRGBA(got, tc.want, delta) { - t.Fatalf("got result %#v want %#v", got, tc.want) - } - }) - } -} - -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) - } - }) - } -} diff --git a/io.go b/io.go new file mode 100644 index 0000000..557bf2f --- /dev/null +++ b/io.go @@ -0,0 +1,463 @@ +package imaging + +import ( + "encoding/binary" + "errors" + "image" + "image/draw" + "image/gif" + "image/jpeg" + "image/png" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "golang.org/x/image/bmp" + "golang.org/x/image/tiff" +) + +// Format is an image file format. +type Format int + +// Image file formats. +const ( + JPEG Format = iota + PNG + GIF + TIFF + BMP +) + +func (f Format) String() string { + switch f { + case JPEG: + return "JPEG" + case PNG: + return "PNG" + case GIF: + return "GIF" + case TIFF: + return "TIFF" + case BMP: + return "BMP" + default: + return "Unsupported" + } +} + +var formatFromExt = map[string]Format{ + "jpg": JPEG, + "jpeg": JPEG, + "png": PNG, + "tif": TIFF, + "tiff": TIFF, + "bmp": BMP, + "gif": GIF, +} + +// FormatFromExtension parses image format from extension: +// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. +func FormatFromExtension(ext string) (Format, error) { + if f, ok := formatFromExt[strings.ToLower(strings.TrimPrefix(ext, "."))]; ok { + return f, nil + } + return -1, ErrUnsupportedFormat +} + +// FormatFromFilename parses image format from filename extension: +// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. +func FormatFromFilename(filename string) (Format, error) { + ext := filepath.Ext(filename) + return FormatFromExtension(ext) +} + +var ( + // ErrUnsupportedFormat means the given image format (or file extension) is unsupported. + ErrUnsupportedFormat = errors.New("imaging: unsupported image format") +) + +type fileSystem interface { + Create(string) (io.WriteCloser, error) + Open(string) (io.ReadCloser, error) +} + +type localFS struct{} + +func (localFS) Create(name string) (io.WriteCloser, error) { return os.Create(name) } +func (localFS) Open(name string) (io.ReadCloser, error) { return os.Open(name) } + +var fs fileSystem = localFS{} + +type decodeConfig struct { + autoOrientation bool +} + +var defaultDecodeConfig = decodeConfig{ + autoOrientation: false, +} + +// DecodeOption sets an optional parameter for the Decode and Open functions. +type DecodeOption func(*decodeConfig) + +// AutoOrientation returns a DecodeOption that sets the auto-orientation mode. +// If auto-orientation is enabled, the image will be transformed after decoding +// according to the EXIF orientation tag (if present). By default it's disabled. +func AutoOrientation(enabled bool) DecodeOption { + return func(c *decodeConfig) { + c.autoOrientation = enabled + } +} + +// Decode reads an image from r. +func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) { + cfg := defaultDecodeConfig + for _, option := range opts { + option(&cfg) + } + + if !cfg.autoOrientation { + img, _, err := image.Decode(r) + return img, err + } + + var orient orientation + pr, pw := io.Pipe() + r = io.TeeReader(r, pw) + done := make(chan struct{}) + go func() { + defer close(done) + orient = readOrientation(pr) + io.Copy(ioutil.Discard, pr) + }() + + img, _, err := image.Decode(r) + pw.Close() + <-done + if err != nil { + return nil, err + } + + return fixOrientation(img, orient), nil +} + +// Open loads an image from file. +// +// Examples: +// +// // Load an image from file. +// img, err := imaging.Open("test.jpg") +// +// // Load an image and transform it depending on the EXIF orientation tag (if present). +// img, err := imaging.Open("test.jpg", imaging.AutoOrientation(true)) +// +func Open(filename string, opts ...DecodeOption) (image.Image, error) { + file, err := fs.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + return Decode(file, opts...) +} + +type encodeConfig struct { + jpegQuality int + gifNumColors int + gifQuantizer draw.Quantizer + gifDrawer draw.Drawer + pngCompressionLevel png.CompressionLevel +} + +var defaultEncodeConfig = encodeConfig{ + jpegQuality: 95, + gifNumColors: 256, + gifQuantizer: nil, + gifDrawer: nil, + pngCompressionLevel: png.DefaultCompression, +} + +// EncodeOption sets an optional parameter for the Encode and Save functions. +type EncodeOption func(*encodeConfig) + +// JPEGQuality returns an EncodeOption that sets the output JPEG quality. +// Quality ranges from 1 to 100 inclusive, higher is better. Default is 95. +func JPEGQuality(quality int) EncodeOption { + return func(c *encodeConfig) { + c.jpegQuality = quality + } +} + +// GIFNumColors returns an EncodeOption that sets the maximum number of colors +// used in the GIF-encoded image. It ranges from 1 to 256. Default is 256. +func GIFNumColors(numColors int) EncodeOption { + return func(c *encodeConfig) { + c.gifNumColors = numColors + } +} + +// GIFQuantizer returns an EncodeOption that sets the quantizer that is used to produce +// a palette of the GIF-encoded image. +func GIFQuantizer(quantizer draw.Quantizer) EncodeOption { + return func(c *encodeConfig) { + c.gifQuantizer = quantizer + } +} + +// GIFDrawer returns an EncodeOption that sets the drawer that is used to convert +// the source image to the desired palette of the GIF-encoded image. +func GIFDrawer(drawer draw.Drawer) EncodeOption { + return func(c *encodeConfig) { + c.gifDrawer = drawer + } +} + +// PNGCompressionLevel returns an EncodeOption that sets the compression level +// of the PNG-encoded image. Default is png.DefaultCompression. +func PNGCompressionLevel(level png.CompressionLevel) EncodeOption { + return func(c *encodeConfig) { + c.pngCompressionLevel = level + } +} + +// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP). +func Encode(w io.Writer, img image.Image, format Format, opts ...EncodeOption) error { + cfg := defaultEncodeConfig + for _, option := range opts { + option(&cfg) + } + + var err error + switch format { + case JPEG: + var rgba *image.RGBA + if nrgba, ok := img.(*image.NRGBA); ok { + if nrgba.Opaque() { + rgba = &image.RGBA{ + Pix: nrgba.Pix, + Stride: nrgba.Stride, + Rect: nrgba.Rect, + } + } + } + if rgba != nil { + err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: cfg.jpegQuality}) + } else { + err = jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality}) + } + + case PNG: + enc := png.Encoder{CompressionLevel: cfg.pngCompressionLevel} + err = enc.Encode(w, img) + + case GIF: + err = gif.Encode(w, img, &gif.Options{ + NumColors: cfg.gifNumColors, + Quantizer: cfg.gifQuantizer, + Drawer: cfg.gifDrawer, + }) + + case TIFF: + err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true}) + + case BMP: + err = bmp.Encode(w, img) + + default: + err = ErrUnsupportedFormat + } + return err +} + +// Save saves the image to file with the specified filename. +// The format is determined from the filename extension: +// "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. +// +// Examples: +// +// // Save the image as PNG. +// err := imaging.Save(img, "out.png") +// +// // Save the image as JPEG with optional quality parameter set to 80. +// err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80)) +// +func Save(img image.Image, filename string, opts ...EncodeOption) (err error) { + f, err := FormatFromFilename(filename) + if err != nil { + return err + } + file, err := fs.Create(filename) + if err != nil { + return err + } + + defer func() { + cerr := file.Close() + if err == nil { + err = cerr + } + }() + + return Encode(file, img, f, opts...) +} + +// orientation is an EXIF flag that specifies the transformation +// that should be applied to image to display it correctly. +type orientation int + +const ( + orientationUnspecified = 0 + orientationNormal = 1 + orientationFlipH = 2 + orientationRotate180 = 3 + orientationFlipV = 4 + orientationTranspose = 5 + orientationRotate270 = 6 + orientationTransverse = 7 + orientationRotate90 = 8 +) + +// readOrientation tries to read the orientation EXIF flag from image data in r. +// If the EXIF data block is not found or the orientation flag is not found +// or any other error occures while reading the data, it returns the +// orientationUnspecified (0) value. +func readOrientation(r io.Reader) orientation { + const ( + markerSOI = 0xffd8 + markerAPP1 = 0xffe1 + exifHeader = 0x45786966 + byteOrderBE = 0x4d4d + byteOrderLE = 0x4949 + orientationTag = 0x0112 + ) + + // Check if JPEG SOI marker is present. + var soi uint16 + if err := binary.Read(r, binary.BigEndian, &soi); err != nil { + return orientationUnspecified + } + if soi != markerSOI { + return orientationUnspecified // Missing JPEG SOI marker. + } + + // Find JPEG APP1 marker. + for { + var marker, size uint16 + if err := binary.Read(r, binary.BigEndian, &marker); err != nil { + return orientationUnspecified + } + if err := binary.Read(r, binary.BigEndian, &size); err != nil { + return orientationUnspecified + } + if marker>>8 != 0xff { + return orientationUnspecified // Invalid JPEG marker. + } + if marker == markerAPP1 { + break + } + if size < 2 { + return orientationUnspecified // Invalid block size. + } + if _, err := io.CopyN(ioutil.Discard, r, int64(size-2)); err != nil { + return orientationUnspecified + } + } + + // Check if EXIF header is present. + var header uint32 + if err := binary.Read(r, binary.BigEndian, &header); err != nil { + return orientationUnspecified + } + if header != exifHeader { + return orientationUnspecified + } + if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { + return orientationUnspecified + } + + // Read byte order information. + var ( + byteOrderTag uint16 + byteOrder binary.ByteOrder + ) + if err := binary.Read(r, binary.BigEndian, &byteOrderTag); err != nil { + return orientationUnspecified + } + switch byteOrderTag { + case byteOrderBE: + byteOrder = binary.BigEndian + case byteOrderLE: + byteOrder = binary.LittleEndian + default: + return orientationUnspecified // Invalid byte order flag. + } + if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { + return orientationUnspecified + } + + // Skip the EXIF offset. + var offset uint32 + if err := binary.Read(r, byteOrder, &offset); err != nil { + return orientationUnspecified + } + if offset < 8 { + return orientationUnspecified // Invalid offset value. + } + if _, err := io.CopyN(ioutil.Discard, r, int64(offset-8)); err != nil { + return orientationUnspecified + } + + // Read the number of tags. + var numTags uint16 + if err := binary.Read(r, byteOrder, &numTags); err != nil { + return orientationUnspecified + } + + // Find the orientation tag. + for i := 0; i < int(numTags); i++ { + var tag uint16 + if err := binary.Read(r, byteOrder, &tag); err != nil { + return orientationUnspecified + } + if tag != orientationTag { + if _, err := io.CopyN(ioutil.Discard, r, 10); err != nil { + return orientationUnspecified + } + continue + } + if _, err := io.CopyN(ioutil.Discard, r, 6); err != nil { + return orientationUnspecified + } + var val uint16 + if err := binary.Read(r, byteOrder, &val); err != nil { + return orientationUnspecified + } + if val < 1 || val > 8 { + return orientationUnspecified // Invalid tag value. + } + return orientation(val) + } + return orientationUnspecified // Missing orientation tag. +} + +// fixOrientation applies a transform to img corresponding to the given orientation flag. +func fixOrientation(img image.Image, o orientation) image.Image { + switch o { + case orientationNormal: + case orientationFlipH: + img = FlipH(img) + case orientationFlipV: + img = FlipV(img) + case orientationRotate90: + img = Rotate90(img) + case orientationRotate180: + img = Rotate180(img) + case orientationRotate270: + img = Rotate270(img) + case orientationTranspose: + img = Transpose(img) + case orientationTransverse: + img = Transverse(img) + } + return img +} diff --git a/io_test.go b/io_test.go new file mode 100644 index 0000000..e7b7087 --- /dev/null +++ b/io_test.go @@ -0,0 +1,435 @@ +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): "Unsupported", + } + 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") + } +} diff --git a/testdata/orientation_0.jpg b/testdata/orientation_0.jpg new file mode 100644 index 0000000..441da0f Binary files /dev/null and b/testdata/orientation_0.jpg differ diff --git a/testdata/orientation_1.jpg b/testdata/orientation_1.jpg new file mode 100644 index 0000000..519d60f Binary files /dev/null and b/testdata/orientation_1.jpg differ diff --git a/testdata/orientation_2.jpg b/testdata/orientation_2.jpg new file mode 100644 index 0000000..2ef6fa7 Binary files /dev/null and b/testdata/orientation_2.jpg differ diff --git a/testdata/orientation_3.jpg b/testdata/orientation_3.jpg new file mode 100644 index 0000000..cf3fe02 Binary files /dev/null and b/testdata/orientation_3.jpg differ diff --git a/testdata/orientation_4.jpg b/testdata/orientation_4.jpg new file mode 100644 index 0000000..4e2c97a Binary files /dev/null and b/testdata/orientation_4.jpg differ diff --git a/testdata/orientation_5.jpg b/testdata/orientation_5.jpg new file mode 100644 index 0000000..6c7352b Binary files /dev/null and b/testdata/orientation_5.jpg differ diff --git a/testdata/orientation_6.jpg b/testdata/orientation_6.jpg new file mode 100644 index 0000000..67016fe Binary files /dev/null and b/testdata/orientation_6.jpg differ diff --git a/testdata/orientation_7.jpg b/testdata/orientation_7.jpg new file mode 100644 index 0000000..3100112 Binary files /dev/null and b/testdata/orientation_7.jpg differ diff --git a/testdata/orientation_8.jpg b/testdata/orientation_8.jpg new file mode 100644 index 0000000..ec6e3db Binary files /dev/null and b/testdata/orientation_8.jpg differ diff --git a/tools.go b/tools.go index fae1fa1..7887946 100644 --- a/tools.go +++ b/tools.go @@ -1,10 +1,44 @@ package imaging import ( + "bytes" "image" + "image/color" "math" ) +// New creates a new image with the specified width and height, and fills it with the specified color. +func New(width, height int, fillColor color.Color) *image.NRGBA { + if width <= 0 || height <= 0 { + return &image.NRGBA{} + } + + c := color.NRGBAModel.Convert(fillColor).(color.NRGBA) + if (c == color.NRGBA{0, 0, 0, 0}) { + return image.NewNRGBA(image.Rect(0, 0, width, height)) + } + + return &image.NRGBA{ + Pix: bytes.Repeat([]byte{c.R, c.G, c.B, c.A}, width*height), + Stride: 4 * width, + Rect: image.Rect(0, 0, width, height), + } +} + +// Clone returns a copy of the given image. +func Clone(img image.Image) *image.NRGBA { + src := newScanner(img) + dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) + size := src.w * 4 + parallel(0, src.h, func(ys <-chan int) { + for y := range ys { + i := y * dst.Stride + src.scan(0, y, src.w, y+1, dst.Pix[i:i+size]) + } + }) + return dst +} + // Anchor is the anchor point for image alignment. type Anchor int diff --git a/tools_test.go b/tools_test.go index 3cc25fd..a714f6f 100644 --- a/tools_test.go +++ b/tools_test.go @@ -1,10 +1,326 @@ package imaging import ( + "bytes" "image" + "image/color" "testing" ) +func TestNew(t *testing.T) { + testCases := []struct { + name string + w, h int + c color.Color + dstBounds image.Rectangle + dstPix []uint8 + }{ + { + "New 1x1 transparent", + 1, 1, + color.Transparent, + image.Rect(0, 0, 1, 1), + []uint8{0x00, 0x00, 0x00, 0x00}, + }, + { + "New 1x2 red", + 1, 2, + color.RGBA{255, 0, 0, 255}, + image.Rect(0, 0, 1, 2), + []uint8{0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff}, + }, + { + "New 2x1 white", + 2, 1, + color.White, + image.Rect(0, 0, 2, 1), + []uint8{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + { + "New 3x3 with alpha", + 3, 3, + color.NRGBA{0x01, 0x23, 0x45, 0x67}, + image.Rect(0, 0, 3, 3), + []uint8{ + 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, + 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, + 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, 0x01, 0x23, 0x45, 0x67, + }, + }, + { + "New 0x0 white", + 0, 0, + color.White, + image.Rect(0, 0, 0, 0), + nil, + }, + { + "New 800x600 custom", + 800, 600, + color.NRGBA{1, 2, 3, 4}, + image.Rect(0, 0, 800, 600), + bytes.Repeat([]byte{1, 2, 3, 4}, 800*600), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := New(tc.w, tc.h, tc.c) + want := image.NewNRGBA(tc.dstBounds) + want.Pix = tc.dstPix + if !compareNRGBA(got, want, 0) { + t.Fatalf("got result %#v want %#v", got, want) + } + }) + } +} + +func BenchmarkNew(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + New(1024, 1024, color.White) + } +} + +func TestClone(t *testing.T) { + testCases := []struct { + name string + src image.Image + want *image.NRGBA + }{ + { + "Clone NRGBA", + &image.NRGBA{ + Rect: image.Rect(-1, -1, 0, 1), + Stride: 1 * 4, + Pix: []uint8{0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 1, 2), + Stride: 1 * 4, + Pix: []uint8{0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, + }, + }, + { + "Clone NRGBA64", + &image.NRGBA64{ + Rect: image.Rect(-1, -1, 0, 1), + Stride: 1 * 8, + Pix: []uint8{ + 0x00, 0x00, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, + 0xcc, 0xcc, 0xdd, 0xdd, 0xee, 0xee, 0xff, 0xff, + }, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 1, 2), + Stride: 1 * 4, + Pix: []uint8{0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, + }, + }, + { + "Clone RGBA", + &image.RGBA{ + Rect: image.Rect(-1, -1, 0, 2), + Stride: 1 * 4, + Pix: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff}, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 1, 3), + Stride: 1 * 4, + Pix: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0x33, 0xcc, 0xdd, 0xee, 0xff}, + }, + }, + { + "Clone RGBA64", + &image.RGBA64{ + Rect: image.Rect(-1, -1, 0, 2), + Stride: 1 * 8, + Pix: []uint8{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x11, 0x11, 0x22, 0x22, 0x33, 0x33, + 0xcc, 0xcc, 0xdd, 0xdd, 0xee, 0xee, 0xff, 0xff, + }, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 1, 3), + Stride: 1 * 4, + Pix: []uint8{0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xaa, 0x33, 0xcc, 0xdd, 0xee, 0xff}, + }, + }, + { + "Clone Gray", + &image.Gray{ + Rect: image.Rect(-1, -1, 0, 1), + Stride: 1 * 1, + Pix: []uint8{0x11, 0xee}, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 1, 2), + Stride: 1 * 4, + Pix: []uint8{0x11, 0x11, 0x11, 0xff, 0xee, 0xee, 0xee, 0xff}, + }, + }, + { + "Clone Gray16", + &image.Gray16{ + Rect: image.Rect(-1, -1, 0, 1), + Stride: 1 * 2, + Pix: []uint8{0x11, 0x11, 0xee, 0xee}, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 1, 2), + Stride: 1 * 4, + Pix: []uint8{0x11, 0x11, 0x11, 0xff, 0xee, 0xee, 0xee, 0xff}, + }, + }, + { + "Clone Alpha", + &image.Alpha{ + Rect: image.Rect(-1, -1, 0, 1), + Stride: 1 * 1, + Pix: []uint8{0x11, 0xee}, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 1, 2), + Stride: 1 * 4, + Pix: []uint8{0xff, 0xff, 0xff, 0x11, 0xff, 0xff, 0xff, 0xee}, + }, + }, + { + "Clone YCbCr", + &image.YCbCr{ + Rect: image.Rect(-1, -1, 5, 0), + SubsampleRatio: image.YCbCrSubsampleRatio444, + YStride: 6, + CStride: 6, + Y: []uint8{0x00, 0xff, 0x7f, 0x26, 0x4b, 0x0e}, + Cb: []uint8{0x80, 0x80, 0x80, 0x6b, 0x56, 0xc0}, + Cr: []uint8{0x80, 0x80, 0x80, 0xc0, 0x4b, 0x76}, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 6, 1), + Stride: 6 * 4, + Pix: []uint8{ + 0x00, 0x00, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0x7f, 0x7f, 0x7f, 0xff, + 0x7f, 0x00, 0x00, 0xff, + 0x00, 0x7f, 0x00, 0xff, + 0x00, 0x00, 0x7f, 0xff, + }, + }, + }, + { + "Clone YCbCr 444", + &image.YCbCr{ + Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, + Cb: []uint8{0x55, 0xd4, 0xff, 0x8e, 0x2c, 0x01, 0x6b, 0xaa, 0xc0, 0x95, 0x56, 0x40, 0x80, 0x80, 0x80, 0x80}, + Cr: []uint8{0xff, 0xeb, 0x6b, 0x36, 0x15, 0x95, 0xc0, 0xb5, 0x76, 0x41, 0x4b, 0x8c, 0x80, 0x80, 0x80, 0x80}, + YStride: 4, + CStride: 4, + SubsampleRatio: image.YCbCrSubsampleRatio444, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + &image.NRGBA{ + Pix: []uint8{0xff, 0x0, 0x0, 0xff, 0xff, 0x0, 0xff, 0xff, 0x0, 0x0, 0xff, 0xff, 0x49, 0xe1, 0xca, 0xff, 0x0, 0xff, 0x0, 0xff, 0xff, 0xff, 0x0, 0xff, 0x7f, 0x0, 0x0, 0xff, 0x7f, 0x0, 0x7f, 0xff, 0x0, 0x0, 0x7f, 0xff, 0x0, 0x7f, 0x7f, 0xff, 0x0, 0x7f, 0x0, 0xff, 0x82, 0x7f, 0x0, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, + Stride: 16, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + }, + { + "Clone YCbCr 440", + &image.YCbCr{ + Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, + Cb: []uint8{0x2c, 0x01, 0x6b, 0xaa, 0x80, 0x80, 0x80, 0x80}, + Cr: []uint8{0x15, 0x95, 0xc0, 0xb5, 0x80, 0x80, 0x80, 0x80}, + YStride: 4, + CStride: 4, + SubsampleRatio: image.YCbCrSubsampleRatio440, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + &image.NRGBA{ + Pix: []uint8{0x0, 0xb5, 0x0, 0xff, 0x86, 0x86, 0x0, 0xff, 0x77, 0x0, 0x0, 0xff, 0xfb, 0x7d, 0xfb, 0xff, 0x0, 0xff, 0x1, 0xff, 0xff, 0xff, 0x1, 0xff, 0x80, 0x0, 0x1, 0xff, 0x7e, 0x0, 0x7e, 0xff, 0xe, 0xe, 0xe, 0xff, 0x59, 0x59, 0x59, 0xff, 0x4b, 0x4b, 0x4b, 0xff, 0x71, 0x71, 0x71, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, + Stride: 16, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + }, + { + "Clone YCbCr 422", + &image.YCbCr{ + Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, + Cb: []uint8{0xd4, 0x8e, 0x01, 0xaa, 0x95, 0x40, 0x80, 0x80}, + Cr: []uint8{0xeb, 0x36, 0x95, 0xb5, 0x41, 0x8c, 0x80, 0x80}, + YStride: 4, + CStride: 2, + SubsampleRatio: image.YCbCrSubsampleRatio422, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + &image.NRGBA{ + Pix: []uint8{0xe2, 0x0, 0xe1, 0xff, 0xff, 0x0, 0xfe, 0xff, 0x0, 0x4d, 0x36, 0xff, 0x49, 0xe1, 0xca, 0xff, 0xb3, 0xb3, 0x0, 0xff, 0xff, 0xff, 0x1, 0xff, 0x70, 0x0, 0x70, 0xff, 0x7e, 0x0, 0x7e, 0xff, 0x0, 0x34, 0x33, 0xff, 0x1, 0x7f, 0x7e, 0xff, 0x5c, 0x58, 0x0, 0xff, 0x82, 0x7e, 0x0, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, + Stride: 16, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + }, + { + "Clone YCbCr 420", + &image.YCbCr{ + Y: []uint8{0x4c, 0x69, 0x1d, 0xb1, 0x96, 0xe2, 0x26, 0x34, 0xe, 0x59, 0x4b, 0x71, 0x0, 0x4c, 0x99, 0xff}, + Cb: []uint8{0x01, 0xaa, 0x80, 0x80}, + Cr: []uint8{0x95, 0xb5, 0x80, 0x80}, + YStride: 4, CStride: 2, + SubsampleRatio: image.YCbCrSubsampleRatio420, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + &image.NRGBA{ + Pix: []uint8{0x69, 0x69, 0x0, 0xff, 0x86, 0x86, 0x0, 0xff, 0x67, 0x0, 0x67, 0xff, 0xfb, 0x7d, 0xfb, 0xff, 0xb3, 0xb3, 0x0, 0xff, 0xff, 0xff, 0x1, 0xff, 0x70, 0x0, 0x70, 0xff, 0x7e, 0x0, 0x7e, 0xff, 0xe, 0xe, 0xe, 0xff, 0x59, 0x59, 0x59, 0xff, 0x4b, 0x4b, 0x4b, 0xff, 0x71, 0x71, 0x71, 0xff, 0x0, 0x0, 0x0, 0xff, 0x4c, 0x4c, 0x4c, 0xff, 0x99, 0x99, 0x99, 0xff, 0xff, 0xff, 0xff, 0xff}, + Stride: 16, + Rect: image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 4, Y: 4}}, + }, + }, + { + "Clone Paletted", + &image.Paletted{ + Rect: image.Rect(-1, -1, 5, 0), + Stride: 6 * 1, + Palette: color.Palette{ + color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, + color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + color.NRGBA{R: 0x7f, G: 0x7f, B: 0x7f, A: 0xff}, + color.NRGBA{R: 0x7f, G: 0x00, B: 0x00, A: 0xff}, + color.NRGBA{R: 0x00, G: 0x7f, B: 0x00, A: 0xff}, + color.NRGBA{R: 0x00, G: 0x00, B: 0x7f, A: 0xff}, + }, + Pix: []uint8{0x0, 0x1, 0x2, 0x3, 0x4, 0x5}, + }, + &image.NRGBA{ + Rect: image.Rect(0, 0, 6, 1), + Stride: 6 * 4, + Pix: []uint8{ + 0x00, 0x00, 0x00, 0xff, + 0xff, 0xff, 0xff, 0xff, + 0x7f, 0x7f, 0x7f, 0xff, + 0x7f, 0x00, 0x00, 0xff, + 0x00, 0x7f, 0x00, 0xff, + 0x00, 0x00, 0x7f, 0xff, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := Clone(tc.src) + delta := 0 + if _, ok := tc.src.(*image.YCbCr); ok { + delta = 1 + } + if !compareNRGBA(got, tc.want, delta) { + t.Fatalf("got result %#v want %#v", got, tc.want) + } + }) + } +} + func TestCrop(t *testing.T) { testCases := []struct { name string