Jelajahi Sumber

io: add decode option: AutoOrientation

Grigory Dryapak 7 tahun lalu
induk
melakukan
0bd5694c78
15 mengubah file dengan 1248 tambahan dan 825 penghapusan
  1. 0 278
      helpers.go
  2. 0 547
      helpers_test.go
  3. 463 0
      io.go
  4. 435 0
      io_test.go
  5. TEMPAT SAMPAH
      testdata/orientation_0.jpg
  6. TEMPAT SAMPAH
      testdata/orientation_1.jpg
  7. TEMPAT SAMPAH
      testdata/orientation_2.jpg
  8. TEMPAT SAMPAH
      testdata/orientation_3.jpg
  9. TEMPAT SAMPAH
      testdata/orientation_4.jpg
  10. TEMPAT SAMPAH
      testdata/orientation_5.jpg
  11. TEMPAT SAMPAH
      testdata/orientation_6.jpg
  12. TEMPAT SAMPAH
      testdata/orientation_7.jpg
  13. TEMPAT SAMPAH
      testdata/orientation_8.jpg
  14. 34 0
      tools.go
  15. 316 0
      tools_test.go

+ 0 - 278
helpers.go

@@ -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
-}

+ 0 - 547
helpers_test.go

@@ -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)
-			}
-		})
-	}
-}

+ 463 - 0
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
+}

+ 435 - 0
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")
+	}
+}

TEMPAT SAMPAH
testdata/orientation_0.jpg


TEMPAT SAMPAH
testdata/orientation_1.jpg


TEMPAT SAMPAH
testdata/orientation_2.jpg


TEMPAT SAMPAH
testdata/orientation_3.jpg


TEMPAT SAMPAH
testdata/orientation_4.jpg


TEMPAT SAMPAH
testdata/orientation_5.jpg


TEMPAT SAMPAH
testdata/orientation_6.jpg


TEMPAT SAMPAH
testdata/orientation_7.jpg


TEMPAT SAMPAH
testdata/orientation_8.jpg


+ 34 - 0
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
 

+ 316 - 0
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