Pārlūkot izejas kodu

transform: add arbitrary rotation

Grigory Dryapak 8 gadi atpakaļ
vecāks
revīzija
1b5cf1a92e
2 mainītis faili ar 364 papildinājumiem un 78 dzēšanām
  1. 163 33
      transform.go
  2. 201 45
      transform_test.go

+ 163 - 33
transform.go

@@ -2,23 +2,25 @@ package imaging
 
 import (
 	"image"
+	"image/color"
+	"math"
 )
 
-// Rotate90 rotates the image 90 degrees counterclockwise and returns the transformed image.
-func Rotate90(img image.Image) *image.NRGBA {
+// 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 := srcH
-	dstH := srcW
+	dstW := srcW
+	dstH := srcH
 	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
+				srcX := dstW - dstX - 1
+				srcY := dstY
 
 				srcOff := srcY*src.Stride + srcX*4
 				dstOff := dstY*dst.Stride + dstX*4
@@ -32,8 +34,8 @@ func Rotate90(img image.Image) *image.NRGBA {
 	return dst
 }
 
-// Rotate180 rotates the image 180 degrees counterclockwise and returns the transformed image.
-func Rotate180(img image.Image) *image.NRGBA {
+// 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
@@ -45,7 +47,7 @@ func Rotate180(img image.Image) *image.NRGBA {
 
 		for dstY := partStart; dstY < partEnd; dstY++ {
 			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstW - dstX - 1
+				srcX := dstX
 				srcY := dstH - dstY - 1
 
 				srcOff := srcY*src.Stride + srcX*4
@@ -60,8 +62,8 @@ func Rotate180(img image.Image) *image.NRGBA {
 	return dst
 }
 
-// Rotate270 rotates the image 270 degrees counterclockwise and returns the transformed image.
-func Rotate270(img image.Image) *image.NRGBA {
+// 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
@@ -74,7 +76,7 @@ func Rotate270(img image.Image) *image.NRGBA {
 		for dstY := partStart; dstY < partEnd; dstY++ {
 			for dstX := 0; dstX < dstW; dstX++ {
 				srcX := dstY
-				srcY := dstW - dstX - 1
+				srcY := dstX
 
 				srcOff := srcY*src.Stride + srcX*4
 				dstOff := dstY*dst.Stride + dstX*4
@@ -88,21 +90,21 @@ func Rotate270(img image.Image) *image.NRGBA {
 	return dst
 }
 
-// FlipH flips the image horizontally (from left to right) and returns the transformed image.
-func FlipH(img image.Image) *image.NRGBA {
+// 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 := srcW
-	dstH := srcH
+	dstW := srcH
+	dstH := srcW
 	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
+				srcX := dstH - dstY - 1
+				srcY := dstW - dstX - 1
 
 				srcOff := srcY*src.Stride + srcX*4
 				dstOff := dstY*dst.Stride + dstX*4
@@ -116,21 +118,21 @@ func FlipH(img image.Image) *image.NRGBA {
 	return dst
 }
 
-// FlipV flips the image vertically (from top to bottom) and returns the transformed image.
-func FlipV(img image.Image) *image.NRGBA {
+// Rotate90 rotates the image 90 degrees counterclockwise 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 := srcW
-	dstH := srcH
+	dstW := srcH
+	dstH := srcW
 	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
+				srcX := dstH - dstY - 1
+				srcY := dstX
 
 				srcOff := srcY*src.Stride + srcX*4
 				dstOff := dstY*dst.Stride + dstX*4
@@ -144,21 +146,21 @@ func FlipV(img image.Image) *image.NRGBA {
 	return dst
 }
 
-// Transpose flips the image horizontally and rotates 90 degrees counter-clockwise.
-func Transpose(img image.Image) *image.NRGBA {
+// Rotate180 rotates the image 180 degrees counterclockwise 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 := srcH
-	dstH := srcW
+	dstW := srcW
+	dstH := srcH
 	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
+				srcX := dstW - dstX - 1
+				srcY := dstH - dstY - 1
 
 				srcOff := srcY*src.Stride + srcX*4
 				dstOff := dstY*dst.Stride + dstX*4
@@ -172,8 +174,8 @@ func Transpose(img image.Image) *image.NRGBA {
 	return dst
 }
 
-// Transverse flips the image vertically and rotates 90 degrees counter-clockwise.
-func Transverse(img image.Image) *image.NRGBA {
+// Rotate270 rotates the image 270 degrees counterclockwise 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
@@ -185,7 +187,7 @@ func Transverse(img image.Image) *image.NRGBA {
 
 		for dstY := partStart; dstY < partEnd; dstY++ {
 			for dstX := 0; dstX < dstW; dstX++ {
-				srcX := dstH - dstY - 1
+				srcX := dstY
 				srcY := dstW - dstX - 1
 
 				srcOff := srcY*src.Stride + srcX*4
@@ -199,3 +201,131 @@ func Transverse(img image.Image) *image.NRGBA {
 
 	return dst
 }
+
+// Rotate rotates an image by the given angle counter-clockwise .
+// The angle parameter is the rotation angle in degrees.
+// The bgColor parameter specifies the color of the uncovered zone after the rotation.
+func Rotate(img image.Image, angle float64, bgColor color.Color) *image.NRGBA {
+	src := toNRGBA(img)
+	srcW := src.Bounds().Max.X
+	srcH := src.Bounds().Max.Y
+	dstW, dstH := rotatedSize(srcW, srcH, angle)
+	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
+
+	if dstW <= 0 || dstH <= 0 {
+		return dst
+	}
+
+	srcXOff := float64(srcW)/2 - 0.5
+	srcYOff := float64(srcH)/2 - 0.5
+	dstXOff := float64(dstW)/2 - 0.5
+	dstYOff := float64(dstH)/2 - 0.5
+
+	bgColorNRGBA := color.NRGBAModel.Convert(bgColor).(color.NRGBA)
+	sin, cos := math.Sincos(math.Pi * float64(angle) / 180)
+
+	parallel(dstH, func(partStart, partEnd int) {
+		for dstY := partStart; dstY < partEnd; dstY++ {
+			for dstX := 0; dstX < dstW; dstX++ {
+				xf, yf := rotatePoint(float64(dstX)-dstXOff, float64(dstY)-dstYOff, sin, cos)
+				xf, yf = xf+srcXOff, yf+srcYOff
+				interpolatePoint(dst, dstX, dstY, src, xf, yf, bgColorNRGBA)
+			}
+		}
+	})
+
+	return dst
+}
+
+func rotatePoint(x, y, sin, cos float64) (float64, float64) {
+	return x*cos - y*sin, x*sin + y*cos
+}
+
+func rotatedSize(w, h int, angle float64) (int, int) {
+	if w <= 0 || h <= 0 {
+		return 0, 0
+	}
+
+	sin, cos := math.Sincos(math.Pi * float64(angle) / 180)
+	x1, y1 := rotatePoint(float64(w-1), 0, sin, cos)
+	x2, y2 := rotatePoint(float64(w-1), float64(h-1), sin, cos)
+	x3, y3 := rotatePoint(0, float64(h-1), sin, cos)
+
+	minx := math.Min(x1, math.Min(x2, math.Min(x3, 0)))
+	maxx := math.Max(x1, math.Max(x2, math.Max(x3, 0)))
+	miny := math.Min(y1, math.Min(y2, math.Min(y3, 0)))
+	maxy := math.Max(y1, math.Max(y2, math.Max(y3, 0)))
+
+	neww := maxx - minx + 1
+	if neww-math.Floor(neww) > 0.1 {
+		neww++
+	}
+	newh := maxy - miny + 1
+	if newh-math.Floor(newh) > 0.1 {
+		newh++
+	}
+
+	return int(neww), int(newh)
+}
+
+func interpolatePoint(dst *image.NRGBA, dstX, dstY int, src *image.NRGBA, xf, yf float64, bgColor color.NRGBA) {
+	dstIndex := dstY*dst.Stride + dstX*4
+
+	x0 := int(math.Floor(xf))
+	y0 := int(math.Floor(yf))
+	bounds := src.Bounds()
+	if !image.Pt(x0, y0).In(image.Rect(bounds.Min.X-1, bounds.Min.Y-1, bounds.Max.X, bounds.Max.Y)) {
+		dst.Pix[dstIndex+0] = bgColor.R
+		dst.Pix[dstIndex+1] = bgColor.G
+		dst.Pix[dstIndex+2] = bgColor.B
+		dst.Pix[dstIndex+3] = bgColor.A
+		return
+	}
+
+	xq := xf - float64(x0)
+	yq := yf - float64(y0)
+
+	var pxs [4]color.NRGBA
+	var cfs [4]float64
+
+	for i := 0; i < 2; i++ {
+		for j := 0; j < 2; j++ {
+			k := i*2 + j
+			pt := image.Pt(x0+j, y0+i)
+			if pt.In(bounds) {
+				l := pt.Y*src.Stride + pt.X*4
+				pxs[k].R = src.Pix[l+0]
+				pxs[k].G = src.Pix[l+1]
+				pxs[k].B = src.Pix[l+2]
+				pxs[k].A = src.Pix[l+3]
+			} else {
+				pxs[k] = bgColor
+			}
+		}
+	}
+
+	cfs[0] = (1 - xq) * (1 - yq)
+	cfs[1] = xq * (1 - yq)
+	cfs[2] = (1 - xq) * yq
+	cfs[3] = xq * yq
+
+	var r, g, b, a float64
+	for i := range pxs {
+		wa := float64(pxs[i].A) * cfs[i]
+		r += float64(pxs[i].R) * wa
+		g += float64(pxs[i].G) * wa
+		b += float64(pxs[i].B) * wa
+		a += wa
+	}
+
+	if a != 0 {
+		r /= a
+		g /= a
+		b /= a
+	}
+
+	dst.Pix[dstIndex+0] = clamp(r)
+	dst.Pix[dstIndex+1] = clamp(g)
+	dst.Pix[dstIndex+2] = clamp(b)
+	dst.Pix[dstIndex+3] = clamp(a)
+}

+ 201 - 45
transform_test.go

@@ -2,17 +2,18 @@ package imaging
 
 import (
 	"image"
+	"image/color"
 	"testing"
 )
 
-func TestRotate90(t *testing.T) {
+func TestFlipH(t *testing.T) {
 	td := []struct {
 		desc string
 		src  image.Image
 		want *image.NRGBA
 	}{
 		{
-			"Rotate90 2x3",
+			"FlipH 2x3",
 			&image.NRGBA{
 				Rect:   image.Rect(-1, -1, 1, 2),
 				Stride: 2 * 4,
@@ -23,17 +24,18 @@ func TestRotate90(t *testing.T) {
 				},
 			},
 			&image.NRGBA{
-				Rect:   image.Rect(0, 0, 3, 2),
-				Stride: 3 * 4,
+				Rect:   image.Rect(0, 0, 2, 3),
+				Stride: 2 * 4,
 				Pix: []uint8{
-					0xcc, 0xdd, 0xee, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
-					0x00, 0x11, 0x22, 0x33, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00,
+					0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33,
+					0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00,
 				},
 			},
 		},
 	}
 	for _, d := range td {
-		got := Rotate90(d.src)
+		got := FlipH(d.src)
 		want := d.want
 		if !compareNRGBA(got, want, 0) {
 			t.Errorf("test [%s] failed: %#v", d.desc, got)
@@ -41,14 +43,14 @@ func TestRotate90(t *testing.T) {
 	}
 }
 
-func TestRotate180(t *testing.T) {
+func TestFlipV(t *testing.T) {
 	td := []struct {
 		desc string
 		src  image.Image
 		want *image.NRGBA
 	}{
 		{
-			"Rotate180 2x3",
+			"FlipV 2x3",
 			&image.NRGBA{
 				Rect:   image.Rect(-1, -1, 1, 2),
 				Stride: 2 * 4,
@@ -62,15 +64,15 @@ func TestRotate180(t *testing.T) {
 				Rect:   image.Rect(0, 0, 2, 3),
 				Stride: 2 * 4,
 				Pix: []uint8{
-					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00,
-					0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
-					0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33,
+					0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff,
+					0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00,
+					0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff,
 				},
 			},
 		},
 	}
 	for _, d := range td {
-		got := Rotate180(d.src)
+		got := FlipV(d.src)
 		want := d.want
 		if !compareNRGBA(got, want, 0) {
 			t.Errorf("test [%s] failed: %#v", d.desc, got)
@@ -78,14 +80,14 @@ func TestRotate180(t *testing.T) {
 	}
 }
 
-func TestRotate270(t *testing.T) {
+func TestTranspose(t *testing.T) {
 	td := []struct {
 		desc string
 		src  image.Image
 		want *image.NRGBA
 	}{
 		{
-			"Rotate270 2x3",
+			"Transpose 2x3",
 			&image.NRGBA{
 				Rect:   image.Rect(-1, -1, 1, 2),
 				Stride: 2 * 4,
@@ -99,14 +101,14 @@ func TestRotate270(t *testing.T) {
 				Rect:   image.Rect(0, 0, 3, 2),
 				Stride: 3 * 4,
 				Pix: []uint8{
-					0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33,
-					0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xcc, 0xdd, 0xee, 0xff,
+					0x00, 0x11, 0x22, 0x33, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00,
+					0xcc, 0xdd, 0xee, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
 				},
 			},
 		},
 	}
 	for _, d := range td {
-		got := Rotate270(d.src)
+		got := Transpose(d.src)
 		want := d.want
 		if !compareNRGBA(got, want, 0) {
 			t.Errorf("test [%s] failed: %#v", d.desc, got)
@@ -114,14 +116,14 @@ func TestRotate270(t *testing.T) {
 	}
 }
 
-func TestFlipV(t *testing.T) {
+func TestTransverse(t *testing.T) {
 	td := []struct {
 		desc string
 		src  image.Image
 		want *image.NRGBA
 	}{
 		{
-			"FlipV 2x3",
+			"Transverse 2x3",
 			&image.NRGBA{
 				Rect:   image.Rect(-1, -1, 1, 2),
 				Stride: 2 * 4,
@@ -132,18 +134,17 @@ func TestFlipV(t *testing.T) {
 				},
 			},
 			&image.NRGBA{
-				Rect:   image.Rect(0, 0, 2, 3),
-				Stride: 2 * 4,
+				Rect:   image.Rect(0, 0, 3, 2),
+				Stride: 3 * 4,
 				Pix: []uint8{
-					0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff,
-					0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00,
-					0x00, 0x11, 0x22, 0x33, 0xcc, 0xdd, 0xee, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xcc, 0xdd, 0xee, 0xff,
+					0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33,
 				},
 			},
 		},
 	}
 	for _, d := range td {
-		got := FlipV(d.src)
+		got := Transverse(d.src)
 		want := d.want
 		if !compareNRGBA(got, want, 0) {
 			t.Errorf("test [%s] failed: %#v", d.desc, got)
@@ -151,14 +152,14 @@ func TestFlipV(t *testing.T) {
 	}
 }
 
-func TestFlipH(t *testing.T) {
+func TestRotate90(t *testing.T) {
 	td := []struct {
 		desc string
 		src  image.Image
 		want *image.NRGBA
 	}{
 		{
-			"FlipH 2x3",
+			"Rotate90 2x3",
 			&image.NRGBA{
 				Rect:   image.Rect(-1, -1, 1, 2),
 				Stride: 2 * 4,
@@ -169,18 +170,17 @@ func TestFlipH(t *testing.T) {
 				},
 			},
 			&image.NRGBA{
-				Rect:   image.Rect(0, 0, 2, 3),
-				Stride: 2 * 4,
+				Rect:   image.Rect(0, 0, 3, 2),
+				Stride: 3 * 4,
 				Pix: []uint8{
-					0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33,
-					0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
-					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00,
+					0xcc, 0xdd, 0xee, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+					0x00, 0x11, 0x22, 0x33, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00,
 				},
 			},
 		},
 	}
 	for _, d := range td {
-		got := FlipH(d.src)
+		got := Rotate90(d.src)
 		want := d.want
 		if !compareNRGBA(got, want, 0) {
 			t.Errorf("test [%s] failed: %#v", d.desc, got)
@@ -188,14 +188,14 @@ func TestFlipH(t *testing.T) {
 	}
 }
 
-func TestTranspose(t *testing.T) {
+func TestRotate180(t *testing.T) {
 	td := []struct {
 		desc string
 		src  image.Image
 		want *image.NRGBA
 	}{
 		{
-			"Transpose 2x3",
+			"Rotate180 2x3",
 			&image.NRGBA{
 				Rect:   image.Rect(-1, -1, 1, 2),
 				Stride: 2 * 4,
@@ -206,17 +206,18 @@ func TestTranspose(t *testing.T) {
 				},
 			},
 			&image.NRGBA{
-				Rect:   image.Rect(0, 0, 3, 2),
-				Stride: 3 * 4,
+				Rect:   image.Rect(0, 0, 2, 3),
+				Stride: 2 * 4,
 				Pix: []uint8{
-					0x00, 0x11, 0x22, 0x33, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00,
-					0xcc, 0xdd, 0xee, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00,
+					0x00, 0xff, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00,
+					0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33,
 				},
 			},
 		},
 	}
 	for _, d := range td {
-		got := Transpose(d.src)
+		got := Rotate180(d.src)
 		want := d.want
 		if !compareNRGBA(got, want, 0) {
 			t.Errorf("test [%s] failed: %#v", d.desc, got)
@@ -224,14 +225,14 @@ func TestTranspose(t *testing.T) {
 	}
 }
 
-func TestTransverse(t *testing.T) {
+func TestRotate270(t *testing.T) {
 	td := []struct {
 		desc string
 		src  image.Image
 		want *image.NRGBA
 	}{
 		{
-			"Transverse 2x3",
+			"Rotate270 2x3",
 			&image.NRGBA{
 				Rect:   image.Rect(-1, -1, 1, 2),
 				Stride: 2 * 4,
@@ -245,17 +246,172 @@ func TestTransverse(t *testing.T) {
 				Rect:   image.Rect(0, 0, 3, 2),
 				Stride: 3 * 4,
 				Pix: []uint8{
-					0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xcc, 0xdd, 0xee, 0xff,
 					0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xcc, 0xdd, 0xee, 0xff,
 				},
 			},
 		},
 	}
 	for _, d := range td {
-		got := Transverse(d.src)
+		got := Rotate270(d.src)
 		want := d.want
 		if !compareNRGBA(got, want, 0) {
 			t.Errorf("test [%s] failed: %#v", d.desc, got)
 		}
 	}
 }
+
+func TestRotate(t *testing.T) {
+	testCases := []struct {
+		desc  string
+		src   image.Image
+		angle float64
+		bg    color.Color
+		want  *image.NRGBA
+	}{
+		{
+			"Rotate 0",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+					0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			0,
+			color.Black,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+					0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"Rotate 90",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+					0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			90,
+			color.Black,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+					0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+					0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+					0xff, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"Rotate 180",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+					0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			180,
+			color.Black,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 4, 4),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+				},
+			},
+		},
+		{
+			"Rotate 45",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 3, 3),
+				Stride: 4 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff, 0x00, 0xff,
+					0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff,
+					0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+			45,
+			color.Black,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 6, 6),
+				Stride: 6 * 4,
+				Pix: []uint8{
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x61, 0x00, 0x00, 0xff, 0x58, 0x08, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x61, 0x00, 0x00, 0xff, 0xe9, 0x16, 0x00, 0xff, 0x35, 0xca, 0x00, 0xff, 0x00, 0x30, 0x30, 0xff, 0x00, 0x00, 0x00, 0xff,
+					0x61, 0x00, 0x00, 0xff, 0xe9, 0x16, 0x00, 0xff, 0x35, 0xca, 0x00, 0xff, 0x00, 0x80, 0x80, 0xff, 0x35, 0x35, 0xff, 0xff, 0x58, 0x58, 0x61, 0xff,
+					0x58, 0x08, 0x00, 0xff, 0x35, 0xca, 0x00, 0xff, 0x00, 0x80, 0x80, 0xff, 0x35, 0x35, 0xff, 0xff, 0xe9, 0xe9, 0xff, 0xff, 0x61, 0x61, 0x61, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x30, 0x30, 0xff, 0x35, 0x35, 0xff, 0xff, 0xe9, 0xe9, 0xff, 0xff, 0x61, 0x61, 0x61, 0xff, 0x00, 0x00, 0x00, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x58, 0x58, 0x61, 0xff, 0x61, 0x61, 0x61, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff,
+				},
+			},
+		},
+		{
+			"Rotate 0x0",
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 0, 0),
+				Stride: 0,
+				Pix:    []uint8{},
+			},
+			123,
+			color.Black,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 0, 0),
+				Stride: 0,
+				Pix:    []uint8{},
+			},
+		},
+		{
+			"Rotate -90",
+			&image.NRGBA{
+				Rect:   image.Rect(-1, -1, 0, 1),
+				Stride: 1 * 4,
+				Pix: []uint8{
+					0xff, 0x00, 0x00, 0xff,
+					0x00, 0xff, 0x00, 0xff,
+				},
+			},
+			-90,
+			color.Black,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 2, 1),
+				Stride: 2 * 4,
+				Pix: []uint8{
+					0x00, 0xff, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff,
+				},
+			},
+		},
+	}
+	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)
+		}
+	}
+}