Browse Source

image processing refactoring

Grigory Dryapak 7 years ago
parent
commit
416a21a28a
30 changed files with 2149 additions and 1541 deletions
  1. 1 1
      LICENSE
  2. 8 11
      README.md
  3. 139 117
      adjust.go
  4. 131 54
      adjust_test.go
  5. 0 312
      clone.go
  6. 0 247
      clone_test.go
  7. 2 4
      convolution.go
  8. 85 12
      convolution_test.go
  9. 6 1
      doc.go
  10. 82 92
      effects.go
  11. 22 20
      effects_test.go
  12. 2 2
      example_test.go
  13. 51 11
      helpers.go
  14. 353 53
      helpers_test.go
  15. 27 19
      histogram.go
  16. 41 31
      histogram_test.go
  17. 102 122
      resize.go
  18. 94 52
      resize_test.go
  19. 250 0
      scanner.go
  20. 229 0
      scanner_test.go
  21. BIN
      testdata/lena_128.png
  22. BIN
      testdata/lena_512.png
  23. BIN
      testdata/out_contrast_m10.png
  24. BIN
      testdata/out_contrast_p10.png
  25. 72 61
      tools.go
  26. 191 56
      tools_test.go
  27. 72 145
      transform.go
  28. 71 63
      transform_test.go
  29. 50 38
      utils.go
  30. 68 17
      utils_test.go

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2012-2017 Grigory Dryapak
+Copyright (c) 2012-2018 Grigory Dryapak
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

+ 8 - 11
README.md

@@ -4,16 +4,13 @@
 [![Build Status](https://travis-ci.org/disintegration/imaging.svg?branch=master)](https://travis-ci.org/disintegration/imaging)
 [![Coverage Status](https://coveralls.io/repos/github/disintegration/imaging/badge.svg?branch=master)](https://coveralls.io/github/disintegration/imaging?branch=master)
 
-Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.). 
+Package imaging provides basic imaging processing functions (resize, rotate, crop, brightness/contrast adjustments, etc.).
 
-Image manipulation functions provided by the package take any image type 
-that implements `image.Image` interface as an input, and return a new image of 
-`*image.NRGBA` type (32bit RGBA colors, not premultiplied by alpha).
+All the image processing functions provided by the package accept any image type that implements `image.Image` interface
+as an input, and return a new image of `*image.NRGBA` type (32bit RGBA colors, not premultiplied by alpha).
 
 ## Installation
 
-Imaging requires Go version 1.2 or greater.
-
     go get -u github.com/disintegration/imaging
     
 ## Documentation
@@ -45,15 +42,15 @@ Imaging supports image resizing using various resampling filters. The most notab
 - `Box` - Simple and fast averaging filter appropriate for downscaling. When upscaling it's similar to NearestNeighbor.
 - `Linear` - Bilinear filter, smooth and reasonably fast.
 - `MitchellNetravali` - А smooth bicubic filter.
-- `CatmullRom` - A sharp bicubic filter. 
+- `CatmullRom` - A sharp bicubic filter.
 - `Gaussian` - Blurring filter that uses gaussian function, useful for noise removal.
-- `Lanczos` - High-quality resampling filter for photographic images yielding sharp results, but it's slower than cubic filters.
+- `Lanczos` - High-quality resampling filter for photographic images yielding sharp results, slower than cubic filters.
 
 The full list of supported filters:  NearestNeighbor, Box, Linear, Hermite, MitchellNetravali, CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine. Custom filters can be created using ResampleFilter struct.
 
 **Resampling filters comparison**
 
-The original image.
+Original image:
 
 ![srcImage](testdata/branches.png)
 
@@ -139,7 +136,7 @@ func main() {
 	// Open a test image.
 	src, err := imaging.Open("testdata/flowers.png")
 	if err != nil {
-		log.Fatalf("Open failed: %v", err)
+		log.Fatalf("failed to open image: %v", err)
 	}
 
 	// Crop the original image to 300x300px size using the center anchor.
@@ -180,7 +177,7 @@ func main() {
 	// Save the resulting image as JPEG.
 	err = imaging.Save(dst, "testdata/out_example.jpg")
 	if err != nil {
-		log.Fatalf("Save failed: %v", err)
+		log.Fatalf("failed to save image: %v", err)
 	}
 }
 ```

+ 139 - 117
adjust.go

@@ -6,50 +6,95 @@ import (
 	"math"
 )
 
-// AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image.
+// Grayscale produces a grayscale version of the image.
+func Grayscale(img image.Image) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
+	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+src.w*4])
+			for x := 0; x < src.w; x++ {
+				r := dst.Pix[i+0]
+				g := dst.Pix[i+1]
+				b := dst.Pix[i+2]
+				f := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
+				y := uint8(f + 0.5)
+				dst.Pix[i+0] = y
+				dst.Pix[i+1] = y
+				dst.Pix[i+2] = y
+				i += 4
+			}
+		}
+	})
+	return dst
+}
+
+// Invert produces an inverted (negated) version of the image.
+func Invert(img image.Image) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
+	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+src.w*4])
+			for x := 0; x < src.w; x++ {
+				dst.Pix[i+0] = 255 - dst.Pix[i+0]
+				dst.Pix[i+1] = 255 - dst.Pix[i+1]
+				dst.Pix[i+2] = 255 - dst.Pix[i+2]
+				i += 4
+			}
+		}
+	})
+	return dst
+}
+
+// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
+// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
+// The percentage = -100 gives solid grey image.
 //
-// Example:
+// Examples:
 //
-//	dstImage = imaging.AdjustFunc(
-//		srcImage,
-//		func(c color.NRGBA) color.NRGBA {
-//			// shift the red channel by 16
-//			r := int(c.R) + 16
-//			if r > 255 {
-//				r = 255
-//			}
-//			return color.NRGBA{uint8(r), c.G, c.B, c.A}
-//		}
-//	)
+//	dstImage = imaging.AdjustContrast(srcImage, -10) // decrease image contrast by 10%
+//	dstImage = imaging.AdjustContrast(srcImage, 20) // increase image contrast by 20%
 //
-func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
-	src := toNRGBA(img)
-	width := src.Bounds().Max.X
-	height := src.Bounds().Max.Y
-	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
-
-	parallel(height, func(partStart, partEnd int) {
-		for y := partStart; y < partEnd; y++ {
-			for x := 0; x < width; x++ {
-				i := y*src.Stride + x*4
-				j := y*dst.Stride + x*4
-
-				r := src.Pix[i+0]
-				g := src.Pix[i+1]
-				b := src.Pix[i+2]
-				a := src.Pix[i+3]
-
-				c := fn(color.NRGBA{r, g, b, a})
+func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
+	percentage = math.Min(math.Max(percentage, -100.0), 100.0)
+	lut := make([]uint8, 256)
 
-				dst.Pix[j+0] = c.R
-				dst.Pix[j+1] = c.G
-				dst.Pix[j+2] = c.B
-				dst.Pix[j+3] = c.A
-			}
+	v := (100.0 + percentage) / 100.0
+	for i := 0; i < 256; i++ {
+		if 0 <= v && v <= 1 {
+			lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
+		} else if 1 < v && v < 2 {
+			lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
+		} else {
+			lut[i] = uint8(float64(i)/255.0+0.5) * 255
 		}
-	})
+	}
 
-	return dst
+	return adjustLUT(img, lut)
+}
+
+// AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
+// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
+// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
+//
+// Examples:
+//
+//	dstImage = imaging.AdjustBrightness(srcImage, -15) // decrease image brightness by 15%
+//	dstImage = imaging.AdjustBrightness(srcImage, 10) // increase image brightness by 10%
+//
+func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
+	percentage = math.Min(math.Max(percentage, -100.0), 100.0)
+	lut := make([]uint8, 256)
+
+	shift := 255.0 * percentage / 100.0
+	for i := 0; i < 256; i++ {
+		lut[i] = clamp(float64(i) + shift)
+	}
+
+	return adjustLUT(img, lut)
 }
 
 // AdjustGamma performs a gamma correction on the image and returns the adjusted image.
@@ -68,15 +113,7 @@ func AdjustGamma(img image.Image, gamma float64) *image.NRGBA {
 		lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0)
 	}
 
-	fn := func(c color.NRGBA) color.NRGBA {
-		return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
-	}
-
-	return AdjustFunc(img, fn)
-}
-
-func sigmoid(a, b, x float64) float64 {
-	return 1 / (1 + math.Exp(b*(a-x)))
+	return adjustLUT(img, lut)
 }
 
 // AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image.
@@ -118,83 +155,68 @@ func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA {
 		}
 	}
 
-	fn := func(c color.NRGBA) color.NRGBA {
-		return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
-	}
-
-	return AdjustFunc(img, fn)
+	return adjustLUT(img, lut)
 }
 
-// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
-// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
-// The percentage = -100 gives solid grey image.
-//
-// Examples:
-//
-//	dstImage = imaging.AdjustContrast(srcImage, -10) // decrease image contrast by 10%
-//	dstImage = imaging.AdjustContrast(srcImage, 20) // increase image contrast by 20%
-//
-func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
-	percentage = math.Min(math.Max(percentage, -100.0), 100.0)
-	lut := make([]uint8, 256)
+func sigmoid(a, b, x float64) float64 {
+	return 1 / (1 + math.Exp(b*(a-x)))
+}
 
-	v := (100.0 + percentage) / 100.0
-	for i := 0; i < 256; i++ {
-		if 0 <= v && v <= 1 {
-			lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
-		} else if 1 < v && v < 2 {
-			lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
-		} else {
-			lut[i] = uint8(float64(i)/255.0+0.5) * 255
+// adjustLUT applies the given lookup table to the colors of the image.
+func adjustLUT(img image.Image, lut []uint8) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
+	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+src.w*4])
+			for x := 0; x < src.w; x++ {
+				dst.Pix[i+0] = lut[dst.Pix[i+0]]
+				dst.Pix[i+1] = lut[dst.Pix[i+1]]
+				dst.Pix[i+2] = lut[dst.Pix[i+2]]
+				i += 4
+			}
 		}
-	}
-
-	fn := func(c color.NRGBA) color.NRGBA {
-		return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
-	}
-
-	return AdjustFunc(img, fn)
+	})
+	return dst
 }
 
-// AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
-// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
-// The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
+// AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image.
 //
-// Examples:
+// Example:
 //
-//	dstImage = imaging.AdjustBrightness(srcImage, -15) // decrease image brightness by 15%
-//	dstImage = imaging.AdjustBrightness(srcImage, 10) // increase image brightness by 10%
+//	dstImage = imaging.AdjustFunc(
+//		srcImage,
+//		func(c color.NRGBA) color.NRGBA {
+//			// shift the red channel by 16
+//			r := int(c.R) + 16
+//			if r > 255 {
+//				r = 255
+//			}
+//			return color.NRGBA{uint8(r), c.G, c.B, c.A}
+//		}
+//	)
 //
-func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
-	percentage = math.Min(math.Max(percentage, -100.0), 100.0)
-	lut := make([]uint8, 256)
-
-	shift := 255.0 * percentage / 100.0
-	for i := 0; i < 256; i++ {
-		lut[i] = clamp(float64(i) + shift)
-	}
-
-	fn := func(c color.NRGBA) color.NRGBA {
-		return color.NRGBA{lut[c.R], lut[c.G], lut[c.B], c.A}
-	}
-
-	return AdjustFunc(img, fn)
-}
-
-// Grayscale produces grayscale version of the image.
-func Grayscale(img image.Image) *image.NRGBA {
-	fn := func(c color.NRGBA) color.NRGBA {
-		f := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
-		y := uint8(f + 0.5)
-		return color.NRGBA{y, y, y, c.A}
-	}
-	return AdjustFunc(img, fn)
-}
-
-// Invert produces inverted (negated) version of the image.
-func Invert(img image.Image) *image.NRGBA {
-	fn := func(c color.NRGBA) color.NRGBA {
-		return color.NRGBA{255 - c.R, 255 - c.G, 255 - c.B, c.A}
-	}
-	return AdjustFunc(img, fn)
+func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
+	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+src.w*4])
+			for x := 0; x < src.w; x++ {
+				r := dst.Pix[i+0]
+				g := dst.Pix[i+1]
+				b := dst.Pix[i+2]
+				a := dst.Pix[i+3]
+				c := fn(color.NRGBA{r, g, b, a})
+				dst.Pix[i+0] = c.R
+				dst.Pix[i+1] = c.G
+				dst.Pix[i+2] = c.B
+				dst.Pix[i+3] = c.A
+				i += 4
+			}
+		}
+	})
+	return dst
 }

+ 131 - 54
adjust_test.go

@@ -2,12 +2,13 @@ package imaging
 
 import (
 	"image"
+	"image/color"
 	"testing"
 )
 
 func TestGrayscale(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -33,12 +34,13 @@ func TestGrayscale(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Grayscale(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Grayscale(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -50,8 +52,8 @@ func BenchmarkGrayscale(b *testing.B) {
 }
 
 func TestInvert(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -77,12 +79,13 @@ func TestInvert(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Invert(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Invert(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -94,8 +97,8 @@ func BenchmarkInvert(b *testing.B) {
 }
 
 func TestAdjustContrast(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		p    float64
 		want *image.NRGBA
@@ -211,12 +214,13 @@ func TestAdjustContrast(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := AdjustContrast(d.src, d.p)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AdjustContrast(tc.src, tc.p)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -228,10 +232,10 @@ func TestAdjustContrastGolden(t *testing.T) {
 		got := AdjustContrast(testdataFlowersSmallPNG, p)
 		want, err := Open("testdata/" + name)
 		if err != nil {
-			t.Errorf("Open: %v", err)
+			t.Fatalf("failed to open image: %v", err)
 		}
 		if !compareNRGBA(got, toNRGBA(want), 0) {
-			t.Errorf("resulting image differs from golden: %s", name)
+			t.Fatalf("resulting image differs from golden: %s", name)
 		}
 	}
 }
@@ -244,8 +248,8 @@ func BenchmarkAdjustContrast(b *testing.B) {
 }
 
 func TestAdjustBrightness(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		p    float64
 		want *image.NRGBA
@@ -361,12 +365,13 @@ func TestAdjustBrightness(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := AdjustBrightness(d.src, d.p)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AdjustBrightness(tc.src, tc.p)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -378,10 +383,10 @@ func TestAdjustBrightnessGolden(t *testing.T) {
 		got := AdjustBrightness(testdataFlowersSmallPNG, p)
 		want, err := Open("testdata/" + name)
 		if err != nil {
-			t.Errorf("Open: %v", err)
+			t.Fatalf("failed to open image: %v", err)
 		}
 		if !compareNRGBA(got, toNRGBA(want), 0) {
-			t.Errorf("resulting image differs from golden: %s", name)
+			t.Fatalf("resulting image differs from golden: %s", name)
 		}
 	}
 }
@@ -394,8 +399,8 @@ func BenchmarkAdjustBrightness(b *testing.B) {
 }
 
 func TestAdjustGamma(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		p    float64
 		want *image.NRGBA
@@ -467,12 +472,13 @@ func TestAdjustGamma(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := AdjustGamma(d.src, d.p)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AdjustGamma(tc.src, tc.p)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -484,10 +490,10 @@ func TestAdjustGammaGolden(t *testing.T) {
 		got := AdjustGamma(testdataFlowersSmallPNG, g)
 		want, err := Open("testdata/" + name)
 		if err != nil {
-			t.Errorf("Open: %v", err)
+			t.Fatalf("failed to open image: %v", err)
 		}
 		if !compareNRGBA(got, toNRGBA(want), 0) {
-			t.Errorf("resulting image differs from golden: %s", name)
+			t.Fatalf("resulting image differs from golden: %s", name)
 		}
 	}
 }
@@ -500,8 +506,8 @@ func BenchmarkAdjustGamma(b *testing.B) {
 }
 
 func TestAdjustSigmoid(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		m    float64
 		p    float64
@@ -577,12 +583,13 @@ func TestAdjustSigmoid(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := AdjustSigmoid(d.src, d.m, d.p)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AdjustSigmoid(tc.src, tc.m, tc.p)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -592,3 +599,73 @@ func BenchmarkAdjustSigmoid(b *testing.B) {
 		AdjustSigmoid(testdataBranchesJPG, 0.5, 3.0)
 	}
 }
+
+func TestAdjustFunc(t *testing.T) {
+	testCases := []struct {
+		name string
+		src  image.Image
+		fn   func(c color.NRGBA) color.NRGBA
+		want *image.NRGBA
+	}{
+		{
+			"invert",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			func(c color.NRGBA) color.NRGBA {
+				return color.NRGBA{255 - c.R, 255 - c.G, 255 - c.B, c.A}
+			},
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x33, 0xff, 0xff, 0x01, 0xff, 0x33, 0xff, 0x02, 0xff, 0xff, 0x33, 0x03,
+					0xee, 0xdd, 0xcc, 0xff, 0xcc, 0xdd, 0xee, 0xff, 0x55, 0xcc, 0x44, 0xff,
+					0xff, 0xff, 0xff, 0xff, 0xcc, 0xcc, 0xcc, 0xff, 0x00, 0x00, 0x00, 0xff,
+				},
+			},
+		},
+		{
+			"threshold",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 2),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+					0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			func(c color.NRGBA) color.NRGBA {
+				y := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
+				if y > 0x55 {
+					return color.NRGBA{0xff, 0xff, 0xff, c.A}
+				}
+				return color.NRGBA{0x00, 0x00, 0x00, c.A}
+			},
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0x02, 0x00, 0x00, 0x00, 0x03,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AdjustFunc(tc.src, tc.fn)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
+	}
+}

+ 0 - 312
clone.go

@@ -1,312 +0,0 @@
-package imaging
-
-import (
-	"image"
-	"image/color"
-)
-
-// Clone returns a copy of the given image.
-func Clone(img image.Image) *image.NRGBA {
-	dstBounds := img.Bounds().Sub(img.Bounds().Min)
-	dst := image.NewNRGBA(dstBounds)
-
-	switch src := img.(type) {
-	case *image.NRGBA:
-		copyNRGBA(dst, src)
-	case *image.NRGBA64:
-		copyNRGBA64(dst, src)
-	case *image.RGBA:
-		copyRGBA(dst, src)
-	case *image.RGBA64:
-		copyRGBA64(dst, src)
-	case *image.Gray:
-		copyGray(dst, src)
-	case *image.Gray16:
-		copyGray16(dst, src)
-	case *image.YCbCr:
-		copyYCbCr(dst, src)
-	case *image.Paletted:
-		copyPaletted(dst, src)
-	default:
-		copyImage(dst, src)
-	}
-
-	return dst
-}
-
-func copyNRGBA(dst *image.NRGBA, src *image.NRGBA) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	rowSize := dstW * 4
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			si := src.PixOffset(srcMinX, srcMinY+dstY)
-			copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
-		}
-	})
-}
-
-func copyNRGBA64(dst *image.NRGBA, src *image.NRGBA64) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			si := src.PixOffset(srcMinX, srcMinY+dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				dst.Pix[di+0] = src.Pix[si+0]
-				dst.Pix[di+1] = src.Pix[si+2]
-				dst.Pix[di+2] = src.Pix[si+4]
-				dst.Pix[di+3] = src.Pix[si+6]
-				di += 4
-				si += 8
-			}
-		}
-	})
-}
-
-func copyRGBA(dst *image.NRGBA, src *image.RGBA) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			si := src.PixOffset(srcMinX, srcMinY+dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				a := src.Pix[si+3]
-				dst.Pix[di+3] = a
-
-				switch a {
-				case 0:
-					dst.Pix[di+0] = 0
-					dst.Pix[di+1] = 0
-					dst.Pix[di+2] = 0
-				case 0xff:
-					dst.Pix[di+0] = src.Pix[si+0]
-					dst.Pix[di+1] = src.Pix[si+1]
-					dst.Pix[di+2] = src.Pix[si+2]
-				default:
-					var tmp uint16
-					tmp = uint16(src.Pix[si+0]) * 0xff / uint16(a)
-					dst.Pix[di+0] = uint8(tmp)
-					tmp = uint16(src.Pix[si+1]) * 0xff / uint16(a)
-					dst.Pix[di+1] = uint8(tmp)
-					tmp = uint16(src.Pix[si+2]) * 0xff / uint16(a)
-					dst.Pix[di+2] = uint8(tmp)
-				}
-
-				di += 4
-				si += 4
-			}
-		}
-	})
-}
-
-func copyRGBA64(dst *image.NRGBA, src *image.RGBA64) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			si := src.PixOffset(srcMinX, srcMinY+dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				a := src.Pix[si+6]
-				dst.Pix[di+3] = a
-
-				switch a {
-				case 0:
-					dst.Pix[di+0] = 0
-					dst.Pix[di+1] = 0
-					dst.Pix[di+2] = 0
-				case 0xff:
-					dst.Pix[di+0] = src.Pix[si+0]
-					dst.Pix[di+1] = src.Pix[si+2]
-					dst.Pix[di+2] = src.Pix[si+4]
-				default:
-					var tmp uint16
-					tmp = uint16(src.Pix[si+0]) * 0xff / uint16(a)
-					dst.Pix[di+0] = uint8(tmp)
-					tmp = uint16(src.Pix[si+2]) * 0xff / uint16(a)
-					dst.Pix[di+1] = uint8(tmp)
-					tmp = uint16(src.Pix[si+4]) * 0xff / uint16(a)
-					dst.Pix[di+2] = uint8(tmp)
-				}
-
-				di += 4
-				si += 8
-			}
-		}
-	})
-}
-
-func copyGray(dst *image.NRGBA, src *image.Gray) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			si := src.PixOffset(srcMinX, srcMinY+dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				c := src.Pix[si]
-				dst.Pix[di+0] = c
-				dst.Pix[di+1] = c
-				dst.Pix[di+2] = c
-				dst.Pix[di+3] = 0xff
-				di += 4
-				si++
-			}
-		}
-	})
-}
-
-func copyGray16(dst *image.NRGBA, src *image.Gray16) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			si := src.PixOffset(srcMinX, srcMinY+dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				c := src.Pix[si]
-				dst.Pix[di+0] = c
-				dst.Pix[di+1] = c
-				dst.Pix[di+2] = c
-				dst.Pix[di+3] = 0xff
-				di += 4
-				si += 2
-			}
-		}
-	})
-}
-
-func copyYCbCr(dst *image.NRGBA, src *image.YCbCr) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			srcY := srcMinY + dstY
-			di := dst.PixOffset(0, dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := srcMinX + dstX
-
-				siy := (srcY-srcMinY)*src.YStride + (srcX - srcMinX)
-
-				var sic int
-				switch src.SubsampleRatio {
-				case image.YCbCrSubsampleRatio444:
-					sic = (srcY-srcMinY)*src.CStride + (srcX - srcMinX)
-				case image.YCbCrSubsampleRatio422:
-					sic = (srcY-srcMinY)*src.CStride + (srcX/2 - srcMinX/2)
-				case image.YCbCrSubsampleRatio420:
-					sic = (srcY/2-srcMinY/2)*src.CStride + (srcX/2 - srcMinX/2)
-				case image.YCbCrSubsampleRatio440:
-					sic = (srcY/2-srcMinY/2)*src.CStride + (srcX - srcMinX)
-				default:
-					sic = src.COffset(srcX, srcY)
-				}
-
-				y := int32(src.Y[siy])
-				cb := int32(src.Cb[sic]) - 128
-				cr := int32(src.Cr[sic]) - 128
-
-				r := (y<<16 + 91881*cr + 1<<15) >> 16
-				if r > 255 {
-					r = 255
-				} else if r < 0 {
-					r = 0
-				}
-
-				g := (y<<16 - 22554*cb - 46802*cr + 1<<15) >> 16
-				if g > 255 {
-					g = 255
-				} else if g < 0 {
-					g = 0
-				}
-
-				b := (y<<16 + 116130*cb + 1<<15) >> 16
-				if b > 255 {
-					b = 255
-				} else if b < 0 {
-					b = 0
-				}
-
-				dst.Pix[di+0] = uint8(r)
-				dst.Pix[di+1] = uint8(g)
-				dst.Pix[di+2] = uint8(b)
-				dst.Pix[di+3] = 255
-
-				di += 4
-			}
-		}
-	})
-}
-
-func copyPaletted(dst *image.NRGBA, src *image.Paletted) {
-	srcMinX := src.Rect.Min.X
-	srcMinY := src.Rect.Min.Y
-	dstW := dst.Rect.Dx()
-	dstH := dst.Rect.Dy()
-	plen := len(src.Palette)
-	pnew := make([]color.NRGBA, plen)
-	for i := 0; i < plen; i++ {
-		pnew[i] = color.NRGBAModel.Convert(src.Palette[i]).(color.NRGBA)
-	}
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			si := src.PixOffset(srcMinX, srcMinY+dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				c := pnew[src.Pix[si]]
-				dst.Pix[di+0] = c.R
-				dst.Pix[di+1] = c.G
-				dst.Pix[di+2] = c.B
-				dst.Pix[di+3] = c.A
-				di += 4
-				si++
-			}
-		}
-	})
-}
-
-func copyImage(dst *image.NRGBA, src image.Image) {
-	srcMinX := src.Bounds().Min.X
-	srcMinY := src.Bounds().Min.Y
-	dstW := dst.Bounds().Dx()
-	dstH := dst.Bounds().Dy()
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			di := dst.PixOffset(0, dstY)
-			for dstX := 0; dstX < dstW; dstX++ {
-				c := color.NRGBAModel.Convert(src.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
-				dst.Pix[di+0] = c.R
-				dst.Pix[di+1] = c.G
-				dst.Pix[di+2] = c.B
-				dst.Pix[di+3] = c.A
-				di += 4
-			}
-		}
-	})
-}
-
-// toNRGBA converts any image type to *image.NRGBA with min-point at (0, 0).
-func toNRGBA(img image.Image) *image.NRGBA {
-	if img, ok := img.(*image.NRGBA); ok && img.Bounds().Min.Eq(image.ZP) {
-		return img
-	}
-	return Clone(img)
-}

+ 0 - 247
clone_test.go

@@ -1,247 +0,0 @@
-package imaging
-
-import (
-	"image"
-	"image/color"
-	"testing"
-)
-
-func TestClone(t *testing.T) {
-	td := []struct {
-		desc 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 _, d := range td {
-		got := Clone(d.src)
-		want := d.want
-
-		delta := 0
-		if _, ok := d.src.(*image.YCbCr); ok {
-			delta = 1
-		}
-
-		if !compareNRGBA(got, want, delta) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
-	}
-}

+ 2 - 4
convolution.go

@@ -58,8 +58,6 @@ func convolve(img image.Image, kernel []float64, options *ConvolveOptions) *imag
 		m = 1
 	case 25:
 		m = 2
-	default:
-		return dst
 	}
 
 	i := 0
@@ -72,8 +70,8 @@ func convolve(img image.Image, kernel []float64, options *ConvolveOptions) *imag
 		}
 	}
 
-	parallel(h, func(partStart, partEnd int) {
-		for y := partStart; y < partEnd; y++ {
+	parallel(0, h, func(ys <-chan int) {
+		for y := range ys {
 			for x := 0; x < w; x++ {
 				var r, g, b float64
 				for _, c := range coefs {

+ 85 - 12
convolution_test.go

@@ -7,7 +7,7 @@ import (
 
 func TestConvolve3x3(t *testing.T) {
 	testCases := []struct {
-		desc    string
+		name    string
 		src     image.Image
 		kernel  [9]float64
 		options *ConvolveOptions
@@ -174,17 +174,18 @@ func TestConvolve3x3(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		got := Convolve3x3(tc.src, tc.kernel, tc.options)
-		want := tc.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: want %#v got %#v", tc.desc, want, got)
-		}
+		t.Run(tc.name, func(t *testing.T) {
+			got := Convolve3x3(tc.src, tc.kernel, tc.options)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
 func TestConvolve5x5(t *testing.T) {
 	testCases := []struct {
-		desc    string
+		name    string
 		src     image.Image
 		kernel  [25]float64
 		options *ConvolveOptions
@@ -224,11 +225,83 @@ func TestConvolve5x5(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		got := Convolve5x5(tc.src, tc.kernel, tc.options)
-		want := tc.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: want %#v got %#v", tc.desc, want, got)
-		}
+		t.Run(tc.name, func(t *testing.T) {
+			got := Convolve5x5(tc.src, tc.kernel, tc.options)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
+	}
+}
+
+func TestNormalizeKernel(t *testing.T) {
+	testCases := []struct {
+		name   string
+		kernel []float64
+		want   []float64
+	}{
+		{
+			name: "positive sum",
+			kernel: []float64{
+				2, 0, 2,
+				0, 2, 0,
+				2, 0, 2,
+			},
+			want: []float64{
+				0.2, 0, 0.2,
+				0, 0.2, 0,
+				0.2, 0, 0.2,
+			},
+		},
+		{
+			name: "negative sum",
+			kernel: []float64{
+				-2, 0, -2,
+				2, 2, 2,
+				-2, 0, -2,
+			},
+			want: []float64{
+				1, 0, 1,
+				-1, -1, -1,
+				1, 0, 1,
+			},
+		},
+		{
+			name: "zero sum",
+			kernel: []float64{
+				0, 2, 0,
+				2, 0, -2,
+				0, -2, 0,
+			},
+			want: []float64{
+				0, 0.5, 0,
+				0.5, 0, -0.5,
+				0, -0.5, 0,
+			},
+		},
+		{
+			name: "all zero",
+			kernel: []float64{
+				0, 0, 0,
+				0, 0, 0,
+				0, 0, 0,
+			},
+			want: []float64{
+				0, 0, 0,
+				0, 0, 0,
+				0, 0, 0,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			normalizeKernel(tc.kernel)
+			for i := range tc.kernel {
+				if tc.kernel[i] != tc.want[i] {
+					t.Fatalf("got kernel %v want %v", tc.kernel, tc.want)
+				}
+			}
+		})
 	}
 }
 

+ 6 - 1
doc.go

@@ -1,2 +1,7 @@
-// Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.).
+/*
+Package imaging provides basic imaging processing functions (resize, rotate, crop, brightness/contrast adjustments, etc.).
+
+All the image processing functions provided by the package accept any image type that implements image.Image interface
+as an input, and return a new image of *image.NRGBA type (32bit RGBA colors, not premultiplied by alpha).
+*/
 package imaging

+ 82 - 92
effects.go

@@ -18,11 +18,9 @@ func gaussianBlurKernel(x, sigma float64) float64 {
 //
 func Blur(img image.Image, sigma float64) *image.NRGBA {
 	if sigma <= 0 {
-		// sigma parameter must be positive!
 		return Clone(img)
 	}
 
-	src := toNRGBA(img)
 	radius := int(math.Ceil(sigma * 3.0))
 	kernel := make([]float64, radius+1)
 
@@ -30,54 +28,50 @@ func Blur(img image.Image, sigma float64) *image.NRGBA {
 		kernel[i] = gaussianBlurKernel(float64(i), sigma)
 	}
 
-	var dst *image.NRGBA
-	dst = blurHorizontal(src, kernel)
-	dst = blurVertical(dst, kernel)
-
-	return dst
+	return blurVertical(blurHorizontal(img, kernel), kernel)
 }
 
-func blurHorizontal(src *image.NRGBA, kernel []float64) *image.NRGBA {
+func blurHorizontal(img image.Image, kernel []float64) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
 	radius := len(kernel) - 1
-	width := src.Bounds().Max.X
-	height := src.Bounds().Max.Y
-
-	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
-
-	parallel(width, func(partStart, partEnd int) {
-		for x := partStart; x < partEnd; x++ {
-			start := x - radius
-			if start < 0 {
-				start = 0
-			}
 
-			end := x + radius
-			if end > width-1 {
-				end = width - 1
-			}
-
-			weightSum := 0.0
-			for ix := start; ix <= end; ix++ {
-				weightSum += kernel[absint(x-ix)]
-			}
+	parallel(0, src.h, func(ys <-chan int) {
+		scanLine := make([]uint8, src.w*4)
+		for y := range ys {
+			src.scan(0, y, src.w, y+1, scanLine)
+			for x := 0; x < src.w; x++ {
+				min := x - radius
+				if min < 0 {
+					min = 0
+				}
+				max := x + radius
+				if max > src.w-1 {
+					max = src.w - 1
+				}
 
-			for y := 0; y < height; y++ {
-				var r, g, b, a float64
-				for ix := start; ix <= end; ix++ {
+				var r, g, b, a, wsum float64
+				for ix := min; ix <= max; ix++ {
+					i := ix * 4
 					weight := kernel[absint(x-ix)]
-					i := y*src.Stride + ix*4
-					wa := float64(src.Pix[i+3]) * weight
-					r += float64(src.Pix[i+0]) * wa
-					g += float64(src.Pix[i+1]) * wa
-					b += float64(src.Pix[i+2]) * wa
+					wsum += weight
+					wa := float64(scanLine[i+3]) * weight
+					r += float64(scanLine[i+0]) * wa
+					g += float64(scanLine[i+1]) * wa
+					b += float64(scanLine[i+2]) * wa
 					a += wa
 				}
+				if a != 0 {
+					r /= a
+					g /= a
+					b /= a
+				}
 
 				j := y*dst.Stride + x*4
-				dst.Pix[j+0] = clamp(r / a)
-				dst.Pix[j+1] = clamp(g / a)
-				dst.Pix[j+2] = clamp(b / a)
-				dst.Pix[j+3] = clamp(a / weightSum)
+				dst.Pix[j+0] = clamp(r)
+				dst.Pix[j+1] = clamp(g)
+				dst.Pix[j+2] = clamp(b)
+				dst.Pix[j+3] = clamp(a / wsum)
 			}
 		}
 	})
@@ -85,47 +79,47 @@ func blurHorizontal(src *image.NRGBA, kernel []float64) *image.NRGBA {
 	return dst
 }
 
-func blurVertical(src *image.NRGBA, kernel []float64) *image.NRGBA {
+func blurVertical(img image.Image, kernel []float64) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
 	radius := len(kernel) - 1
-	width := src.Bounds().Max.X
-	height := src.Bounds().Max.Y
-
-	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
-
-	parallel(height, func(partStart, partEnd int) {
-		for y := partStart; y < partEnd; y++ {
-			start := y - radius
-			if start < 0 {
-				start = 0
-			}
-
-			end := y + radius
-			if end > height-1 {
-				end = height - 1
-			}
 
-			weightSum := 0.0
-			for iy := start; iy <= end; iy++ {
-				weightSum += kernel[absint(y-iy)]
-			}
+	parallel(0, src.w, func(xs <-chan int) {
+		scanLine := make([]uint8, src.h*4)
+		for x := range xs {
+			src.scan(x, 0, x+1, src.h, scanLine)
+			for y := 0; y < src.h; y++ {
+				min := y - radius
+				if min < 0 {
+					min = 0
+				}
+				max := y + radius
+				if max > src.h-1 {
+					max = src.h - 1
+				}
 
-			for x := 0; x < width; x++ {
-				var r, g, b, a float64
-				for iy := start; iy <= end; iy++ {
+				var r, g, b, a, wsum float64
+				for iy := min; iy <= max; iy++ {
+					i := iy * 4
 					weight := kernel[absint(y-iy)]
-					i := iy*src.Stride + x*4
-					wa := float64(src.Pix[i+3]) * weight
-					r += float64(src.Pix[i+0]) * wa
-					g += float64(src.Pix[i+1]) * wa
-					b += float64(src.Pix[i+2]) * wa
+					wsum += weight
+					wa := float64(scanLine[i+3]) * weight
+					r += float64(scanLine[i+0]) * wa
+					g += float64(scanLine[i+1]) * wa
+					b += float64(scanLine[i+2]) * wa
 					a += wa
 				}
+				if a != 0 {
+					r /= a
+					g /= a
+					b /= a
+				}
 
 				j := y*dst.Stride + x*4
-				dst.Pix[j+0] = clamp(r / a)
-				dst.Pix[j+1] = clamp(g / a)
-				dst.Pix[j+2] = clamp(b / a)
-				dst.Pix[j+3] = clamp(a / weightSum)
+				dst.Pix[j+0] = clamp(r)
+				dst.Pix[j+1] = clamp(g)
+				dst.Pix[j+2] = clamp(b)
+				dst.Pix[j+3] = clamp(a / wsum)
 			}
 		}
 	})
@@ -142,31 +136,27 @@ func blurVertical(src *image.NRGBA, kernel []float64) *image.NRGBA {
 //
 func Sharpen(img image.Image, sigma float64) *image.NRGBA {
 	if sigma <= 0 {
-		// sigma parameter must be positive!
 		return Clone(img)
 	}
 
-	src := toNRGBA(img)
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
 	blurred := Blur(img, sigma)
 
-	width := src.Bounds().Max.X
-	height := src.Bounds().Max.Y
-	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
-
-	parallel(height, func(partStart, partEnd int) {
-		for y := partStart; y < partEnd; y++ {
-			for x := 0; x < width; x++ {
-				i := y*src.Stride + x*4
-				for j := 0; j < 4; j++ {
-					k := i + j
-					val := int(src.Pix[k])<<1 - int(blurred.Pix[k])
-					if val < 0 {
-						val = 0
-					} else if val > 255 {
-						val = 255
-					}
-					dst.Pix[k] = uint8(val)
+	parallel(0, src.h, func(ys <-chan int) {
+		scanLine := make([]uint8, src.w*4)
+		for y := range ys {
+			src.scan(0, y, src.w, y+1, scanLine)
+			j := y * dst.Stride
+			for i := 0; i < src.w*4; i++ {
+				val := int(scanLine[i])<<1 - int(blurred.Pix[j])
+				if val < 0 {
+					val = 0
+				} else if val > 0xff {
+					val = 0xff
 				}
+				dst.Pix[j] = uint8(val)
+				j++
 			}
 		}
 	})

+ 22 - 20
effects_test.go

@@ -6,8 +6,8 @@ import (
 )
 
 func TestBlur(t *testing.T) {
-	td := []struct {
-		desc  string
+	testCases := []struct {
+		name  string
 		src   image.Image
 		sigma float64
 		want  *image.NRGBA
@@ -79,12 +79,13 @@ func TestBlur(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Blur(d.src, d.sigma)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Blur(tc.src, tc.sigma)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -96,10 +97,10 @@ func TestBlurGolden(t *testing.T) {
 		got := Blur(testdataFlowersSmallPNG, sigma)
 		want, err := Open("testdata/" + name)
 		if err != nil {
-			t.Errorf("Open: %v", err)
+			t.Fatalf("failed to open image: %v", err)
 		}
 		if !compareNRGBA(got, toNRGBA(want), 0) {
-			t.Errorf("resulting image differs from golden: %s", name)
+			t.Fatalf("resulting image differs from golden: %s", name)
 		}
 	}
 }
@@ -112,8 +113,8 @@ func BenchmarkBlur(b *testing.B) {
 }
 
 func TestSharpen(t *testing.T) {
-	td := []struct {
-		desc  string
+	testCases := []struct {
+		name  string
 		src   image.Image
 		sigma float64
 		want  *image.NRGBA
@@ -203,12 +204,13 @@ func TestSharpen(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Sharpen(d.src, d.sigma)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Sharpen(tc.src, tc.sigma)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -220,10 +222,10 @@ func TestSharpenGolden(t *testing.T) {
 		got := Sharpen(testdataFlowersSmallPNG, sigma)
 		want, err := Open("testdata/" + name)
 		if err != nil {
-			t.Errorf("Open: %v", err)
+			t.Fatalf("failed to open image: %v", err)
 		}
 		if !compareNRGBA(got, toNRGBA(want), 0) {
-			t.Errorf("resulting image differs from golden: %s", name)
+			t.Fatalf("resulting image differs from golden: %s", name)
 		}
 	}
 }

+ 2 - 2
example_test.go

@@ -12,7 +12,7 @@ func Example() {
 	// Open a test image.
 	src, err := imaging.Open("testdata/flowers.png")
 	if err != nil {
-		log.Fatalf("Open failed: %v", err)
+		log.Fatalf("failed to open image: %v", err)
 	}
 
 	// Crop the original image to 300x300px size using the center anchor.
@@ -53,6 +53,6 @@ func Example() {
 	// Save the resulting image as JPEG.
 	err = imaging.Save(dst, "testdata/out_example.jpg")
 	if err != nil {
-		log.Fatalf("Save failed: %v", err)
+		log.Fatalf("failed to save image: %v", err)
 	}
 }

+ 51 - 11
helpers.go

@@ -50,6 +50,18 @@ var (
 	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)
@@ -58,7 +70,7 @@ func Decode(r io.Reader) (image.Image, error) {
 
 // Open loads an image from file
 func Open(filename string) (image.Image, error) {
-	file, err := os.Open(filename)
+	file, err := fs.Open(filename)
 	if err != nil {
 		return nil, err
 	}
@@ -153,11 +165,17 @@ func Save(img image.Image, filename string, opts ...EncodeOption) (err error) {
 		return ErrUnsupportedFormat
 	}
 
-	file, err := os.Create(filename)
+	file, err := fs.Create(filename)
 	if err != nil {
 		return err
 	}
-	defer file.Close()
+
+	defer func() {
+		cerr := file.Close()
+		if err == nil {
+			err = cerr
+		}
+	}()
 
 	return Encode(file, img, f, opts...)
 }
@@ -175,16 +193,38 @@ func New(width, height int, fillColor color.Color) *image.NRGBA {
 		return dst
 	}
 
-	cs := []uint8{c.R, c.G, c.B, c.A}
-
-	// fill the first row
+	// Fill the first row.
+	i := 0
 	for x := 0; x < width; x++ {
-		copy(dst.Pix[x*4:(x+1)*4], cs)
-	}
-	// copy the first row to other rows
-	for y := 1; y < height; y++ {
-		copy(dst.Pix[y*dst.Stride:y*dst.Stride+width*4], dst.Pix[0:width*4])
+		dst.Pix[i+0] = c.R
+		dst.Pix[i+1] = c.G
+		dst.Pix[i+2] = c.B
+		dst.Pix[i+3] = c.A
+		i += 4
 	}
 
+	// Copy the first row to other rows.
+	size := width * 4
+	parallel(1, height, func(ys <-chan int) {
+		for y := range ys {
+			i = y * dst.Stride
+			copy(dst.Pix[i:i+size], dst.Pix[0:size])
+		}
+	})
+
+	return dst
+}
+
+// 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
 }

+ 353 - 53
helpers_test.go

@@ -2,99 +2,148 @@ package imaging
 
 import (
 	"bytes"
+	"errors"
 	"image"
 	"image/color"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"testing"
 )
 
-func compareNRGBA(img1, img2 *image.NRGBA, delta int) bool {
-	if !img1.Rect.Eq(img2.Rect) {
-		return false
-	}
+var (
+	errCreate = errors.New("failed to create file")
+	errClose  = errors.New("failed to close file")
+	errOpen   = errors.New("failed to open file")
+)
 
-	if len(img1.Pix) != len(img2.Pix) {
-		return false
-	}
+type badFS struct{}
 
-	for i := 0; i < len(img1.Pix); i++ {
-		if absint(int(img1.Pix[i])-int(img2.Pix[i])) > delta {
-			return false
-		}
+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
+}
 
-	return true
+func (badFile) Close() error {
+	return errClose
 }
 
-func TestEncodeDecode(t *testing.T) {
-	imgWithAlpha := image.NewNRGBA(image.Rect(0, 0, 3, 3))
+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{
-		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
-		127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138,
-		244, 245, 246, 247, 248, 249, 250, 252, 252, 253, 254, 255,
+		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,
 	}
 
-	imgWithoutAlpha := image.NewNRGBA(image.Rect(0, 0, 3, 3))
-	imgWithoutAlpha.Pix = []uint8{
-		0, 1, 2, 255, 4, 5, 6, 255, 8, 9, 10, 255,
-		127, 128, 129, 255, 131, 132, 133, 255, 135, 136, 137, 255,
-		244, 245, 246, 255, 248, 249, 250, 255, 252, 253, 254, 255,
+	options := []EncodeOption{
+		JPEGQuality(100),
 	}
 
-	for _, format := range []Format{JPEG, PNG, GIF, BMP, TIFF} {
+	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 format == PNG {
+		if ext == "png" {
 			img = imgWithAlpha
 		}
 
-		buf := &bytes.Buffer{}
-		err := Encode(buf, img, format)
+		err := Save(img, filename, options...)
 		if err != nil {
-			t.Errorf("fail encoding format %s", format)
-			continue
+			t.Fatalf("failed to save image (%q): %v", filename, err)
 		}
 
-		img2, err := Decode(buf)
+		img2, err := Open(filename)
 		if err != nil {
-			t.Errorf("fail decoding format %s", format)
-			continue
+			t.Fatalf("failed to open image (%q): %v", filename, err)
 		}
-		img2cloned := Clone(img2)
+		got := Clone(img2)
 
 		delta := 0
-		if format == JPEG {
+		if ext == "jpg" || ext == "jpeg" || ext == "gif" {
 			delta = 3
-		} else if format == GIF {
-			delta = 16
 		}
 
-		if !compareNRGBA(img, img2cloned, delta) {
-			t.Errorf("test [DecodeEncode %s] failed: %#v %#v", format, img, img2cloned)
-			continue
+		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)
+	err = Encode(buf, imgWithAlpha, JPEG)
 	if err != nil {
-		t.Errorf("failed encoding alpha to JPEG format %s", err)
+		t.Fatalf("failed to encode alpha to JPEG: %v", err)
 	}
 
 	buf = &bytes.Buffer{}
 	err = Encode(buf, imgWithAlpha, Format(100))
 	if err != ErrUnsupportedFormat {
-		t.Errorf("expected ErrUnsupportedFormat")
+		t.Fatalf("got %v want ErrUnsupportedFormat", err)
 	}
 
 	buf = bytes.NewBuffer([]byte("bad data"))
 	_, err = Decode(buf)
 	if err == nil {
-		t.Errorf("decoding bad data, expected error")
+		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) {
-	td := []struct {
-		desc      string
+	testCases := []struct {
+		name      string
 		w, h      int
 		c         color.Color
 		dstBounds image.Rectangle
@@ -121,6 +170,17 @@ func TestNew(t *testing.T) {
 			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,
@@ -130,13 +190,15 @@ func TestNew(t *testing.T) {
 		},
 	}
 
-	for _, d := range td {
-		got := New(d.w, d.h, d.c)
-		want := image.NewNRGBA(d.dstBounds)
-		want.Pix = d.dstPix
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	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)
+			}
+		})
 	}
 }
 
@@ -159,8 +221,246 @@ func TestFormats(t *testing.T) {
 	for format, name := range formatNames {
 		got := format.String()
 		if got != name {
-			t.Errorf("test [Format names] failed: got %#v want %#v", got, name)
-			continue
+			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)
+			}
+		})
+	}
+}

+ 27 - 19
histogram.go

@@ -2,6 +2,7 @@ package imaging
 
 import (
 	"image"
+	"sync"
 )
 
 // Histogram returns a normalized histogram of an image.
@@ -9,35 +10,42 @@ import (
 // Resulting histogram is represented as an array of 256 floats, where
 // histogram[i] is a probability of a pixel being of a particular luminance i.
 func Histogram(img image.Image) [256]float64 {
-	src := toNRGBA(img)
-	width := src.Bounds().Max.X
-	height := src.Bounds().Max.Y
-
+	var mu sync.Mutex
 	var histogram [256]float64
 	var total float64
 
-	if width == 0 || height == 0 {
+	src := newScanner(img)
+	if src.w == 0 || src.h == 0 {
 		return histogram
 	}
 
-	for y := 0; y < height; y++ {
-		for x := 0; x < width; x++ {
-			i := y*src.Stride + x*4
-
-			r := src.Pix[i+0]
-			g := src.Pix[i+1]
-			b := src.Pix[i+2]
-
-			y := 0.299*float32(r) + 0.587*float32(g) + 0.114*float32(b)
-
-			histogram[int(y+0.5)]++
-			total++
+	parallel(0, src.h, func(ys <-chan int) {
+		var tmpHistogram [256]float64
+		var tmpTotal float64
+		scanLine := make([]uint8, src.w*4)
+		for y := range ys {
+			src.scan(0, y, src.w, y+1, scanLine)
+			i := 0
+			for x := 0; x < src.w; x++ {
+				r := scanLine[i+0]
+				g := scanLine[i+1]
+				b := scanLine[i+2]
+				y := 0.299*float32(r) + 0.587*float32(g) + 0.114*float32(b)
+				tmpHistogram[int(y+0.5)]++
+				tmpTotal++
+				i += 4
+			}
 		}
-	}
+		mu.Lock()
+		for i := 0; i < 256; i++ {
+			histogram[i] += tmpHistogram[i]
+		}
+		total += tmpTotal
+		mu.Unlock()
+	})
 
 	for i := 0; i < 256; i++ {
 		histogram[i] = histogram[i] / total
 	}
-
 	return histogram
 }

+ 41 - 31
histogram_test.go

@@ -2,42 +2,52 @@ package imaging
 
 import (
 	"image"
-	"image/color"
 	"testing"
 )
 
 func TestHistogram(t *testing.T) {
-	b := image.Rectangle{image.Point{0, 0}, image.Point{2, 2}}
-
-	i1 := image.NewRGBA(b)
-	i1.Set(0, 0, image.Black)
-	i1.Set(1, 0, image.White)
-	i1.Set(1, 1, image.White)
-	i1.Set(0, 1, color.Gray{123})
-
-	h := Histogram(i1)
-	if h[0] != 0.25 || h[123] != 0.25 || h[255] != 0.5 {
-		t.Errorf("Incorrect histogram for image i1")
-	}
-
-	i2 := image.NewRGBA(b)
-	i2.Set(0, 0, color.Gray{51})
-	i2.Set(0, 1, color.Gray{14})
-	i2.Set(1, 0, color.Gray{14})
-
-	h = Histogram(i2)
-	if h[14] != 0.5 || h[51] != 0.25 || h[0] != 0.25 {
-		t.Errorf("Incorrect histogram for image i2")
+	testCases := []struct {
+		name string
+		img  image.Image
+		want [256]float64
+	}{
+		{
+			name: "grayscale",
+			img: &image.RGBA{
+				Rect:   image.Rect(-1, -1, 1, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff,
+					0xff, 0xff, 0xff, 0xff, 0x80, 0x80, 0x80, 0xff,
+				},
+			},
+			want: [256]float64{0x00: 0.25, 0x80: 0.25, 0xff: 0.5},
+		},
+		{
+			name: "colorful",
+			img: &image.RGBA{
+				Rect:   image.Rect(-1, -1, 1, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x44, 0x55, 0xff,
+					0x55, 0x44, 0x33, 0xff, 0x77, 0x66, 0x55, 0xff,
+				},
+			},
+			want: [256]float64{0x00: 0.25, 0x41: 0.25, 0x47: 0.25, 0x69: 0.25},
+		},
+		{
+			name: "zero",
+			img:  &image.RGBA{},
+			want: [256]float64{},
+		},
 	}
-
-	b = image.Rectangle{image.Point{0, 0}, image.Point{0, 0}}
-	i3 := image.NewRGBA(b)
-	h = Histogram(i3)
-	for _, val := range h {
-		if val != 0 {
-			t.Errorf("Histogram for an empty image should be a zero histogram.")
-			return
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Histogram(tc.img)
+			if got != tc.want {
+				t.Fatalf("got histogram %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 

+ 102 - 122
resize.go

@@ -24,17 +24,17 @@ func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) [][]indexWei
 	for v := 0; v < dstSize; v++ {
 		fu := (float64(v)+0.5)*du - 0.5
 
-		startu := int(math.Ceil(fu - ru))
-		if startu < 0 {
-			startu = 0
+		begin := int(math.Ceil(fu - ru))
+		if begin < 0 {
+			begin = 0
 		}
-		endu := int(math.Floor(fu + ru))
-		if endu > srcSize-1 {
-			endu = srcSize - 1
+		end := int(math.Floor(fu + ru))
+		if end > srcSize-1 {
+			end = srcSize - 1
 		}
 
 		var sum float64
-		for u := startu; u <= endu; u++ {
+		for u := begin; u <= end; u++ {
 			w := filter.Kernel((float64(u) - fu) / scale)
 			if w != 0 {
 				sum += w
@@ -67,7 +67,6 @@ func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) [][]indexWei
 //
 func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
 	dstW, dstH := width, height
-
 	if dstW < 0 || dstH < 0 {
 		return &image.NRGBA{}
 	}
@@ -75,16 +74,13 @@ func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NR
 		return &image.NRGBA{}
 	}
 
-	src := toNRGBA(img)
-
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-
+	srcW := img.Bounds().Dx()
+	srcH := img.Bounds().Dy()
 	if srcW <= 0 || srcH <= 0 {
 		return &image.NRGBA{}
 	}
 
-	// if new width or height is 0 then preserve aspect ratio, minimum 1px
+	// If new width or height is 0 then preserve aspect ratio, minimum 1px.
 	if dstW == 0 {
 		tmpW := float64(dstH) * float64(srcW) / float64(srcH)
 		dstW = int(math.Max(1.0, math.Floor(tmpW+0.5)))
@@ -94,57 +90,45 @@ func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NR
 		dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
 	}
 
-	var dst *image.NRGBA
-
-	if filter.Support <= 0.0 {
-		// nearest-neighbor special case
-		dst = resizeNearest(src, dstW, dstH)
-
-	} else {
-		// two-pass resize
-		if srcW != dstW {
-			dst = resizeHorizontal(src, dstW, filter)
-		} else {
-			dst = src
-		}
-
-		if srcH != dstH {
-			dst = resizeVertical(dst, dstH, filter)
-		}
+	if filter.Support <= 0 {
+		// Nearest-neighbor special case.
+		return resizeNearest(img, dstW, dstH)
 	}
 
-	return dst
+	if srcW != dstW && srcH != dstH {
+		return resizeVertical(resizeHorizontal(img, dstW, filter), dstH, filter)
+	}
+	if srcW != dstW {
+		return resizeHorizontal(img, dstW, filter)
+	}
+	if srcH != dstH {
+		return resizeVertical(img, dstH, filter)
+	}
+	return Clone(img)
 }
 
-func resizeHorizontal(src *image.NRGBA, width int, filter ResampleFilter) *image.NRGBA {
-	srcBounds := src.Bounds()
-	srcW := srcBounds.Max.X
-	srcH := srcBounds.Max.Y
-
-	dstW := width
-	dstH := srcH
-
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	weights := precomputeWeights(dstW, srcW, filter)
-
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			i0 := dstY * src.Stride
-			j0 := dstY * dst.Stride
-			for dstX := 0; dstX < dstW; dstX++ {
+func resizeHorizontal(img image.Image, width int, filter ResampleFilter) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, width, src.h))
+	weights := precomputeWeights(width, src.w, filter)
+	parallel(0, src.h, func(ys <-chan int) {
+		scanLine := make([]uint8, src.w*4)
+		for y := range ys {
+			src.scan(0, y, src.w, y+1, scanLine)
+			j0 := y * dst.Stride
+			for x := 0; x < width; x++ {
 				var r, g, b, a float64
-				for _, w := range weights[dstX] {
-					i := i0 + w.index*4
-					aw := float64(src.Pix[i+3]) * w.weight
-					r += float64(src.Pix[i+0]) * aw
-					g += float64(src.Pix[i+1]) * aw
-					b += float64(src.Pix[i+2]) * aw
+				for _, w := range weights[x] {
+					i := w.index * 4
+					aw := float64(scanLine[i+3]) * w.weight
+					r += float64(scanLine[i+0]) * aw
+					g += float64(scanLine[i+1]) * aw
+					b += float64(scanLine[i+2]) * aw
 					a += aw
 				}
 				if a != 0 {
 					aInv := 1 / a
-					j := j0 + dstX*4
+					j := j0 + x*4
 					dst.Pix[j+0] = clamp(r * aInv)
 					dst.Pix[j+1] = clamp(g * aInv)
 					dst.Pix[j+2] = clamp(b * aInv)
@@ -153,37 +137,30 @@ func resizeHorizontal(src *image.NRGBA, width int, filter ResampleFilter) *image
 			}
 		}
 	})
-
 	return dst
 }
 
-func resizeVertical(src *image.NRGBA, height int, filter ResampleFilter) *image.NRGBA {
-	srcBounds := src.Bounds()
-	srcW := srcBounds.Max.X
-	srcH := srcBounds.Max.Y
-
-	dstW := srcW
-	dstH := height
-
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	weights := precomputeWeights(dstH, srcH, filter)
-
-	parallel(dstW, func(partStart, partEnd int) {
-		for dstX := partStart; dstX < partEnd; dstX++ {
-			for dstY := 0; dstY < dstH; dstY++ {
+func resizeVertical(img image.Image, height int, filter ResampleFilter) *image.NRGBA {
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, src.w, height))
+	weights := precomputeWeights(height, src.h, filter)
+	parallel(0, src.w, func(xs <-chan int) {
+		scanLine := make([]uint8, src.h*4)
+		for x := range xs {
+			src.scan(x, 0, x+1, src.h, scanLine)
+			for y := 0; y < height; y++ {
 				var r, g, b, a float64
-				for _, w := range weights[dstY] {
-					i := w.index*src.Stride + dstX*4
-					aw := float64(src.Pix[i+3]) * w.weight
-					r += float64(src.Pix[i+0]) * aw
-					g += float64(src.Pix[i+1]) * aw
-					b += float64(src.Pix[i+2]) * aw
+				for _, w := range weights[y] {
+					i := w.index * 4
+					aw := float64(scanLine[i+3]) * w.weight
+					r += float64(scanLine[i+0]) * aw
+					g += float64(scanLine[i+1]) * aw
+					b += float64(scanLine[i+2]) * aw
 					a += aw
 				}
 				if a != 0 {
 					aInv := 1 / a
-					j := dstY*dst.Stride + dstX*4
+					j := y*dst.Stride + x*4
 					dst.Pix[j+0] = clamp(r * aInv)
 					dst.Pix[j+1] = clamp(g * aInv)
 					dst.Pix[j+2] = clamp(b * aInv)
@@ -192,45 +169,47 @@ func resizeVertical(src *image.NRGBA, height int, filter ResampleFilter) *image.
 			}
 		}
 	})
-
 	return dst
 }
 
 // resizeNearest is a fast nearest-neighbor resize, no filtering.
-func resizeNearest(src *image.NRGBA, width, height int) *image.NRGBA {
-	dstW, dstH := width, height
-
-	srcBounds := src.Bounds()
-	srcW := srcBounds.Max.X
-	srcH := srcBounds.Max.Y
-
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	dx := float64(srcW) / float64(dstW)
-	dy := float64(srcH) / float64(dstH)
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			srcY := int((float64(dstY) + 0.5) * dy)
-			if srcY > srcH-1 {
-				srcY = srcH - 1
+func resizeNearest(img image.Image, width, height int) *image.NRGBA {
+	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
+	dx := float64(img.Bounds().Dx()) / float64(width)
+	dy := float64(img.Bounds().Dy()) / float64(height)
+
+	if height < img.Bounds().Dy() {
+		src := newScanner(img)
+		parallel(0, height, func(ys <-chan int) {
+			scanLine := make([]uint8, src.w*4)
+			for y := range ys {
+				srcY := int((float64(y) + 0.5) * dy)
+				src.scan(0, srcY, src.w, srcY+1, scanLine)
+				dstOff := y * dst.Stride
+				for x := 0; x < width; x++ {
+					srcX := int((float64(x) + 0.5) * dx)
+					srcOff := srcX * 4
+					copy(dst.Pix[dstOff:dstOff+4], scanLine[srcOff:srcOff+4])
+					dstOff += 4
+				}
 			}
-
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := int((float64(dstX) + 0.5) * dx)
-				if srcX > srcW-1 {
-					srcX = srcW - 1
+		})
+	} else {
+		src := toNRGBA(img)
+		parallel(0, height, func(ys <-chan int) {
+			for y := range ys {
+				srcY := int((float64(y) + 0.5) * dy)
+				srcOff0 := srcY * src.Stride
+				dstOff := y * dst.Stride
+				for x := 0; x < width; x++ {
+					srcX := int((float64(x) + 0.5) * dx)
+					srcOff := srcOff0 + srcX*4
+					copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
+					dstOff += 4
 				}
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
 			}
-		}
-
-	})
+		})
+	}
 
 	return dst
 }
@@ -344,27 +323,28 @@ func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image
 //	General filter recommendations:
 //
 //	- Lanczos
-//		Probably the best resampling filter for photographic images yielding sharp results,
-//		but it's slower than cubic filters (see below).
+//		High-quality resampling filter for photographic images yielding sharp results.
+//		It's slower than cubic filters (see below).
 //
 //	- CatmullRom
 //		A sharp cubic filter. It's a good filter for both upscaling and downscaling if sharp results are needed.
 //
 //	- MitchellNetravali
-//		A high quality cubic filter that produces smoother results with less ringing than CatmullRom.
+//		A high quality cubic filter that produces smoother results with less ringing artifacts than CatmullRom.
 //
 //	- BSpline
 //		A good filter if a very smooth output is needed.
 //
 //	- Linear
-//		Bilinear interpolation filter, produces reasonably good, smooth output. It's faster than cubic filters.
+//		Bilinear interpolation filter, produces reasonably good, smooth output.
+//		It's faster than cubic filters.
 //
 //	- Box
-//		Simple and fast resampling filter appropriate for downscaling.
+//		Simple and fast averaging filter appropriate for downscaling.
 //		When upscaling it's similar to NearestNeighbor.
 //
 //	- NearestNeighbor
-//		Fastest resample filter, no antialiasing at all. Rarely used.
+//		Fastest resampling filter, no antialiasing.
 //
 type ResampleFilter struct {
 	Support float64
@@ -417,14 +397,14 @@ var Welch ResampleFilter
 var Cosine ResampleFilter
 
 func bcspline(x, b, c float64) float64 {
+	var y float64
 	x = math.Abs(x)
 	if x < 1.0 {
-		return ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
-	}
-	if x < 2.0 {
-		return ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
+		y = ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
+	} else if x < 2.0 {
+		y = ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
 	}
-	return 0
+	return y
 }
 
 func sinc(x float64) float64 {

+ 94 - 52
resize_test.go

@@ -7,8 +7,8 @@ import (
 )
 
 func TestResize(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		w, h int
 		f    ResampleFilter
@@ -32,6 +32,47 @@ func TestResize(t *testing.T) {
 				Pix:    []uint8{0x55, 0x55, 0x55, 0xc0},
 			},
 		},
+		{
+			"Resize 2x2 1x2 box",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 1, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff,
+				},
+			},
+			1, 2,
+			Box,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 1, 2),
+				Stride: 1 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0x80,
+					0x00, 0x80, 0x80, 0xff,
+				},
+			},
+		},
+		{
+			"Resize 2x2 2x1 box",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 1, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff,
+				},
+			},
+			2, 1,
+			Box,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 2, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0x00, 0xff, 0x00, 0x80, 0x80, 0x00, 0x80, 0xff,
+				},
+			},
+		},
 		{
 			"Resize 2x2 2x2 box",
 			&image.NRGBA{
@@ -156,15 +197,18 @@ func TestResize(t *testing.T) {
 			&image.NRGBA{},
 		},
 	}
-	for _, d := range td {
-		got := Resize(d.src, d.w, d.h, d.f)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Resize(tc.src, tc.w, tc.h, tc.f)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
+}
 
-	for i, filter := range []ResampleFilter{
+func TestResampleFilters(t *testing.T) {
+	for _, filter := range []ResampleFilter{
 		NearestNeighbor,
 		Box,
 		Linear,
@@ -181,24 +225,19 @@ func TestResize(t *testing.T) {
 		Welch,
 		Cosine,
 	} {
-		src := image.NewNRGBA(image.Rect(-1, -1, 2, 3))
-		got := Resize(src, 5, 6, filter)
-		want := image.NewNRGBA(image.Rect(0, 0, 5, 6))
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [Resize all filters #%d] failed: %#v", i, got)
-		}
-
-		if filter.Kernel != nil {
-			x := filter.Kernel(filter.Support + 0.0001)
-			if x != 0 {
-				t.Errorf("test [ResampleFilter edge cases #%d] failed: %f", i, x)
+		t.Run("", func(t *testing.T) {
+			src := image.NewNRGBA(image.Rect(-1, -1, 2, 3))
+			got := Resize(src, 5, 6, filter)
+			want := image.NewNRGBA(image.Rect(0, 0, 5, 6))
+			if !compareNRGBA(got, want, 0) {
+				t.Fatalf("got result %#v want %#v", got, want)
 			}
-		}
-	}
-
-	bcs2 := bcspline(2, 1, 0)
-	if bcs2 != 0 {
-		t.Errorf("test [bcspline 2] failed: %f", bcs2)
+			if filter.Kernel != nil {
+				if x := filter.Kernel(filter.Support + 0.0001); x != 0 {
+					t.Fatalf("got kernel value %f want 0", x)
+				}
+			}
+		})
 	}
 }
 
@@ -212,17 +251,17 @@ func TestResizeGolden(t *testing.T) {
 		got := Resize(testdataBranchesPNG, 150, 0, filter)
 		want, err := Open("testdata/" + name)
 		if err != nil {
-			t.Errorf("Open: %v", err)
+			t.Fatalf("failed to open image: %v", err)
 		}
 		if !compareNRGBA(got, toNRGBA(want), 0) {
-			t.Errorf("resulting image differs from golden: %s", name)
+			t.Fatalf("resulting image differs from golden: %s", name)
 		}
 	}
 }
 
 func TestFit(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		w, h int
 		f    ResampleFilter
@@ -325,18 +364,19 @@ func TestFit(t *testing.T) {
 			&image.NRGBA{},
 		},
 	}
-	for _, d := range td {
-		got := Fit(d.src, d.w, d.h, d.f)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Fit(tc.src, tc.w, tc.h, tc.f)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
 func TestFill(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		w, h int
 		a    Anchor
@@ -499,18 +539,19 @@ func TestFill(t *testing.T) {
 			&image.NRGBA{},
 		},
 	}
-	for _, d := range td {
-		got := Fill(d.src, d.w, d.h, d.a, d.f)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Fill(tc.src, tc.w, tc.h, tc.a, tc.f)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
 func TestThumbnail(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		w, h int
 		f    ResampleFilter
@@ -579,12 +620,13 @@ func TestThumbnail(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Thumbnail(d.src, d.w, d.h, d.f)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Thumbnail(tc.src, tc.w, tc.h, tc.f)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 

+ 250 - 0
scanner.go

@@ -0,0 +1,250 @@
+package imaging
+
+import (
+	"image"
+	"image/color"
+)
+
+type scanner struct {
+	image   image.Image
+	w, h    int
+	palette []color.NRGBA
+}
+
+func newScanner(img image.Image) *scanner {
+	s := &scanner{
+		image: img,
+		w:     img.Bounds().Dx(),
+		h:     img.Bounds().Dy(),
+	}
+	if img, ok := img.(*image.Paletted); ok {
+		s.palette = make([]color.NRGBA, len(img.Palette))
+		for i := 0; i < len(img.Palette); i++ {
+			s.palette[i] = color.NRGBAModel.Convert(img.Palette[i]).(color.NRGBA)
+		}
+	}
+	return s
+}
+
+// scan scans the given rectangular region of the image into dst.
+func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) {
+	switch img := s.image.(type) {
+	case *image.NRGBA:
+		size := (x2 - x1) * 4
+		j := 0
+		i := y1*img.Stride + x1*4
+		for y := y1; y < y2; y++ {
+			copy(dst[j:j+size], img.Pix[i:i+size])
+			j += size
+			i += img.Stride
+		}
+
+	case *image.NRGBA64:
+		j := 0
+		for y := y1; y < y2; y++ {
+			i := y*img.Stride + x1*8
+			for x := x1; x < x2; x++ {
+				dst[j+0] = img.Pix[i+0]
+				dst[j+1] = img.Pix[i+2]
+				dst[j+2] = img.Pix[i+4]
+				dst[j+3] = img.Pix[i+6]
+				j += 4
+				i += 8
+			}
+		}
+
+	case *image.RGBA:
+		j := 0
+		for y := y1; y < y2; y++ {
+			i := y*img.Stride + x1*4
+			for x := x1; x < x2; x++ {
+				a := img.Pix[i+3]
+				switch a {
+				case 0:
+					dst[j+0] = 0
+					dst[j+1] = 0
+					dst[j+2] = 0
+				case 0xff:
+					dst[j+0] = img.Pix[i+0]
+					dst[j+1] = img.Pix[i+1]
+					dst[j+2] = img.Pix[i+2]
+				default:
+					r16 := uint16(img.Pix[i+0])
+					g16 := uint16(img.Pix[i+1])
+					b16 := uint16(img.Pix[i+2])
+					a16 := uint16(a)
+					dst[j+0] = uint8(r16 * 0xff / a16)
+					dst[j+1] = uint8(g16 * 0xff / a16)
+					dst[j+2] = uint8(b16 * 0xff / a16)
+				}
+				dst[j+3] = a
+				j += 4
+				i += 4
+			}
+		}
+
+	case *image.RGBA64:
+		j := 0
+		for y := y1; y < y2; y++ {
+			i := y*img.Stride + x1*8
+			for x := x1; x < x2; x++ {
+				a := img.Pix[i+6]
+				switch a {
+				case 0:
+					dst[j+0] = 0
+					dst[j+1] = 0
+					dst[j+2] = 0
+				case 0xff:
+					dst[j+0] = img.Pix[i+0]
+					dst[j+1] = img.Pix[i+2]
+					dst[j+2] = img.Pix[i+4]
+				default:
+					r32 := uint32(img.Pix[i+0])<<8 | uint32(img.Pix[i+1])
+					g32 := uint32(img.Pix[i+2])<<8 | uint32(img.Pix[i+3])
+					b32 := uint32(img.Pix[i+4])<<8 | uint32(img.Pix[i+5])
+					a32 := uint32(img.Pix[i+6])<<8 | uint32(img.Pix[i+7])
+					dst[j+0] = uint8((r32 * 0xffff / a32) >> 8)
+					dst[j+1] = uint8((g32 * 0xffff / a32) >> 8)
+					dst[j+2] = uint8((b32 * 0xffff / a32) >> 8)
+				}
+				dst[j+3] = a
+				j += 4
+				i += 8
+			}
+		}
+
+	case *image.Gray:
+		j := 0
+		for y := y1; y < y2; y++ {
+			i := y*img.Stride + x1
+			for x := x1; x < x2; x++ {
+				c := img.Pix[i]
+				dst[j+0] = c
+				dst[j+1] = c
+				dst[j+2] = c
+				dst[j+3] = 0xff
+				j += 4
+				i++
+			}
+		}
+
+	case *image.Gray16:
+		j := 0
+		for y := y1; y < y2; y++ {
+			i := y*img.Stride + x1*2
+			for x := x1; x < x2; x++ {
+				c := img.Pix[i]
+				dst[j+0] = c
+				dst[j+1] = c
+				dst[j+2] = c
+				dst[j+3] = 0xff
+				j += 4
+				i += 2
+			}
+		}
+
+	case *image.YCbCr:
+		j := 0
+		x1 += img.Rect.Min.X
+		x2 += img.Rect.Min.X
+		y1 += img.Rect.Min.Y
+		y2 += img.Rect.Min.Y
+		for y := y1; y < y2; y++ {
+			iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X)
+			for x := x1; x < x2; x++ {
+				var ic int
+				switch img.SubsampleRatio {
+				case image.YCbCrSubsampleRatio444:
+					ic = (y-img.Rect.Min.Y)*img.CStride + (x - img.Rect.Min.X)
+				case image.YCbCrSubsampleRatio422:
+					ic = (y-img.Rect.Min.Y)*img.CStride + (x/2 - img.Rect.Min.X/2)
+				case image.YCbCrSubsampleRatio420:
+					ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x/2 - img.Rect.Min.X/2)
+				case image.YCbCrSubsampleRatio440:
+					ic = (y/2-img.Rect.Min.Y/2)*img.CStride + (x - img.Rect.Min.X)
+				default:
+					ic = img.COffset(x, y)
+				}
+
+				yy := int(img.Y[iy])
+				cb := int(img.Cb[ic]) - 128
+				cr := int(img.Cr[ic]) - 128
+
+				r := (yy<<16 + 91881*cr + 1<<15) >> 16
+				if r > 0xff {
+					r = 0xff
+				} else if r < 0 {
+					r = 0
+				}
+
+				g := (yy<<16 - 22554*cb - 46802*cr + 1<<15) >> 16
+				if g > 0xff {
+					g = 0xff
+				} else if g < 0 {
+					g = 0
+				}
+
+				b := (yy<<16 + 116130*cb + 1<<15) >> 16
+				if b > 0xff {
+					b = 0xff
+				} else if b < 0 {
+					b = 0
+				}
+
+				dst[j+0] = uint8(r)
+				dst[j+1] = uint8(g)
+				dst[j+2] = uint8(b)
+				dst[j+3] = 0xff
+
+				iy++
+				j += 4
+			}
+		}
+
+	case *image.Paletted:
+		j := 0
+		for y := y1; y < y2; y++ {
+			i := y*img.Stride + x1
+			for x := x1; x < x2; x++ {
+				c := s.palette[img.Pix[i]]
+				dst[j+0] = c.R
+				dst[j+1] = c.G
+				dst[j+2] = c.B
+				dst[j+3] = c.A
+				j += 4
+				i++
+			}
+		}
+
+	default:
+		j := 0
+		b := s.image.Bounds()
+		x1 += b.Min.X
+		x2 += b.Min.X
+		y1 += b.Min.Y
+		y2 += b.Min.Y
+		for y := y1; y < y2; y++ {
+			for x := x1; x < x2; x++ {
+				r16, g16, b16, a16 := s.image.At(x, y).RGBA()
+				switch a16 {
+				case 0xffff:
+					dst[j+0] = uint8(r16 >> 8)
+					dst[j+1] = uint8(g16 >> 8)
+					dst[j+2] = uint8(b16 >> 8)
+					dst[j+3] = 0xff
+				case 0:
+					dst[j+0] = 0
+					dst[j+1] = 0
+					dst[j+2] = 0
+					dst[j+3] = 0
+				default:
+					dst[j+0] = uint8(((r16 * 0xffff) / a16) >> 8)
+					dst[j+1] = uint8(((g16 * 0xffff) / a16) >> 8)
+					dst[j+2] = uint8(((b16 * 0xffff) / a16) >> 8)
+					dst[j+3] = uint8(a16 >> 8)
+				}
+				j += 4
+			}
+		}
+	}
+}

+ 229 - 0
scanner_test.go

@@ -0,0 +1,229 @@
+package imaging
+
+import (
+	"fmt"
+	"image"
+	"image/color"
+	"image/color/palette"
+	"image/draw"
+	"testing"
+)
+
+func TestScanner(t *testing.T) {
+	rect := image.Rect(-1, -1, 15, 15)
+	colors := palette.Plan9
+	testCases := []struct {
+		name string
+		img  image.Image
+	}{
+		{
+			name: "NRGBA",
+			img:  makeNRGBAImage(rect, colors),
+		},
+		{
+			name: "NRGBA64",
+			img:  makeNRGBA64Image(rect, colors),
+		},
+		{
+			name: "RGBA",
+			img:  makeRGBAImage(rect, colors),
+		},
+		{
+			name: "RGBA64",
+			img:  makeRGBA64Image(rect, colors),
+		},
+		{
+			name: "Gray",
+			img:  makeGrayImage(rect, colors),
+		},
+		{
+			name: "Gray16",
+			img:  makeGray16Image(rect, colors),
+		},
+		{
+			name: "YCbCr-444",
+			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio444),
+		},
+		{
+			name: "YCbCr-422",
+			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio422),
+		},
+		{
+			name: "YCbCr-420",
+			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio420),
+		},
+		{
+			name: "YCbCr-440",
+			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio440),
+		},
+		{
+			name: "YCbCr-410",
+			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio410),
+		},
+		{
+			name: "YCbCr-411",
+			img:  makeYCbCrImage(rect, colors, image.YCbCrSubsampleRatio411),
+		},
+		{
+			name: "Paletted",
+			img:  makePalettedImage(rect, colors),
+		},
+		{
+			name: "Alpha",
+			img:  makeAlphaImage(rect, colors),
+		},
+		{
+			name: "Alpha16",
+			img:  makeAlpha16Image(rect, colors),
+		},
+		{
+			name: "Generic",
+			img:  makeGenericImage(rect, colors),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			r := tc.img.Bounds()
+			s := newScanner(tc.img)
+			for y := r.Min.Y; y < r.Max.Y; y++ {
+				buf := make([]byte, r.Dx()*4)
+				s.scan(0, y-r.Min.Y, r.Dx(), y+1-r.Min.Y, buf)
+				wantBuf := readRow(tc.img, y)
+				if !compareBytes(buf, wantBuf, 1) {
+					fmt.Println(tc.img)
+					t.Fatalf("scan horizontal line (y=%d): got %v want %v", y, buf, wantBuf)
+				}
+			}
+			for x := r.Min.X; x < r.Max.X; x++ {
+				buf := make([]byte, r.Dy()*4)
+				s.scan(x-r.Min.X, 0, x+1-r.Min.X, r.Dy(), buf)
+				wantBuf := readColumn(tc.img, x)
+				if !compareBytes(buf, wantBuf, 1) {
+					t.Fatalf("scan vertical line (x=%d): got %v want %v", x, buf, wantBuf)
+				}
+			}
+		})
+	}
+}
+
+func makeYCbCrImage(rect image.Rectangle, colors []color.Color, sr image.YCbCrSubsampleRatio) *image.YCbCr {
+	img := image.NewYCbCr(rect, sr)
+	j := 0
+	for y := rect.Min.Y; y < rect.Max.Y; y++ {
+		for x := rect.Min.X; x < rect.Max.X; x++ {
+			iy := img.YOffset(x, y)
+			ic := img.COffset(x, y)
+			c := color.NRGBAModel.Convert(colors[j]).(color.NRGBA)
+			img.Y[iy], img.Cb[ic], img.Cr[ic] = color.RGBToYCbCr(c.R, c.G, c.B)
+			j++
+		}
+	}
+	return img
+}
+
+func makeNRGBAImage(rect image.Rectangle, colors []color.Color) *image.NRGBA {
+	img := image.NewNRGBA(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeNRGBA64Image(rect image.Rectangle, colors []color.Color) *image.NRGBA64 {
+	img := image.NewNRGBA64(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeRGBAImage(rect image.Rectangle, colors []color.Color) *image.RGBA {
+	img := image.NewRGBA(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeRGBA64Image(rect image.Rectangle, colors []color.Color) *image.RGBA64 {
+	img := image.NewRGBA64(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeGrayImage(rect image.Rectangle, colors []color.Color) *image.Gray {
+	img := image.NewGray(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeGray16Image(rect image.Rectangle, colors []color.Color) *image.Gray16 {
+	img := image.NewGray16(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makePalettedImage(rect image.Rectangle, colors []color.Color) *image.Paletted {
+	img := image.NewPaletted(rect, colors)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeAlphaImage(rect image.Rectangle, colors []color.Color) *image.Alpha {
+	img := image.NewAlpha(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeAlpha16Image(rect image.Rectangle, colors []color.Color) *image.Alpha16 {
+	img := image.NewAlpha16(rect)
+	fillDrawImage(img, colors)
+	return img
+}
+
+func makeGenericImage(rect image.Rectangle, colors []color.Color) image.Image {
+	img := image.NewRGBA(rect)
+	fillDrawImage(img, colors)
+	type genericImage struct{ *image.RGBA }
+	return &genericImage{img}
+}
+
+func fillDrawImage(img draw.Image, colors []color.Color) {
+	colorsNRGBA := make([]color.NRGBA, len(colors))
+	for i, c := range colors {
+		nrgba := color.NRGBAModel.Convert(c).(color.NRGBA)
+		nrgba.A = uint8(i % 256)
+		colorsNRGBA[i] = nrgba
+	}
+	rect := img.Bounds()
+	i := 0
+	for y := rect.Min.Y; y < rect.Max.Y; y++ {
+		for x := rect.Min.X; x < rect.Max.X; x++ {
+			img.Set(x, y, colorsNRGBA[i])
+			i++
+		}
+	}
+}
+
+func readRow(img image.Image, y int) []uint8 {
+	row := make([]byte, img.Bounds().Dx()*4)
+	i := 0
+	for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
+		c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
+		row[i+0] = c.R
+		row[i+1] = c.G
+		row[i+2] = c.B
+		row[i+3] = c.A
+		i += 4
+	}
+	return row
+}
+
+func readColumn(img image.Image, x int) []uint8 {
+	column := make([]byte, img.Bounds().Dy()*4)
+	i := 0
+	for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
+		c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
+		column[i+0] = c.R
+		column[i+1] = c.G
+		column[i+2] = c.B
+		column[i+3] = c.A
+		i += 4
+	}
+	return column
+}

BIN
testdata/lena_128.png


BIN
testdata/lena_512.png


BIN
testdata/out_contrast_m10.png


BIN
testdata/out_contrast_p10.png


+ 72 - 61
tools.go

@@ -58,10 +58,20 @@ func anchorPt(b image.Rectangle, w, h int, anchor Anchor) image.Point {
 // Crop cuts out a rectangular region with the specified bounds
 // from the image and returns the cropped image.
 func Crop(img image.Image, rect image.Rectangle) *image.NRGBA {
-	src := toNRGBA(img)
-	srcRect := rect.Sub(img.Bounds().Min)
-	sub := src.SubImage(srcRect)
-	return Clone(sub) // New image Bounds().Min point will be (0, 0)
+	r := rect.Intersect(img.Bounds()).Sub(img.Bounds().Min)
+	if r.Empty() {
+		return &image.NRGBA{}
+	}
+	src := newScanner(img)
+	dst := image.NewNRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
+	rowSize := r.Dx() * 4
+	parallel(r.Min.Y, r.Max.Y, func(ys <-chan int) {
+		for y := range ys {
+			i := (y - r.Min.Y) * dst.Stride
+			src.scan(r.Min.X, y, r.Max.X, y+1, dst.Pix[i:i+rowSize])
+		}
+	})
+	return dst
 }
 
 // CropAnchor cuts out a rectangular region with the specified size
@@ -82,34 +92,25 @@ func CropCenter(img image.Image, width, height int) *image.NRGBA {
 
 // Paste pastes the img image to the background image at the specified position and returns the combined image.
 func Paste(background, img image.Image, pos image.Point) *image.NRGBA {
-	src := toNRGBA(img)
-	dst := Clone(background)                    // cloned image bounds start at (0, 0)
-	startPt := pos.Sub(background.Bounds().Min) // so we should translate start point
-	endPt := startPt.Add(src.Bounds().Size())
-	pasteBounds := image.Rectangle{startPt, endPt}
-
-	if dst.Bounds().Overlaps(pasteBounds) {
-		intersectBounds := dst.Bounds().Intersect(pasteBounds)
-
-		rowSize := intersectBounds.Dx() * 4
-		numRows := intersectBounds.Dy()
-
-		srcStartX := intersectBounds.Min.X - pasteBounds.Min.X
-		srcStartY := intersectBounds.Min.Y - pasteBounds.Min.Y
-
-		i0 := dst.PixOffset(intersectBounds.Min.X, intersectBounds.Min.Y)
-		j0 := src.PixOffset(srcStartX, srcStartY)
-
-		di := dst.Stride
-		dj := src.Stride
-
-		for row := 0; row < numRows; row++ {
-			copy(dst.Pix[i0:i0+rowSize], src.Pix[j0:j0+rowSize])
-			i0 += di
-			j0 += dj
-		}
+	dst := Clone(background)
+	pos = pos.Sub(background.Bounds().Min)
+	pasteRect := image.Rectangle{Min: pos, Max: pos.Add(img.Bounds().Size())}
+	interRect := pasteRect.Intersect(dst.Bounds())
+	if interRect.Empty() {
+		return dst
 	}
-
+	src := newScanner(img)
+	parallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) {
+		for y := range ys {
+			x1 := interRect.Min.X - pasteRect.Min.X
+			x2 := interRect.Max.X - pasteRect.Min.X
+			y1 := y - pasteRect.Min.Y
+			y2 := y1 + 1
+			i1 := y*dst.Stride + interRect.Min.X*4
+			i2 := i1 + interRect.Dx()*4
+			src.scan(x1, y1, x2, y2, dst.Pix[i1:i2])
+		}
+	})
 	return dst
 }
 
@@ -136,49 +137,59 @@ func PasteCenter(background, img image.Image) *image.NRGBA {
 //
 // Usage examples:
 //
-//	// draw the sprite over the background at position (50, 50)
+//	// Draw spriteImage over backgroundImage at the given position (x=50, y=50).
 //	dstImage := imaging.Overlay(backgroundImage, spriteImage, image.Pt(50, 50), 1.0)
 //
-//	// blend two opaque images of the same size
+//	// Blend two opaque images of the same size.
 //	dstImage := imaging.Overlay(imageOne, imageTwo, image.Pt(0, 0), 0.5)
 //
 func Overlay(background, img image.Image, pos image.Point, opacity float64) *image.NRGBA {
-	opacity = math.Min(math.Max(opacity, 0.0), 1.0) // check: 0.0 <= opacity <= 1.0
-
-	src := toNRGBA(img)
-	dst := Clone(background)                    // cloned image bounds start at (0, 0)
-	startPt := pos.Sub(background.Bounds().Min) // so we should translate start point
-	endPt := startPt.Add(src.Bounds().Size())
-	pasteBounds := image.Rectangle{startPt, endPt}
-
-	if dst.Bounds().Overlaps(pasteBounds) {
-		intersectBounds := dst.Bounds().Intersect(pasteBounds)
-
-		for y := intersectBounds.Min.Y; y < intersectBounds.Max.Y; y++ {
-			for x := intersectBounds.Min.X; x < intersectBounds.Max.X; x++ {
-				i := y*dst.Stride + x*4
-
-				srcX := x - pasteBounds.Min.X
-				srcY := y - pasteBounds.Min.Y
-				j := srcY*src.Stride + srcX*4
-
+	opacity = math.Min(math.Max(opacity, 0.0), 1.0) // Ensure 0.0 <= opacity <= 1.0.
+	dst := Clone(background)
+	pos = pos.Sub(background.Bounds().Min)
+	pasteRect := image.Rectangle{Min: pos, Max: pos.Add(img.Bounds().Size())}
+	interRect := pasteRect.Intersect(dst.Bounds())
+	if interRect.Empty() {
+		return dst
+	}
+	src := newScanner(img)
+	parallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) {
+		scanLine := make([]uint8, interRect.Dx()*4)
+		for y := range ys {
+			x1 := interRect.Min.X - pasteRect.Min.X
+			x2 := interRect.Max.X - pasteRect.Min.X
+			y1 := y - pasteRect.Min.Y
+			y2 := y1 + 1
+			src.scan(x1, y1, x2, y2, scanLine)
+			i := y*dst.Stride + interRect.Min.X*4
+			j := 0
+			for x := interRect.Min.X; x < interRect.Max.X; x++ {
+				r1 := float64(dst.Pix[i+0])
+				g1 := float64(dst.Pix[i+1])
+				b1 := float64(dst.Pix[i+2])
 				a1 := float64(dst.Pix[i+3])
-				a2 := float64(src.Pix[j+3])
 
-				coef2 := opacity * a2 / 255.0
-				coef1 := (1 - coef2) * a1 / 255.0
+				r2 := float64(scanLine[j+0])
+				g2 := float64(scanLine[j+1])
+				b2 := float64(scanLine[j+2])
+				a2 := float64(scanLine[j+3])
+
+				coef2 := opacity * a2 / 255
+				coef1 := (1 - coef2) * a1 / 255
 				coefSum := coef1 + coef2
 				coef1 /= coefSum
 				coef2 /= coefSum
 
-				dst.Pix[i+0] = uint8(float64(dst.Pix[i+0])*coef1 + float64(src.Pix[j+0])*coef2)
-				dst.Pix[i+1] = uint8(float64(dst.Pix[i+1])*coef1 + float64(src.Pix[j+1])*coef2)
-				dst.Pix[i+2] = uint8(float64(dst.Pix[i+2])*coef1 + float64(src.Pix[j+2])*coef2)
-				dst.Pix[i+3] = uint8(math.Min(a1+a2*opacity*(255.0-a1)/255.0, 255.0))
+				dst.Pix[i+0] = uint8(r1*coef1 + r2*coef2)
+				dst.Pix[i+1] = uint8(g1*coef1 + g2*coef2)
+				dst.Pix[i+2] = uint8(b1*coef1 + b2*coef2)
+				dst.Pix[i+3] = uint8(math.Min(a1+a2*opacity*(255-a1)/255, 255))
+
+				i += 4
+				j += 4
 			}
 		}
-	}
-
+	})
 	return dst
 }
 

+ 191 - 56
tools_test.go

@@ -6,8 +6,8 @@ import (
 )
 
 func TestCrop(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		r    image.Rectangle
 		want *image.NRGBA
@@ -33,12 +33,13 @@ func TestCrop(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Crop(d.src, d.r)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Crop(tc.src, tc.r)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -50,8 +51,8 @@ func BenchmarkCrop(b *testing.B) {
 }
 
 func TestCropCenter(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		w, h int
 		want *image.NRGBA
@@ -117,18 +118,19 @@ func TestCropCenter(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := CropCenter(d.src, d.w, d.h)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := CropCenter(tc.src, tc.w, tc.h)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
 func TestCropAnchor(t *testing.T) {
-	td := []struct {
-		desc   string
+	testCases := []struct {
+		name   string
 		src    image.Image
 		w, h   int
 		anchor Anchor
@@ -432,18 +434,19 @@ func TestCropAnchor(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := CropAnchor(d.src, d.w, d.h, d.anchor)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := CropAnchor(tc.src, tc.w, tc.h, tc.anchor)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
 func TestPaste(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src1 image.Image
 		src2 image.Image
 		p    image.Point
@@ -478,13 +481,113 @@ func TestPaste(t *testing.T) {
 				},
 			},
 		},
+		{
+			"Paste 3x4 4x3 bottom right intersection",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
+					0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b,
+				},
+			},
+			&image.NRGBA{
+				Rect:   image.Rect(1, 1, 5, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
+					0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
+					0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
+				},
+			},
+			image.Pt(0, 1),
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 4),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+					0x30, 0x31, 0x32, 0x33, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
+					0x40, 0x41, 0x42, 0x43, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
+				},
+			},
+		},
+		{
+			"Paste 3x4 4x3 top left intersection",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
+					0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b,
+				},
+			},
+			&image.NRGBA{
+				Rect:   image.Rect(1, 1, 5, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
+					0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
+					0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
+				},
+			},
+			image.Pt(-3, -2),
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 4),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0x18, 0x19, 0x1a, 0x1b,
+					0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0x28, 0x29, 0x2a, 0x2b,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
+					0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b,
+				},
+			},
+		},
+		{
+			"Paste 3x4 4x3 no intersection",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 2, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
+					0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b,
+				},
+			},
+			&image.NRGBA{
+				Rect:   image.Rect(1, 1, 5, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
+					0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
+					0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
+				},
+			},
+			image.Pt(-20, 20),
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 4),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+					0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+					0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
+					0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b,
+				},
+			},
+		},
 	}
-	for _, d := range td {
-		got := Paste(d.src1, d.src2, d.p)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Paste(tc.src1, tc.src2, tc.p)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -496,8 +599,8 @@ func BenchmarkPaste(b *testing.B) {
 }
 
 func TestPasteCenter(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src1 image.Image
 		src2 image.Image
 		want *image.NRGBA
@@ -531,18 +634,19 @@ func TestPasteCenter(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := PasteCenter(d.src1, d.src2)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := PasteCenter(tc.src1, tc.src2)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
 func TestOverlay(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src1 image.Image
 		src2 image.Image
 		p    image.Point
@@ -608,13 +712,43 @@ func TestOverlay(t *testing.T) {
 				},
 			},
 		},
+		{
+			"Overlay 2x2 2x2 0.5 no intersection",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 1, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x20, 0x20, 0x20, 0x00,
+				},
+			},
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 1, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+					0xff, 0xff, 0x00, 0xff, 0x20, 0x20, 0x20, 0xff,
+				},
+			},
+			image.Pt(-10, 10),
+			0.5,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 2, 2),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x20, 0x20, 0x20, 0x00,
+				},
+			},
+		},
 	}
-	for _, d := range td {
-		got := Overlay(d.src1, d.src2, d.p, d.a)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Overlay(tc.src1, tc.src2, tc.p, tc.a)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -626,8 +760,8 @@ func BenchmarkOverlay(b *testing.B) {
 }
 
 func TestOverlayCenter(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src1 image.Image
 		src2 image.Image
 		a    float64
@@ -663,11 +797,12 @@ func TestOverlayCenter(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := OverlayCenter(d.src1, d.src2, 0.5)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := OverlayCenter(tc.src1, tc.src2, 0.5)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }

+ 72 - 145
transform.go

@@ -8,197 +8,124 @@ import (
 
 // FlipH flips the image horizontally (from left to right) and returns the transformed image.
 func FlipH(img image.Image) *image.NRGBA {
-	src := toNRGBA(img)
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-	dstW := srcW
-	dstH := srcH
+	src := newScanner(img)
+	dstW := src.w
+	dstH := src.h
+	rowSize := dstW * 4
 	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstW - dstX - 1
-				srcY := dstY
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
-			}
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
+			i := dstY * dst.Stride
+			srcY := dstY
+			src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize])
+			reverse(dst.Pix[i : i+rowSize])
 		}
-
 	})
-
 	return dst
 }
 
 // FlipV flips the image vertically (from top to bottom) and returns the transformed image.
 func FlipV(img image.Image) *image.NRGBA {
-	src := toNRGBA(img)
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-	dstW := srcW
-	dstH := srcH
+	src := newScanner(img)
+	dstW := src.w
+	dstH := src.h
+	rowSize := dstW * 4
 	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstX
-				srcY := dstH - dstY - 1
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
-			}
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
+			i := dstY * dst.Stride
+			srcY := dstH - dstY - 1
+			src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize])
 		}
-
 	})
-
 	return dst
 }
 
 // Transpose flips the image horizontally and rotates 90 degrees counter-clockwise.
 func Transpose(img image.Image) *image.NRGBA {
-	src := toNRGBA(img)
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-	dstW := srcH
-	dstH := srcW
+	src := newScanner(img)
+	dstW := src.h
+	dstH := src.w
+	rowSize := dstW * 4
 	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstY
-				srcY := dstX
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
-			}
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
+			i := dstY * dst.Stride
+			srcX := dstY
+			src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
 		}
-
 	})
-
 	return dst
 }
 
 // Transverse flips the image vertically and rotates 90 degrees counter-clockwise.
 func Transverse(img image.Image) *image.NRGBA {
-	src := toNRGBA(img)
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-	dstW := srcH
-	dstH := srcW
+	src := newScanner(img)
+	dstW := src.h
+	dstH := src.w
+	rowSize := dstW * 4
 	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstH - dstY - 1
-				srcY := dstW - dstX - 1
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
-			}
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
+			i := dstY * dst.Stride
+			srcX := dstH - dstY - 1
+			src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
+			reverse(dst.Pix[i : i+rowSize])
 		}
-
 	})
-
 	return dst
 }
 
-// Rotate90 rotates the image 90 degrees counterclockwise and returns the transformed image.
+// Rotate90 rotates the image 90 degrees counter-clockwise and returns the transformed image.
 func Rotate90(img image.Image) *image.NRGBA {
-	src := toNRGBA(img)
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-	dstW := srcH
-	dstH := srcW
+	src := newScanner(img)
+	dstW := src.h
+	dstH := src.w
+	rowSize := dstW * 4
 	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstH - dstY - 1
-				srcY := dstX
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
-			}
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
+			i := dstY * dst.Stride
+			srcX := dstH - dstY - 1
+			src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
 		}
-
 	})
-
 	return dst
 }
 
-// Rotate180 rotates the image 180 degrees counterclockwise and returns the transformed image.
+// Rotate180 rotates the image 180 degrees counter-clockwise and returns the transformed image.
 func Rotate180(img image.Image) *image.NRGBA {
-	src := toNRGBA(img)
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-	dstW := srcW
-	dstH := srcH
+	src := newScanner(img)
+	dstW := src.w
+	dstH := src.h
+	rowSize := dstW * 4
 	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstW - dstX - 1
-				srcY := dstH - dstY - 1
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
-			}
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
+			i := dstY * dst.Stride
+			srcY := dstH - dstY - 1
+			src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize])
+			reverse(dst.Pix[i : i+rowSize])
 		}
-
 	})
-
 	return dst
 }
 
-// Rotate270 rotates the image 270 degrees counterclockwise and returns the transformed image.
+// Rotate270 rotates the image 270 degrees counter-clockwise and returns the transformed image.
 func Rotate270(img image.Image) *image.NRGBA {
-	src := toNRGBA(img)
-	srcW := src.Bounds().Max.X
-	srcH := src.Bounds().Max.Y
-	dstW := srcH
-	dstH := srcW
+	src := newScanner(img)
+	dstW := src.h
+	dstH := src.w
+	rowSize := dstW * 4
 	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	parallel(dstH, func(partStart, partEnd int) {
-
-		for dstY := partStart; dstY < partEnd; dstY++ {
-			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstY
-				srcY := dstW - dstX - 1
-
-				srcOff := srcY*src.Stride + srcX*4
-				dstOff := dstY*dst.Stride + dstX*4
-
-				copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4])
-			}
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
+			i := dstY * dst.Stride
+			srcX := dstY
+			src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize])
+			reverse(dst.Pix[i : i+rowSize])
 		}
-
 	})
-
 	return dst
 }
 
@@ -237,8 +164,8 @@ func Rotate(img image.Image, angle float64, bgColor color.Color) *image.NRGBA {
 	bgColorNRGBA := color.NRGBAModel.Convert(bgColor).(color.NRGBA)
 	sin, cos := math.Sincos(math.Pi * angle / 180)
 
-	parallel(dstH, func(partStart, partEnd int) {
-		for dstY := partStart; dstY < partEnd; dstY++ {
+	parallel(0, dstH, func(ys <-chan int) {
+		for dstY := range ys {
 			for dstX := 0; dstX < dstW; dstX++ {
 				xf, yf := rotatePoint(float64(dstX)-dstXOff, float64(dstY)-dstYOff, sin, cos)
 				xf, yf = xf+srcXOff, yf+srcYOff

+ 71 - 63
transform_test.go

@@ -7,8 +7,8 @@ import (
 )
 
 func TestFlipH(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -34,12 +34,13 @@ func TestFlipH(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := FlipH(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := FlipH(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -51,8 +52,8 @@ func BenchmarkFlipH(b *testing.B) {
 }
 
 func TestFlipV(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -78,12 +79,13 @@ func TestFlipV(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := FlipV(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := FlipV(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -95,8 +97,8 @@ func BenchmarkFlipV(b *testing.B) {
 }
 
 func TestTranspose(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -121,12 +123,13 @@ func TestTranspose(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Transpose(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Transpose(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -138,8 +141,8 @@ func BenchmarkTranspose(b *testing.B) {
 }
 
 func TestTransverse(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -164,12 +167,13 @@ func TestTransverse(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Transverse(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Transverse(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -181,8 +185,8 @@ func BenchmarkTransverse(b *testing.B) {
 }
 
 func TestRotate90(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -207,12 +211,13 @@ func TestRotate90(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Rotate90(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Rotate90(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -224,8 +229,8 @@ func BenchmarkRotate90(b *testing.B) {
 }
 
 func TestRotate180(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -251,12 +256,13 @@ func TestRotate180(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Rotate180(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Rotate180(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -268,8 +274,8 @@ func BenchmarkRotate180(b *testing.B) {
 }
 
 func TestRotate270(t *testing.T) {
-	td := []struct {
-		desc string
+	testCases := []struct {
+		name string
 		src  image.Image
 		want *image.NRGBA
 	}{
@@ -294,12 +300,13 @@ func TestRotate270(t *testing.T) {
 			},
 		},
 	}
-	for _, d := range td {
-		got := Rotate270(d.src)
-		want := d.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", d.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Rotate270(tc.src)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 
@@ -312,7 +319,7 @@ func BenchmarkRotate270(b *testing.B) {
 
 func TestRotate(t *testing.T) {
 	testCases := []struct {
-		desc  string
+		name  string
 		src   image.Image
 		angle float64
 		bg    color.Color
@@ -620,12 +627,13 @@ func TestRotate(t *testing.T) {
 			},
 		},
 	}
-	for _, test := range testCases {
-		got := Rotate(test.src, test.angle, test.bg)
-		want := test.want
-		if !compareNRGBA(got, want, 0) {
-			t.Errorf("test [%s] failed: %#v", test.desc, got)
-		}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := Rotate(tc.src, tc.angle, tc.bg)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
 	}
 }
 

+ 50 - 38
utils.go

@@ -1,53 +1,38 @@
 package imaging
 
 import (
+	"image"
 	"runtime"
 	"sync"
-	"sync/atomic"
 )
 
-// parallel starts parallel image processing based on the current GOMAXPROCS value.
-// If GOMAXPROCS = 1 it uses no parallelization.
-// If GOMAXPROCS > 1 it spawns N=GOMAXPROCS workers in separate goroutines.
-func parallel(dataSize int, fn func(partStart, partEnd int)) {
-	numGoroutines := 1
-	partSize := dataSize
-
-	numProcs := runtime.GOMAXPROCS(0)
-	if numProcs > 1 {
-		numGoroutines = numProcs
-		partSize = dataSize / (numGoroutines * 10)
-		if partSize < 1 {
-			partSize = 1
-		}
+// parallel processes the data in separate goroutines.
+func parallel(start, stop int, fn func(<-chan int)) {
+	count := stop - start
+	if count < 1 {
+		return
 	}
 
-	if numGoroutines == 1 {
-		fn(0, dataSize)
-	} else {
-		var wg sync.WaitGroup
-		wg.Add(numGoroutines)
-		idx := uint64(0)
+	procs := runtime.GOMAXPROCS(0)
+	if procs > count {
+		procs = count
+	}
 
-		for p := 0; p < numGoroutines; p++ {
-			go func() {
-				defer wg.Done()
-				for {
-					partStart := int(atomic.AddUint64(&idx, uint64(partSize))) - partSize
-					if partStart >= dataSize {
-						break
-					}
-					partEnd := partStart + partSize
-					if partEnd > dataSize {
-						partEnd = dataSize
-					}
-					fn(partStart, partEnd)
-				}
-			}()
-		}
+	c := make(chan int, count)
+	for i := start; i < stop; i++ {
+		c <- i
+	}
+	close(c)
 
-		wg.Wait()
+	var wg sync.WaitGroup
+	for i := 0; i < procs; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			fn(c)
+		}()
 	}
+	wg.Wait()
 }
 
 // absint returns the absolute value of i.
@@ -69,3 +54,30 @@ func clamp(x float64) uint8 {
 	}
 	return 0
 }
+
+func reverse(pix []uint8) {
+	if len(pix) <= 4 {
+		return
+	}
+	i := 0
+	j := len(pix) - 4
+	for i < j {
+		pix[i+0], pix[j+0] = pix[j+0], pix[i+0]
+		pix[i+1], pix[j+1] = pix[j+1], pix[i+1]
+		pix[i+2], pix[j+2] = pix[j+2], pix[i+2]
+		pix[i+3], pix[j+3] = pix[j+3], pix[i+3]
+		i += 4
+		j -= 4
+	}
+}
+
+func toNRGBA(img image.Image) *image.NRGBA {
+	if img, ok := img.(*image.NRGBA); ok {
+		return &image.NRGBA{
+			Pix:    img.Pix,
+			Stride: img.Stride,
+			Rect:   img.Rect.Sub(img.Rect.Min),
+		}
+	}
+	return Clone(img)
+}

+ 68 - 17
utils_test.go

@@ -9,7 +9,6 @@ import (
 var (
 	testdataBranchesJPG     = mustOpen("testdata/branches.jpg")
 	testdataBranchesPNG     = mustOpen("testdata/branches.png")
-	testdataFlowersPNG      = mustOpen("testdata/flowers.png")
 	testdataFlowersSmallPNG = mustOpen("testdata/flowers_small.png")
 )
 
@@ -21,12 +20,22 @@ func mustOpen(filename string) image.Image {
 	return img
 }
 
+func TestParallel(t *testing.T) {
+	for _, n := range []int{0, 1, 10, 100, 1000} {
+		for _, p := range []int{1, 2, 4, 8, 16, 100} {
+			if !testParallelN(n, p) {
+				t.Fatalf("test [parallel %d %d] failed", n, p)
+			}
+		}
+	}
+}
+
 func testParallelN(n, procs int) bool {
 	data := make([]bool, n)
 	before := runtime.GOMAXPROCS(0)
 	runtime.GOMAXPROCS(procs)
-	parallel(n, func(start, end int) {
-		for i := start; i < end; i++ {
+	parallel(0, n, func(is <-chan int) {
+		for i := range is {
 			data[i] = true
 		}
 	})
@@ -39,18 +48,8 @@ func testParallelN(n, procs int) bool {
 	return true
 }
 
-func TestParallel(t *testing.T) {
-	for _, n := range []int{0, 1, 10, 100, 1000} {
-		for _, p := range []int{1, 2, 4, 8, 16, 100} {
-			if !testParallelN(n, p) {
-				t.Errorf("test [parallel %d %d] failed", n, p)
-			}
-		}
-	}
-}
-
 func TestClamp(t *testing.T) {
-	td := []struct {
+	testCases := []struct {
 		f float64
 		u uint8
 	}{
@@ -67,9 +66,61 @@ func TestClamp(t *testing.T) {
 		{127.6, 128},
 	}
 
-	for _, d := range td {
-		if clamp(d.f) != d.u {
-			t.Errorf("test [clamp %v %v] failed: %v", d.f, d.u, clamp(d.f))
+	for _, tc := range testCases {
+		if clamp(tc.f) != tc.u {
+			t.Fatalf("test [clamp %v %v] failed: %v", tc.f, tc.u, clamp(tc.f))
 		}
 	}
 }
+
+func TestReverse(t *testing.T) {
+	testCases := []struct {
+		pix  []uint8
+		want []uint8
+	}{
+		{
+			pix:  []uint8{},
+			want: []uint8{},
+		},
+		{
+			pix:  []uint8{1, 2, 3, 4},
+			want: []uint8{1, 2, 3, 4},
+		},
+		{
+			pix:  []uint8{1, 2, 3, 4, 5, 6, 7, 8},
+			want: []uint8{5, 6, 7, 8, 1, 2, 3, 4},
+		},
+		{
+			pix:  []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
+			want: []uint8{9, 10, 11, 12, 5, 6, 7, 8, 1, 2, 3, 4},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run("", func(t *testing.T) {
+			reverse(tc.pix)
+			if !compareBytes(tc.pix, tc.want, 0) {
+				t.Fatalf("got pix %v want %v", tc.pix, tc.want)
+			}
+		})
+	}
+}
+
+func compareNRGBA(img1, img2 *image.NRGBA, delta int) bool {
+	if !img1.Rect.Eq(img2.Rect) {
+		return false
+	}
+	return compareBytes(img1.Pix, img2.Pix, delta)
+}
+
+func compareBytes(a, b []uint8, delta int) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := 0; i < len(a); i++ {
+		if absint(int(a[i])-int(b[i])) > delta {
+			return false
+		}
+	}
+	return true
+}