Browse Source

reorganization, optimization, parallelization, blur, sharpen

disintegration 11 năm trước cách đây
mục cha
commit
6429426f30
14 tập tin đã thay đổi với 1412 bổ sung946 xóa
  1. 213 35
      README.md
  2. 153 0
      blur.go
  3. 172 0
      clone.go
  4. 36 0
      crop.go
  5. 60 0
      doc.go
  6. 99 0
      helpers.go
  7. 0 911
      imaging.go
  8. 58 0
      overlay.go
  9. 54 0
      parallel.go
  10. 55 0
      paste.go
  11. 321 0
      resample.go
  12. 0 0
      resample_filters.go
  13. 46 0
      sharpen.go
  14. 145 0
      transform.go

+ 213 - 35
README.md

@@ -1,71 +1,249 @@
 # Imaging
 
-Package imaging provides basic image manipulation functions 
-(resize, rotate, flip, crop, etc.) as well as simplified image loading and saving.
-This package is based on the standard Go image package. All the image 
-manipulation functions provided by the package take any image type that 
-implements `image.Image` interface, and return a new image of 
-`*image.NRGBA` type (32 bit RGBA colors, not premultiplied by alpha). 
+Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.). 
+This package is based on the standard Go image package and works best along with it. 
 
 ###Installation
 
-    go get github.com/disintegration/imaging
+    go get -u github.com/disintegration/imaging
     
 ### Documentation
 
 http://godoc.org/github.com/disintegration/imaging
+
+###Overview
+
+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).
+
+Note: some of examples below require importing standard `image` or `image/color` packages.
+
+##### Parallelization
+
+Imaging package uses parallel goroutines for faster image processing.
+To achieve maximum performance, make sure to allow Go to utilize all CPU cores. Use standard `runtime` package:
+```go 
+runtime.GOMAXPROCS(runtime.NumCPU())
+```
+
+##### Resize
+
+There are three image resizing functions in the package: `Resize`, `Fit` and `Thumbnail`.
+
+`Resize` resizes the image to the specified width and height using the specified resample
+filter and returns the transformed image. If one of width or height is 0, the image aspect
+ratio is preserved.
+
+`Fit` scales down the image using the specified resample filter to fit the specified
+maximum width and height and returns the transformed image. The image aspect
+ratio is preserved.
+
+`Thumbnail` scales the image up or down using the specified resample filter, crops it
+to the specified width and hight and returns the transformed image.
+
+All three resizing function take `ResampleFilter` as the last argument.
+A complete list of supported filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+CatmullRom (cubic filter) and Lanczos are recommended for high quality general purpose image resizing. 
+NearestNeighbor is the fastest one but applies no anti-aliasing.
+
+Examples:
+```go
+// resize srcImage to width = 800px preserving the aspect ratio
+dstImage := imaging.Resize(srcImage, 800, 0, imaging.Lanczos)
+
+// scale down srcImage to fit the 800x600px bounding box
+dstImage = imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
+
+// make a 100x100px thumbnail from srcImage
+dstImage = imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
+```
+
+##### Rotate & flip
     
-### Usage
+Imaging package implements functions to rotate an image 90, 180 or 270 degrees (counter-clockwise)
+and to flip an image horizontally or vertically.
+
+Examples:
+```go
+dstImage = imaging.Rotate90(srcImage)  // rotate 90 degrees counter-clockwise
+dstImage = imaging.Rotate180(srcImage) // rotate 180 degrees counter-clockwise
+dstImage = imaging.Rotate270(srcImage) // rotate 270 degrees counter-clockwise
+dstImage = imaging.FlipH(srcImage)     // flip horizontally (from left to right)
+dstImage = imaging.FlipV(srcImage)     // flip vertically (from top to bottom)
+```
+
+##### Crop, Paste, Overlay
+
+`Crop` cuts out a rectangular region with the specified bounds from the image and returns 
+the cropped image. 
+
+`CropCenter` cuts out a rectangular region with the specified size from the center of the image
+and returns the cropped image.
+
+`Paste` pastes one image into another at the specified position and returns the combined image.
+
+`PasteCenter` pastes one image to the center of another image and returns the combined image.
+
+`Overlay` draws one image over another image at the specified position with the specified opacity 
+and returns the combined image. Opacity parameter must be from 0.0 (fully transparent) to 1.0 (opaque).
+
+Examples:
+```go
+// cut out a rectangular region from the image
+dstImage = imaging.Crop(srcImage, image.Rect(50, 50, 100, 100)) 
+
+// cut out a 100x100 px region from the center of the image
+dstImage = imaging.CropCenter(srcImage, 100, 100)   
+
+// paste the srcImage to the backgroundImage at the (50, 50) position
+dstImage = imaging.Paste(backgroundImage, srcImage, image.Pt(50, 50))     
+
+// paste the srcImage to the center of the backgroundImage
+dstImage = imaging.PasteCenter(backgroundImage, srcImage)                   
+
+// draw the srcImage over the backgroundImage at the (50, 50) position with opacity=0.5
+dstImage = imaging.Overlay(backgroundImage, srcImage, image.Pt(50, 50), 0.5)
+```
+##### Blur and Sharpen
+
+`Blur` produces a blurred version of the image.
+
+`Sharpen` produces a sharpened version of the image.
+
+Both functions take the `sigma` argument that is used in a Gaussian function. 
+Sigma must be a positive floating point value indicating how much the image 
+will be blurred or sharpened and how many neighbours of each pixel will be affected.
+
+Examples:
+```go
+dstImage = imaging.Blur(srcImage, 4.5)
+dstImage = imaging.Sharpen(srcImage, 3.0)
+```
+
+##### Load, Save, New, Clone
+
+Imaging package provides useful shortcuts for image loading, saving, creation and copying.
+
+Examples:
+```go
+// load an image from file
+img, err := imaging.Open("src.png") 
+if err != nil {
+    panic(err)
+}
+
+// save the image to file
+err = imaging.Save(img, "dst.jpg") 
+if err != nil {
+    panic(err)
+}
+
+// create a new 800x600px image filled with red color
+newImg := imaging.New(800, 600, color.NRGBA{255, 0, 0, 255})
+
+// make a copy of the image
+copiedImg := imaging.Clone(img)
+```
+
+Open and Save functions support JPEG and PNG images. 
+External libraries can be used to work with other image formats.
+The example of TIFF image resizing:
 
 ```go
 package main
 
 import (
-    "github.com/disintegration/imaging"
     "image"
-    "image/color"
+    "os"
+    "runtime"
+
+    // package to load and save tiff images
+    "code.google.com/p/go.image/tiff"
+    
+    "github.com/disintegration/imaging"
 )
 
 func main() {
-    src, err := imaging.Open("src.png") // load an image from file (returns image.Image interface)
+    // use all CPU cores for maximum performance
+    runtime.GOMAXPROCS(runtime.NumCPU())
+
+    // Load TIFF image
+    file, err := os.Open("src.tif")
     if err != nil {
         panic(err)
     }
+    defer file.Close()
 
-    var dst *image.NRGBA
+    srcImage, _, err := image.Decode(file)
+    if err != nil {
+        panic(err)
+    }
 
-    dst = imaging.New(800, 600, color.NRGBA{255, 0, 0, 255}) // create a new 800x600px image filled with red color
-    dst = imaging.Clone(src)                                 // make a copy of the image
+    // Resize the image
+    resizedImg := imaging.Resize(srcImage, 800, 0, imaging.Lanczos)
+
+    // Save to file
+    outfile, err := os.Create("dst.tif")
+    if err != nil {
+        panic(err)
+    }
+    defer outfile.Close()
+
+    err = tiff.Encode(outfile, resizedImg, nil)
+    if err != nil {
+        panic(err)
+    }
+}
+```
 
-    dst = imaging.Rotate90(src)  // rotate 90 degrees counterclockwiseclockwise
-    dst = imaging.Rotate180(src) // rotate 180 degrees counterclockwiseclockwise
-    dst = imaging.Rotate270(src) // rotate 270 degrees counterclockwiseclockwise
 
-    dst = imaging.FlipH(src) // flip horizontally (from left to right)
-    dst = imaging.FlipV(src) // flip vertically (from top to bottom)
+### Code example
+Here is the complete example that loades several images, makes thumbnails of them
+and joins them together.
 
-    // Resize, Fit and Thumbnail functions take resampling filter as 4th argument.
-    // Supported filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
-    // CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+```go
+package main
 
-    dst = imaging.Resize(src, 600, 400, imaging.CatmullRom) // resize to 600x400 px using CatmullRom cubic filter
-    dst = imaging.Resize(src, 600, 0, imaging.CatmullRom)   // resize to width = 600, preserve the image aspect ratio
-    dst = imaging.Resize(src, 0, 400, imaging.CatmullRom)   // resize to height = 400, preserve the image aspect ratio
+import (
+    "image"
+    "image/color"
+    "runtime"
+    
+    "github.com/disintegration/imaging"
+)
 
-    dst = imaging.Fit(src, 800, 600, imaging.CatmullRom)       // scale down the image to fit the given maximum width and height
-    dst = imaging.Thumbnail(src, 100, 100, imaging.CatmullRom) // resize and crop the image to make a 100x100 thumbnail
+func main() {
+    // use all CPU cores for maximum performance
+    runtime.GOMAXPROCS(runtime.NumCPU())
+
+    // input files
+    files := []string{"01.jpg", "02.jpg", "03.jpg"}
+
+    // load images and make 100x100 thumbnails of them
+    var thumbnails []image.Image
+    for _, file := range files {
+        img, err := imaging.Open(file)
+        if err != nil {
+            panic(err)
+        }
+        thumb := imaging.Thumbnail(img, 100, 100, imaging.CatmullRom)
+        thumbnails = append(thumbnails, thumb)
+    }
 
-    dst = imaging.Crop(src, image.Rect(50, 50, 100, 100)) // cut out a rectangular region from the image
-    dst = imaging.CropCenter(src, 200, 100)               // cut out a 200x100 px region from the center of the image
-    dst = imaging.Paste(dst, src, image.Pt(50, 50))       // paste the src image to the dst image at the given position
-    dst = imaging.PasteCenter(dst, src)                   // paste the src image to the center of the dst image
+    // create a new blank image
+    dst := imaging.New(100*len(thumbnails), 100, color.NRGBA{0, 0, 0, 0})
 
-    // draw one image over another at the given position and with the given opacity (from 0.0 to 1.0)
-    dst = imaging.Overlay(dst, src, image.Pt(50, 30), 1.0)
+    // paste thumbnails into the new image side by side
+    for i, thumb := range thumbnails {
+        dst = imaging.Paste(dst, thumb, image.Pt(i*100, 0))
+    }
 
-    err = imaging.Save(dst, "dst.jpg") // save the image to file
+    // save the combined image to file
+    err := imaging.Save(dst, "dst.jpg")
     if err != nil {
         panic(err)
     }
 }
-```
+```

+ 153 - 0
blur.go

@@ -0,0 +1,153 @@
+package imaging
+
+import (
+	"errors"
+	"image"
+	"math"
+)
+
+func gaussianBlurKernel(x, sigma float64) float64 {
+	return math.Exp(-(x*x)/(2*sigma*sigma)) / (sigma * math.Sqrt(2*math.Pi))
+}
+
+func absint(i int) int {
+	if i < 0 {
+		return -i
+	}
+	return i
+}
+
+// Blur produces a blurred version of the image using a Gaussian function.
+// Sigma parameter must be positive and indicates how much the image will be blurred.
+//
+// Usage example:
+//
+//		dstImage := imaging.Blur(srcImage, 3.5)
+//
+func Blur(img image.Image, sigma float64) *image.NRGBA {
+	if sigma <= 0 {
+		errors.New("sigma parameter must be positive")
+	}
+
+	src := toNRGBA(img)
+	radius := int(math.Ceil(sigma * 2.0))
+	kernel := make([]float64, radius+1)
+
+	for i := 0; i <= radius; i++ {
+		kernel[i] = gaussianBlurKernel(float64(i), sigma)
+	}
+
+	var dst *image.NRGBA
+	dst = blurHorizontal(src, kernel)
+	dst = blurVertical(dst, kernel)
+
+	return dst
+}
+
+func blurHorizontal(src *image.NRGBA, kernel []float64) *image.NRGBA {
+	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)]
+			}
+
+			for y := 0; y < height; y++ {
+
+				r, g, b, a := 0.0, 0.0, 0.0, 0.0
+				for ix := start; ix <= end; ix++ {
+					weight := kernel[absint(x-ix)]
+					i := y*src.Stride + ix*4
+					r += float64(src.Pix[i+0]) * weight
+					g += float64(src.Pix[i+1]) * weight
+					b += float64(src.Pix[i+2]) * weight
+					a += float64(src.Pix[i+3]) * weight
+				}
+
+				r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
+				g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
+				b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
+				a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
+
+				j := y*dst.Stride + x*4
+				dst.Pix[j+0] = uint8(r + 0.5)
+				dst.Pix[j+1] = uint8(g + 0.5)
+				dst.Pix[j+2] = uint8(b + 0.5)
+				dst.Pix[j+3] = uint8(a + 0.5)
+
+			}
+		}
+	})
+
+	return dst
+}
+
+func blurVertical(src *image.NRGBA, kernel []float64) *image.NRGBA {
+	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)]
+			}
+
+			for x := 0; x < width; x++ {
+
+				r, g, b, a := 0.0, 0.0, 0.0, 0.0
+				for iy := start; iy <= end; iy++ {
+					weight := kernel[absint(y-iy)]
+					i := iy*src.Stride + x*4
+					r += float64(src.Pix[i+0]) * weight
+					g += float64(src.Pix[i+1]) * weight
+					b += float64(src.Pix[i+2]) * weight
+					a += float64(src.Pix[i+3]) * weight
+				}
+
+				r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
+				g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
+				b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
+				a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
+
+				j := y*dst.Stride + x*4
+				dst.Pix[j+0] = uint8(r + 0.5)
+				dst.Pix[j+1] = uint8(g + 0.5)
+				dst.Pix[j+2] = uint8(b + 0.5)
+				dst.Pix[j+3] = uint8(a + 0.5)
+
+			}
+		}
+	})
+
+	return dst
+}

+ 172 - 0
clone.go

@@ -0,0 +1,172 @@
+package imaging
+
+import (
+	"image"
+	"image/color"
+)
+
+// Clone returns a copy of the given image.
+func Clone(img image.Image) *image.NRGBA {
+	srcBounds := img.Bounds()
+	dstBounds := srcBounds.Sub(srcBounds.Min)
+
+	dst := image.NewNRGBA(dstBounds)
+
+	dstMinX := dstBounds.Min.X
+	dstMinY := dstBounds.Min.Y
+
+	srcMinX := srcBounds.Min.X
+	srcMinY := srcBounds.Min.Y
+	srcMaxX := srcBounds.Max.X
+	srcMaxY := srcBounds.Max.Y
+
+	switch src0 := img.(type) {
+
+	case *image.NRGBA:
+		rowSize := srcBounds.Dx() * 4
+		numRows := srcBounds.Dy()
+
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		j0 := src0.PixOffset(srcMinX, srcMinY)
+
+		di := dst.Stride
+		dj := src0.Stride
+
+		for row := 0; row < numRows; row++ {
+			copy(dst.Pix[i0:i0+rowSize], src0.Pix[j0:j0+rowSize])
+			i0 += di
+			j0 += dj
+		}
+
+	case *image.NRGBA64:
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
+			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
+
+				j := src0.PixOffset(x, y)
+
+				dst.Pix[i+0] = src0.Pix[j+0]
+				dst.Pix[i+1] = src0.Pix[j+2]
+				dst.Pix[i+2] = src0.Pix[j+4]
+				dst.Pix[i+3] = src0.Pix[j+6]
+
+			}
+		}
+
+	case *image.RGBA:
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
+			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
+
+				j := src0.PixOffset(x, y)
+				a := src0.Pix[j+3]
+				dst.Pix[i+3] = a
+
+				switch a {
+				case 0:
+					dst.Pix[i+0] = 0
+					dst.Pix[i+1] = 0
+					dst.Pix[i+2] = 0
+				case 0xff:
+					dst.Pix[i+0] = src0.Pix[j+0]
+					dst.Pix[i+1] = src0.Pix[j+1]
+					dst.Pix[i+2] = src0.Pix[j+2]
+				default:
+					dst.Pix[i+0] = uint8(uint16(src0.Pix[j+0]) * 0xff / uint16(a))
+					dst.Pix[i+1] = uint8(uint16(src0.Pix[j+1]) * 0xff / uint16(a))
+					dst.Pix[i+2] = uint8(uint16(src0.Pix[j+2]) * 0xff / uint16(a))
+				}
+			}
+		}
+
+	case *image.RGBA64:
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
+			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
+
+				j := src0.PixOffset(x, y)
+				a := src0.Pix[j+6]
+				dst.Pix[i+3] = a
+
+				switch a {
+				case 0:
+					dst.Pix[i+0] = 0
+					dst.Pix[i+1] = 0
+					dst.Pix[i+2] = 0
+				case 0xff:
+					dst.Pix[i+0] = src0.Pix[j+0]
+					dst.Pix[i+1] = src0.Pix[j+2]
+					dst.Pix[i+2] = src0.Pix[j+4]
+				default:
+					dst.Pix[i+0] = uint8(uint16(src0.Pix[j+0]) * 0xff / uint16(a))
+					dst.Pix[i+1] = uint8(uint16(src0.Pix[j+2]) * 0xff / uint16(a))
+					dst.Pix[i+2] = uint8(uint16(src0.Pix[j+4]) * 0xff / uint16(a))
+				}
+			}
+		}
+
+	case *image.Gray:
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
+			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
+
+				j := src0.PixOffset(x, y)
+				c := src0.Pix[j]
+				dst.Pix[i+0] = c
+				dst.Pix[i+1] = c
+				dst.Pix[i+2] = c
+				dst.Pix[i+3] = 0xff
+
+			}
+		}
+
+	case *image.Gray16:
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
+			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
+
+				j := src0.PixOffset(x, y)
+				c := src0.Pix[j]
+				dst.Pix[i+0] = c
+				dst.Pix[i+1] = c
+				dst.Pix[i+2] = c
+				dst.Pix[i+3] = 0xff
+
+			}
+		}
+
+	case *image.YCbCr:
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
+			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
+
+				yj := src0.YOffset(x, y)
+				cj := src0.COffset(x, y)
+				r, g, b := color.YCbCrToRGB(src0.Y[yj], src0.Cb[cj], src0.Cr[cj])
+
+				dst.Pix[i+0] = r
+				dst.Pix[i+1] = g
+				dst.Pix[i+2] = b
+				dst.Pix[i+3] = 0xff
+
+			}
+		}
+
+	default:
+		i0 := dst.PixOffset(dstMinX, dstMinY)
+		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
+			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
+
+				c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
+
+				dst.Pix[i+0] = c.R
+				dst.Pix[i+1] = c.G
+				dst.Pix[i+2] = c.B
+				dst.Pix[i+3] = c.A
+
+			}
+		}
+	}
+
+	return dst
+}

+ 36 - 0
crop.go

@@ -0,0 +1,36 @@
+package imaging
+
+import (
+	"image"
+)
+
+// 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)
+}
+
+// CropCenter cuts out a rectangular region with the specified size
+// from the center of the image and returns the cropped image.
+func CropCenter(img image.Image, width, height int) *image.NRGBA {
+	cropW, cropH := width, height
+
+	srcBounds := img.Bounds()
+	srcW := srcBounds.Dx()
+	srcH := srcBounds.Dy()
+	srcMinX := srcBounds.Min.X
+	srcMinY := srcBounds.Min.Y
+
+	centerX := srcMinX + srcW/2
+	centerY := srcMinY + srcH/2
+
+	x0 := centerX - cropW/2
+	y0 := centerY - cropH/2
+	x1 := x0 + cropW
+	y1 := y0 + cropH
+
+	return Crop(img, image.Rect(x0, y0, x1, y1))
+}

+ 60 - 0
doc.go

@@ -0,0 +1,60 @@
+/*
+Package imaging provides basic image manipulation functions (resize, rotate, flip, crop, etc.).
+This package is based on the standard Go image package and works best along with it.
+
+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).
+
+Imaging package uses parallel goroutines for faster image processing.
+To achieve maximum performance, make sure to allow Go to utilize all CPU cores:
+
+	runtime.GOMAXPROCS(runtime.NumCPU())
+
+Here is the complete example that loades several images, makes thumbnails of them
+and joins them together.
+
+	package main
+
+	import (
+		"image"
+		"image/color"
+		"runtime"
+
+		"github.com/disintegration/imaging"
+	)
+
+	func main() {
+		// use all CPU cores for maximum performance
+		runtime.GOMAXPROCS(runtime.NumCPU())
+
+		// input files
+		files := []string{"01.jpg", "02.jpg", "03.jpg"}
+
+		// load images and make 100x100 thumbnails of them
+		var thumbnails []image.Image
+		for _, file := range files {
+			img, err := imaging.Open(file)
+			if err != nil {
+				panic(err)
+			}
+			thumb := imaging.Thumbnail(img, 100, 100, imaging.CatmullRom)
+			thumbnails = append(thumbnails, thumb)
+		}
+
+		// create a new blank image
+		dst := imaging.New(100*len(thumbnails), 100, color.NRGBA{0, 0, 0, 0})
+
+		// paste thumbnails into the new image side by side
+		for i, thumb := range thumbnails {
+			dst = imaging.Paste(dst, thumb, image.Pt(i*100, 0))
+		}
+
+		// save the combined image to file
+		err := imaging.Save(dst, "dst.jpg")
+		if err != nil {
+			panic(err)
+		}
+	}
+*/
+package imaging

+ 99 - 0
helpers.go

@@ -0,0 +1,99 @@
+package imaging
+
+import (
+	"fmt"
+	"image"
+	"image/color"
+	_ "image/gif"
+	"image/jpeg"
+	"image/png"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+// Open loads an image from file
+func Open(filename string) (img image.Image, err error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return
+	}
+	defer file.Close()
+
+	img, _, err = image.Decode(file)
+	if err != nil {
+		return
+	}
+
+	img = toNRGBA(img)
+	return
+}
+
+// Save saves the image to file with the specified filename.
+// The format is determined from the filename extension, "jpg" (or "jpeg") and "png" are supported.
+func Save(img image.Image, filename string) (err error) {
+	format := strings.ToLower(filepath.Ext(filename))
+	if format != ".jpg" && format != ".jpeg" && format != ".png" {
+		err = fmt.Errorf("unknown image format: %s", format)
+		return
+	}
+
+	file, err := os.Create(filename)
+	if err != nil {
+		return
+	}
+	defer file.Close()
+
+	switch format {
+	case ".jpg", ".jpeg":
+		var rgba *image.RGBA
+		if nrgba, ok := img.(*image.NRGBA); ok {
+			if nrgba.Opaque() {
+				rgba = &image.RGBA{
+					Pix:    nrgba.Pix,
+					Stride: nrgba.Stride,
+					Rect:   nrgba.Rect,
+				}
+			}
+		}
+		if rgba != nil {
+			err = jpeg.Encode(file, rgba, &jpeg.Options{Quality: 95})
+		} else {
+			err = jpeg.Encode(file, img, &jpeg.Options{Quality: 95})
+		}
+
+	case ".png":
+		err = png.Encode(file, img)
+	}
+	return
+}
+
+// New creates a new image with the specified width and height, and fills it with the specified color.
+func New(width, height int, fillColor color.Color) *image.NRGBA {
+	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
+
+	c := color.NRGBAModel.Convert(fillColor).(color.NRGBA)
+	cs := []uint8{c.R, c.G, c.B, c.A}
+
+	// fill the first row
+	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])
+	}
+
+	return dst
+}
+
+// This function used internally to convert any image type to NRGBA if needed.
+func toNRGBA(img image.Image) *image.NRGBA {
+	srcBounds := img.Bounds()
+	if srcBounds.Min.X == 0 && srcBounds.Min.Y == 0 {
+		if src0, ok := img.(*image.NRGBA); ok {
+			return src0
+		}
+	}
+	return Clone(img)
+}

+ 0 - 911
imaging.go

@@ -1,911 +0,0 @@
-// Package imaging provides basic image manipulation functions
-// (resize, rotate, flip, crop, etc.) as well as simplified image loading and saving.
-//
-// This package is based on the standard Go image package. All the image
-// manipulation functions provided by the package take any image type that
-// implements image.Image interface, and return a new image of
-// *image.NRGBA type (32 bit RGBA colors, not premultiplied by alpha).
-//
-package imaging
-
-import (
-	"fmt"
-	"image"
-	"image/color"
-	_ "image/gif"
-	"image/jpeg"
-	"image/png"
-	"math"
-	"os"
-	"path/filepath"
-	"strings"
-)
-
-// Open loads an image from file
-func Open(filename string) (img image.Image, err error) {
-	file, err := os.Open(filename)
-	if err != nil {
-		return
-	}
-	defer file.Close()
-
-	img, _, err = image.Decode(file)
-	return
-}
-
-// Save saves the image to file with the specified filename.
-// The format is determined from the filename extension, "jpg" (or "jpeg") and "png" are supported.
-func Save(img image.Image, filename string) (err error) {
-	format := strings.ToLower(filepath.Ext(filename))
-	if format != ".jpg" && format != ".jpeg" && format != ".png" {
-		err = fmt.Errorf("unknown image format: %s", format)
-		return
-	}
-
-	file, err := os.Create(filename)
-	if err != nil {
-		return
-	}
-	defer file.Close()
-
-	switch format {
-	case ".jpg", ".jpeg":
-		var rgba *image.RGBA
-		if nrgba, ok := img.(*image.NRGBA); ok {
-			if nrgba.Opaque() {
-				rgba = &image.RGBA{
-					Pix:    nrgba.Pix,
-					Stride: nrgba.Stride,
-					Rect:   nrgba.Rect,
-				}
-			}
-		}
-		if rgba != nil {
-			err = jpeg.Encode(file, rgba, &jpeg.Options{Quality: 95})
-		} else {
-			err = jpeg.Encode(file, img, &jpeg.Options{Quality: 95})
-		}
-
-	case ".png":
-		err = png.Encode(file, img)
-	}
-	return
-}
-
-// New creates a new image with the specified width and height, and fills it with the specified color.
-func New(width, height int, fillColor color.Color) *image.NRGBA {
-	dst := image.NewNRGBA(image.Rect(0, 0, width, height))
-	c := color.NRGBAModel.Convert(fillColor).(color.NRGBA)
-
-	i0 := dst.PixOffset(0, 0)
-	for y := 0; y < height; y, i0 = y+1, i0+dst.Stride {
-		for x, i := 0, i0; x < width; x, i = x+1, i+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
-		}
-	}
-
-	return dst
-}
-
-// This function converts any image type to *image.NRGBA for faster pixel access
-// Optimized for most standard image types: NRGBA64, RGBA, RGBA64, YCbCr, Gray, Gray16
-// If clone is true, the new image bounds will start at (0,0), also, a new copy
-// will be created even if the source image's type is already NRGBA
-func toNRGBA(src image.Image, clone bool) *image.NRGBA {
-	if !clone {
-		if src0, ok := src.(*image.NRGBA); ok {
-			return src0
-		}
-	}
-
-	srcBounds := src.Bounds()
-	dstBounds := srcBounds
-
-	// if we need a copy - translate Min point to (0, 0)
-	if clone {
-		dstBounds = dstBounds.Sub(dstBounds.Min)
-	}
-
-	dst := image.NewNRGBA(dstBounds)
-
-	dstMinX := dstBounds.Min.X
-	dstMinY := dstBounds.Min.Y
-
-	srcMinX := srcBounds.Min.X
-	srcMinY := srcBounds.Min.Y
-	srcMaxX := srcBounds.Max.X
-	srcMaxY := srcBounds.Max.Y
-
-	switch src0 := src.(type) {
-
-	case *image.NRGBA:
-		rowSize := srcBounds.Dx() * 4
-		numRows := srcBounds.Dy()
-
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		j0 := src0.PixOffset(srcMinX, srcMinY)
-
-		di := dst.Stride
-		dj := src0.Stride
-
-		for row := 0; row < numRows; row++ {
-			copy(dst.Pix[i0:i0+rowSize], src0.Pix[j0:j0+rowSize])
-			i0 += di
-			j0 += dj
-		}
-
-	case *image.NRGBA64:
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
-			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
-
-				j := src0.PixOffset(x, y)
-
-				dst.Pix[i+0] = src0.Pix[j+0]
-				dst.Pix[i+1] = src0.Pix[j+2]
-				dst.Pix[i+2] = src0.Pix[j+4]
-				dst.Pix[i+3] = src0.Pix[j+6]
-
-			}
-		}
-
-	case *image.RGBA:
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
-			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
-
-				j := src0.PixOffset(x, y)
-				a := src0.Pix[j+3]
-				dst.Pix[i+3] = a
-
-				switch a {
-				case 0:
-					dst.Pix[i+0] = 0
-					dst.Pix[i+1] = 0
-					dst.Pix[i+2] = 0
-				case 0xff:
-					dst.Pix[i+0] = src0.Pix[j+0]
-					dst.Pix[i+1] = src0.Pix[j+1]
-					dst.Pix[i+2] = src0.Pix[j+2]
-				default:
-					dst.Pix[i+0] = uint8(uint16(src0.Pix[j+0]) * 0xff / uint16(a))
-					dst.Pix[i+1] = uint8(uint16(src0.Pix[j+1]) * 0xff / uint16(a))
-					dst.Pix[i+2] = uint8(uint16(src0.Pix[j+2]) * 0xff / uint16(a))
-				}
-			}
-		}
-
-	case *image.RGBA64:
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
-			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
-
-				j := src0.PixOffset(x, y)
-				a := src0.Pix[j+6]
-				dst.Pix[i+3] = a
-
-				switch a {
-				case 0:
-					dst.Pix[i+0] = 0
-					dst.Pix[i+1] = 0
-					dst.Pix[i+2] = 0
-				case 0xff:
-					dst.Pix[i+0] = src0.Pix[j+0]
-					dst.Pix[i+1] = src0.Pix[j+2]
-					dst.Pix[i+2] = src0.Pix[j+4]
-				default:
-					dst.Pix[i+0] = uint8(uint16(src0.Pix[j+0]) * 0xff / uint16(a))
-					dst.Pix[i+1] = uint8(uint16(src0.Pix[j+2]) * 0xff / uint16(a))
-					dst.Pix[i+2] = uint8(uint16(src0.Pix[j+4]) * 0xff / uint16(a))
-				}
-			}
-		}
-
-	case *image.Gray:
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
-			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
-
-				j := src0.PixOffset(x, y)
-				c := src0.Pix[j]
-				dst.Pix[i+0] = c
-				dst.Pix[i+1] = c
-				dst.Pix[i+2] = c
-				dst.Pix[i+3] = 0xff
-
-			}
-		}
-
-	case *image.Gray16:
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
-			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
-
-				j := src0.PixOffset(x, y)
-				c := src0.Pix[j]
-				dst.Pix[i+0] = c
-				dst.Pix[i+1] = c
-				dst.Pix[i+2] = c
-				dst.Pix[i+3] = 0xff
-
-			}
-		}
-
-	case *image.YCbCr:
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
-			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
-
-				yj := src0.YOffset(x, y)
-				cj := src0.COffset(x, y)
-				r, g, b := color.YCbCrToRGB(src0.Y[yj], src0.Cb[cj], src0.Cr[cj])
-
-				dst.Pix[i+0] = r
-				dst.Pix[i+1] = g
-				dst.Pix[i+2] = b
-				dst.Pix[i+3] = 0xff
-
-			}
-		}
-
-	default:
-		i0 := dst.PixOffset(dstMinX, dstMinY)
-		for y := srcMinY; y < srcMaxY; y, i0 = y+1, i0+dst.Stride {
-			for x, i := srcMinX, i0; x < srcMaxX; x, i = x+1, i+4 {
-
-				c := color.NRGBAModel.Convert(src.At(x, y)).(color.NRGBA)
-
-				dst.Pix[i+0] = c.R
-				dst.Pix[i+1] = c.G
-				dst.Pix[i+2] = c.B
-				dst.Pix[i+3] = c.A
-
-			}
-		}
-	}
-
-	return dst
-}
-
-// This function is used internally to check if the image type is *image.NRGBA
-// If not - converts any image type to *image.NRGBA for faster pixel access
-func convertToNRGBA(img image.Image) *image.NRGBA {
-	// 'false' indicates that we don't need a new copy of img if it is already NRGBA
-	// and that the new image's bounds will be equal the bounds of the source image
-	return toNRGBA(img, false)
-}
-
-// Clone returns a copy of the img. New image bounds will be (0, 0)-(width, height).
-func Clone(img image.Image) *image.NRGBA {
-	// 'true' indicates that we need a new copy of img even if it is already NRGBA
-	// and that the new image's bounds will start at point (0, 0)
-	return toNRGBA(img, true)
-}
-
-// 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 := convertToNRGBA(img)
-	sub := src.SubImage(rect)
-	return Clone(sub) // New image Bounds().Min point will be (0, 0)
-}
-
-// Crop cuts out a rectangular region with the specified size
-// from the center of the image and returns the cropped image.
-func CropCenter(img image.Image, width, height int) *image.NRGBA {
-	cropW, cropH := width, height
-
-	srcBounds := img.Bounds()
-	srcW := srcBounds.Dx()
-	srcH := srcBounds.Dy()
-	srcMinX := srcBounds.Min.X
-	srcMinY := srcBounds.Min.Y
-
-	centerX := srcMinX + srcW/2
-	centerY := srcMinY + srcH/2
-
-	x0 := centerX - cropW/2
-	y0 := centerY - cropH/2
-	x1 := x0 + cropW
-	y1 := y0 + cropH
-
-	return Crop(img, image.Rect(x0, y0, x1, y1))
-}
-
-// Paste pastes the src image to the img image at the specified position and returns the combined image.
-func Paste(img, src image.Image, pos image.Point) *image.NRGBA {
-	srcBounds := src.Bounds()
-	src0 := convertToNRGBA(src)
-
-	dst := Clone(img)                    // cloned image bounds start at (0, 0)
-	startPt := pos.Sub(img.Bounds().Min) // so we should translate start point
-	endPt := startPt.Add(srcBounds.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 + srcBounds.Min.X
-		srcStartY := intersectBounds.Min.Y - pasteBounds.Min.Y + srcBounds.Min.Y
-
-		i0 := dst.PixOffset(intersectBounds.Min.X, intersectBounds.Min.Y)
-		j0 := src0.PixOffset(srcStartX, srcStartY)
-
-		di := dst.Stride
-		dj := src0.Stride
-
-		for row := 0; row < numRows; row++ {
-			copy(dst.Pix[i0:i0+rowSize], src0.Pix[j0:j0+rowSize])
-			i0 += di
-			j0 += dj
-		}
-
-	}
-
-	return dst
-}
-
-// Paste pastes the src image to the center of the img image and returns the combined image.
-func PasteCenter(img, src image.Image) *image.NRGBA {
-	imgBounds := img.Bounds()
-	imgW := imgBounds.Dx()
-	imgH := imgBounds.Dy()
-	imgMinX := imgBounds.Min.X
-	imgMinY := imgBounds.Min.Y
-
-	centerX := imgMinX + imgW/2
-	centerY := imgMinY + imgH/2
-
-	x0 := centerX - src.Bounds().Dx()/2
-	y0 := centerY - src.Bounds().Dy()/2
-
-	return Paste(img, src, image.Pt(x0, y0))
-}
-
-// Overlay draws the source image over the background image at given position
-// and returns the combined image. Opacity parameter is the opacity of the source
-// image layer, used to compose the images, it must be from 0.0 to 1.0.
-//
-// Usage examples:
-//
-//		// draw the sprite over the background at position (50, 50)
-//		dstImage := imaging.Overlay(backgroundImage, spriteImage, image.Pt(50, 50), 1.0)
-//
-//		// blend two opaque images of the same size
-//		dstImage := imaging.Overlay(imageOne, imageTwo, image.Pt(0, 0), 0.5)
-//
-func Overlay(background, source 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 := convertToNRGBA(source)
-	srcBounds := src.Bounds()
-
-	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(srcBounds.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 := dst.PixOffset(x, y)
-				srcX := x - pasteBounds.Min.X + srcBounds.Min.X
-				srcY := y - pasteBounds.Min.Y + srcBounds.Min.Y
-				j := src.PixOffset(srcX, srcY)
-
-				a1 := float64(dst.Pix[i+3])
-				a2 := float64(src.Pix[j+3])
-
-				coef2 := opacity * a2 / 255.0
-				coef1 := (1 - coef2) * a1 / 255.0
-				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))
-			}
-		}
-	}
-
-	return dst
-}
-
-// Rotate90 rotates the image 90 degrees counterclockwise and returns the transformed image.
-func Rotate90(img image.Image) *image.NRGBA {
-	src := convertToNRGBA(img)
-	srcBounds := src.Bounds()
-	srcMaxX := srcBounds.Max.X
-	srcMinY := srcBounds.Min.Y
-
-	dstW := srcBounds.Dy()
-	dstH := srcBounds.Dx()
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	for dstY := 0; dstY < dstH; dstY++ {
-		for dstX := 0; dstX < dstW; dstX++ {
-
-			srcX := srcMaxX - dstY - 1
-			srcY := srcMinY + dstX
-
-			srcOff := src.PixOffset(srcX, srcY)
-			dstOff := dst.PixOffset(dstX, dstY)
-
-			dst.Pix[dstOff+0] = src.Pix[srcOff+0]
-			dst.Pix[dstOff+1] = src.Pix[srcOff+1]
-			dst.Pix[dstOff+2] = src.Pix[srcOff+2]
-			dst.Pix[dstOff+3] = src.Pix[srcOff+3]
-		}
-	}
-
-	return dst
-}
-
-// Rotate180 rotates the image 180 degrees counterclockwise and returns the transformed image.
-func Rotate180(img image.Image) *image.NRGBA {
-	src := convertToNRGBA(img)
-	srcBounds := src.Bounds()
-	srcMaxX := srcBounds.Max.X
-	srcMaxY := srcBounds.Max.Y
-
-	dstW := srcBounds.Dx()
-	dstH := srcBounds.Dy()
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	for dstY := 0; dstY < dstH; dstY++ {
-		for dstX := 0; dstX < dstW; dstX++ {
-
-			srcX := srcMaxX - dstX - 1
-			srcY := srcMaxY - dstY - 1
-
-			srcOff := src.PixOffset(srcX, srcY)
-			dstOff := dst.PixOffset(dstX, dstY)
-
-			dst.Pix[dstOff+0] = src.Pix[srcOff+0]
-			dst.Pix[dstOff+1] = src.Pix[srcOff+1]
-			dst.Pix[dstOff+2] = src.Pix[srcOff+2]
-			dst.Pix[dstOff+3] = src.Pix[srcOff+3]
-		}
-	}
-
-	return dst
-}
-
-// Rotate270 rotates the image 270 degrees counterclockwise and returns the transformed image.
-func Rotate270(img image.Image) *image.NRGBA {
-	src := convertToNRGBA(img)
-	srcBounds := src.Bounds()
-	srcMaxY := srcBounds.Max.Y
-	srcMinX := srcBounds.Min.X
-
-	dstW := srcBounds.Dy()
-	dstH := srcBounds.Dx()
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	for dstY := 0; dstY < dstH; dstY++ {
-		for dstX := 0; dstX < dstW; dstX++ {
-
-			srcX := srcMinX + dstY
-			srcY := srcMaxY - dstX - 1
-
-			srcOff := src.PixOffset(srcX, srcY)
-			dstOff := dst.PixOffset(dstX, dstY)
-
-			dst.Pix[dstOff+0] = src.Pix[srcOff+0]
-			dst.Pix[dstOff+1] = src.Pix[srcOff+1]
-			dst.Pix[dstOff+2] = src.Pix[srcOff+2]
-			dst.Pix[dstOff+3] = src.Pix[srcOff+3]
-		}
-	}
-
-	return dst
-}
-
-// FlipH flips the image horizontally (from left to right) and returns the transformed image.
-func FlipH(img image.Image) *image.NRGBA {
-	src := convertToNRGBA(img)
-	srcBounds := src.Bounds()
-	srcMaxX := srcBounds.Max.X
-	srcMinY := srcBounds.Min.Y
-
-	dstW := srcBounds.Dx()
-	dstH := srcBounds.Dy()
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	for dstY := 0; dstY < dstH; dstY++ {
-		for dstX := 0; dstX < dstW; dstX++ {
-
-			srcX := srcMaxX - dstX - 1
-			srcY := srcMinY + dstY
-
-			srcOff := src.PixOffset(srcX, srcY)
-			dstOff := dst.PixOffset(dstX, dstY)
-
-			dst.Pix[dstOff+0] = src.Pix[srcOff+0]
-			dst.Pix[dstOff+1] = src.Pix[srcOff+1]
-			dst.Pix[dstOff+2] = src.Pix[srcOff+2]
-			dst.Pix[dstOff+3] = src.Pix[srcOff+3]
-		}
-	}
-
-	return dst
-}
-
-// FlipV flips the image vertically (from top to bottom) and returns the transformed image.
-func FlipV(img image.Image) *image.NRGBA {
-	src := convertToNRGBA(img)
-	srcBounds := src.Bounds()
-	srcMaxY := srcBounds.Max.Y
-	srcMinX := srcBounds.Min.X
-
-	dstW := srcBounds.Dx()
-	dstH := srcBounds.Dy()
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	for dstY := 0; dstY < dstH; dstY++ {
-		for dstX := 0; dstX < dstW; dstX++ {
-
-			srcX := srcMinX + dstX
-			srcY := srcMaxY - dstY - 1
-
-			srcOff := src.PixOffset(srcX, srcY)
-			dstOff := dst.PixOffset(dstX, dstY)
-
-			dst.Pix[dstOff+0] = src.Pix[srcOff+0]
-			dst.Pix[dstOff+1] = src.Pix[srcOff+1]
-			dst.Pix[dstOff+2] = src.Pix[srcOff+2]
-			dst.Pix[dstOff+3] = src.Pix[srcOff+3]
-		}
-	}
-
-	return dst
-}
-
-// Resize resizes the image to the specified width and height using the specified resampling
-// filter and returns the transformed image. If one of width or height is 0, the image aspect
-// ratio is preserved.
-//
-// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
-// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
-//
-// Usage example:
-//
-//		dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos)
-//
-func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
-	if filter.Support <= 0.0 { // nearest-neighbor special case
-		return resizeNearest(img, width, height)
-	}
-
-	dstW, dstH := width, height
-
-	if dstW < 0 || dstH < 0 {
-		return &image.NRGBA{}
-	}
-	if dstW == 0 && dstH == 0 {
-		return &image.NRGBA{}
-	}
-
-	srcBounds := img.Bounds()
-	srcW := srcBounds.Dx()
-	srcH := srcBounds.Dy()
-
-	if srcW <= 0 || srcH <= 0 {
-		return &image.NRGBA{}
-	}
-
-	// 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)))
-	}
-	if dstH == 0 {
-		tmpH := float64(dstW) * float64(srcH) / float64(srcW)
-		dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
-	}
-
-	src := convertToNRGBA(img)
-	var tmp, dst *image.NRGBA
-
-	// two-pass resize
-	if srcW != dstW {
-		tmp = resizeHorizontal(src, dstW, filter)
-	} else {
-		tmp = src
-	}
-
-	if srcH != dstH {
-		dst = resizeVertical(tmp, dstH, filter)
-	} else {
-		dst = tmp
-	}
-
-	return dst
-}
-
-func resizeHorizontal(src *image.NRGBA, width int, filter ResampleFilter) *image.NRGBA {
-	srcBounds := src.Bounds()
-	srcW := srcBounds.Dx()
-	srcH := srcBounds.Dy()
-	srcMinX := srcBounds.Min.X
-	srcMinY := srcBounds.Min.Y
-	srcMaxX := srcBounds.Max.X
-
-	dstW := width
-	dstH := srcH
-
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	dX := float64(srcW) / float64(dstW)
-	scaleX := math.Max(dX, 1.0)
-	rX := math.Ceil(scaleX * filter.Support)
-	weights := make([]float64, int(rX+2)*2)
-
-	for dstX := 0; dstX < dstW; dstX++ {
-		fX := float64(srcMinX) + (float64(dstX)+0.5)*dX - 0.5
-
-		startX := int(math.Ceil(fX - rX))
-		if startX < srcMinX {
-			startX = srcMinX
-		}
-		endX := int(math.Floor(fX + rX))
-		if endX > srcMaxX-1 {
-			endX = srcMaxX - 1
-		}
-
-		// cache weights
-		weightSum := 0.0
-		for x := startX; x <= endX; x++ {
-			w := filter.Kernel((float64(x) - fX) / scaleX)
-			weightSum += w
-			weights[x-startX] = w
-		}
-
-		for dstY := 0; dstY < dstH; dstY++ {
-			srcY := srcMinY + dstY
-
-			r, g, b, a := 0.0, 0.0, 0.0, 0.0
-			for x := startX; x <= endX; x++ {
-				weight := weights[x-startX]
-				i := src.PixOffset(x, srcY)
-				r += float64(src.Pix[i+0]) * weight
-				g += float64(src.Pix[i+1]) * weight
-				b += float64(src.Pix[i+2]) * weight
-				a += float64(src.Pix[i+3]) * weight
-			}
-
-			r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
-			g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
-			b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
-			a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
-
-			j := dst.PixOffset(dstX, dstY)
-			dst.Pix[j+0] = uint8(r + 0.5)
-			dst.Pix[j+1] = uint8(g + 0.5)
-			dst.Pix[j+2] = uint8(b + 0.5)
-			dst.Pix[j+3] = uint8(a + 0.5)
-		}
-	}
-
-	return dst
-}
-
-func resizeVertical(src *image.NRGBA, height int, filter ResampleFilter) *image.NRGBA {
-	srcBounds := src.Bounds()
-	srcW := srcBounds.Dx()
-	srcH := srcBounds.Dy()
-	srcMinX := srcBounds.Min.X
-	srcMinY := srcBounds.Min.Y
-	srcMaxY := srcBounds.Max.Y
-
-	dstW := srcW
-	dstH := height
-
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	dY := float64(srcH) / float64(dstH)
-	scaleY := math.Max(dY, 1.0)
-	rY := math.Ceil(scaleY * filter.Support)
-	weights := make([]float64, int(rY+2)*2)
-
-	for dstY := 0; dstY < dstH; dstY++ {
-		fY := float64(srcMinY) + (float64(dstY)+0.5)*dY - 0.5
-
-		startY := int(math.Ceil(fY - rY))
-		if startY < srcMinY {
-			startY = srcMinY
-		}
-		endY := int(math.Floor(fY + rY))
-		if endY > srcMaxY-1 {
-			endY = srcMaxY - 1
-		}
-
-		// cache weights
-		weightSum := 0.0
-		for y := startY; y <= endY; y++ {
-			w := filter.Kernel((float64(y) - fY) / scaleY)
-			weightSum += w
-			weights[y-startY] = w
-		}
-
-		for dstX := 0; dstX < dstW; dstX++ {
-			srcX := srcMinX + dstX
-
-			r, g, b, a := 0.0, 0.0, 0.0, 0.0
-			for y := startY; y <= endY; y++ {
-				weight := weights[y-startY]
-				i := src.PixOffset(srcX, y)
-				r += float64(src.Pix[i+0]) * weight
-				g += float64(src.Pix[i+1]) * weight
-				b += float64(src.Pix[i+2]) * weight
-				a += float64(src.Pix[i+3]) * weight
-			}
-
-			r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
-			g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
-			b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
-			a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
-
-			j := dst.PixOffset(dstX, dstY)
-			dst.Pix[j+0] = uint8(r + 0.5)
-			dst.Pix[j+1] = uint8(g + 0.5)
-			dst.Pix[j+2] = uint8(b + 0.5)
-			dst.Pix[j+3] = uint8(a + 0.5)
-		}
-	}
-
-	return dst
-}
-
-// fast nearest-neighbor resize, no filtering
-func resizeNearest(img image.Image, width, height int) *image.NRGBA {
-	dstW, dstH := width, height
-
-	if dstW < 0 || dstH < 0 {
-		return &image.NRGBA{}
-	}
-	if dstW == 0 && dstH == 0 {
-		return &image.NRGBA{}
-	}
-
-	srcBounds := img.Bounds()
-	srcW := srcBounds.Dx()
-	srcH := srcBounds.Dy()
-	srcMinX := srcBounds.Min.X
-	srcMinY := srcBounds.Min.Y
-	srcMaxX := srcBounds.Max.X
-	srcMaxY := srcBounds.Max.Y
-
-	if srcW <= 0 || srcH <= 0 {
-		return &image.NRGBA{}
-	}
-
-	// 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)))
-	}
-	if dstH == 0 {
-		tmpH := float64(dstW) * float64(srcH) / float64(srcW)
-		dstH = int(math.Max(1.0, math.Floor(tmpH+0.5)))
-	}
-
-	src := convertToNRGBA(img)
-	dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH))
-
-	dx := float64(srcW) / float64(dstW)
-	dy := float64(srcH) / float64(dstH)
-
-	for dstY := 0; dstY < dstH; dstY++ {
-		fy := float64(srcMinY) + (float64(dstY)+0.5)*dy - 0.5
-
-		for dstX := 0; dstX < dstW; dstX++ {
-			fx := float64(srcMinX) + (float64(dstX)+0.5)*dx - 0.5
-
-			srcX := int(math.Min(math.Max(math.Floor(fx+0.5), float64(srcMinX)), float64(srcMaxX)))
-			srcY := int(math.Min(math.Max(math.Floor(fy+0.5), float64(srcMinY)), float64(srcMaxY)))
-
-			srcOffset := src.PixOffset(srcX, srcY)
-			dstOffset := dst.PixOffset(dstX, dstY)
-
-			dst.Pix[dstOffset+0] = src.Pix[srcOffset+0]
-			dst.Pix[dstOffset+1] = src.Pix[srcOffset+1]
-			dst.Pix[dstOffset+2] = src.Pix[srcOffset+2]
-			dst.Pix[dstOffset+3] = src.Pix[srcOffset+3]
-		}
-	}
-
-	return dst
-}
-
-// Fit scales down the image using the specified resample filter to fit the specified
-// maximum width and height and returns the transformed image.
-//
-// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
-// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
-//
-// Usage example:
-//
-//		dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
-//
-func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
-	maxW, maxH := width, height
-
-	if maxW <= 0 || maxH <= 0 {
-		return &image.NRGBA{}
-	}
-
-	srcBounds := img.Bounds()
-	srcW := srcBounds.Dx()
-	srcH := srcBounds.Dy()
-
-	if srcW <= 0 || srcH <= 0 {
-		return &image.NRGBA{}
-	}
-
-	if srcW <= maxW && srcH <= maxH {
-		return Clone(img)
-	}
-
-	srcAspectRatio := float64(srcW) / float64(srcH)
-	maxAspectRatio := float64(maxW) / float64(maxH)
-
-	var newW, newH int
-	if srcAspectRatio > maxAspectRatio {
-		newW = maxW
-		newH = int(float64(newW) / srcAspectRatio)
-	} else {
-		newH = maxH
-		newW = int(float64(newH) * srcAspectRatio)
-	}
-
-	return Resize(img, newW, newH, filter)
-}
-
-// Thumbnail scales the image up or down using the specified resample filter, crops it
-// to the specified width and hight and returns the transformed image.
-//
-// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
-// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
-//
-// Usage example:
-//
-//		dstImage := imaging.Fit(srcImage, 100, 100, imaging.Lanczos)
-//
-func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
-	thumbW, thumbH := width, height
-
-	if thumbW <= 0 || thumbH <= 0 {
-		return &image.NRGBA{}
-	}
-
-	srcBounds := img.Bounds()
-	srcW := srcBounds.Dx()
-	srcH := srcBounds.Dy()
-
-	if srcW <= 0 || srcH <= 0 {
-		return &image.NRGBA{}
-	}
-
-	srcAspectRatio := float64(srcW) / float64(srcH)
-	thumbAspectRatio := float64(thumbW) / float64(thumbH)
-
-	var tmp image.Image
-	if srcAspectRatio > thumbAspectRatio {
-		tmp = Resize(img, 0, thumbH, filter)
-	} else {
-		tmp = Resize(img, thumbW, 0, filter)
-	}
-
-	return CropCenter(tmp, thumbW, thumbH)
-}

+ 58 - 0
overlay.go

@@ -0,0 +1,58 @@
+package imaging
+
+import (
+	"image"
+	"math"
+)
+
+// Overlay draws the img image over the background image at given position
+// and returns the combined image. Opacity parameter is the opacity of the img
+// image layer, used to compose the images, it must be from 0.0 to 1.0.
+//
+// Usage examples:
+//
+//		// draw the sprite over the background at position (50, 50)
+//		dstImage := imaging.Overlay(backgroundImage, spriteImage, image.Pt(50, 50), 1.0)
+//
+//		// 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
+
+				a1 := float64(dst.Pix[i+3])
+				a2 := float64(src.Pix[j+3])
+
+				coef2 := opacity * a2 / 255.0
+				coef1 := (1 - coef2) * a1 / 255.0
+				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))
+			}
+		}
+	}
+
+	return dst
+}

+ 54 - 0
parallel.go

@@ -0,0 +1,54 @@
+package imaging
+
+import (
+	"runtime"
+	"sync"
+	"sync/atomic"
+)
+
+var parallelizationEnabled = true
+
+// if GOMAXPROCS = 1: no goroutines used
+// if GOMAXPROCS > 1: spawn N=GOMAXPROCS workers in separate goroutines
+func parallel(dataSize int, fn func(partStart, partEnd int)) {
+	numGoroutines := 1
+	partSize := dataSize
+
+	if parallelizationEnabled {
+		numProcs := runtime.GOMAXPROCS(0)
+		if numProcs > 1 {
+			numGoroutines = numProcs
+			partSize = dataSize / (numGoroutines * 100)
+			if partSize < 1 {
+				partSize = 1
+			}
+		}
+	}
+
+	if numGoroutines == 1 {
+		fn(0, dataSize)
+	} else {
+		var wg sync.WaitGroup
+		wg.Add(numGoroutines)
+		idx := uint64(0)
+
+		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)
+				}
+			}()
+		}
+
+		wg.Wait()
+	}
+}

+ 55 - 0
paste.go

@@ -0,0 +1,55 @@
+package imaging
+
+import (
+	"image"
+)
+
+// 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
+		}
+	}
+
+	return dst
+}
+
+// PasteCenter pastes the img image to the center of the background image and returns the combined image.
+func PasteCenter(background, img image.Image) *image.NRGBA {
+	bgBounds := background.Bounds()
+	bgW := bgBounds.Dx()
+	bgH := bgBounds.Dy()
+	bgMinX := bgBounds.Min.X
+	bgMinY := bgBounds.Min.Y
+
+	centerX := bgMinX + bgW/2
+	centerY := bgMinY + bgH/2
+
+	x0 := centerX - img.Bounds().Dx()/2
+	y0 := centerY - img.Bounds().Dy()/2
+
+	return Paste(background, img, image.Pt(x0, y0))
+}

+ 321 - 0
resample.go

@@ -0,0 +1,321 @@
+package imaging
+
+import (
+	"image"
+	"math"
+)
+
+// Resize resizes the image to the specified width and height using the specified resampling
+// filter and returns the transformed image. If one of width or height is 0, the image aspect
+// ratio is preserved.
+//
+// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+//
+// Usage example:
+//
+//		dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos)
+//
+func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
+	dstW, dstH := width, height
+
+	if dstW < 0 || dstH < 0 {
+		return &image.NRGBA{}
+	}
+	if dstW == 0 && dstH == 0 {
+		return &image.NRGBA{}
+	}
+
+	src := toNRGBA(img)
+	srcW := src.Bounds().Max.X
+	srcH := src.Bounds().Max.Y
+
+	if srcW <= 0 || srcH <= 0 {
+		return &image.NRGBA{}
+	}
+
+	// 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)))
+	}
+	if dstH == 0 {
+		tmpH := float64(dstW) * float64(srcH) / float64(srcW)
+		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)
+		}
+	}
+
+	return dst
+}
+
+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))
+
+	dX := float64(srcW) / float64(dstW)
+	scaleX := math.Max(dX, 1.0)
+	rX := math.Ceil(scaleX * filter.Support)
+
+	parallel(dstW, func(partStart, partEnd int) {
+
+		weights := make([]float64, int(rX+2)*2)
+
+		for dstX := partStart; dstX < partEnd; dstX++ {
+
+			fX := (float64(dstX)+0.5)*dX - 0.5
+
+			startX := int(math.Ceil(fX - rX))
+			if startX < 0 {
+				startX = 0
+			}
+			endX := int(math.Floor(fX + rX))
+			if endX > srcW-1 {
+				endX = srcW - 1
+			}
+
+			// cache weights
+			weightSum := 0.0
+			for x := startX; x <= endX; x++ {
+				w := filter.Kernel((float64(x) - fX) / scaleX)
+				weightSum += w
+				weights[x-startX] = w
+			}
+
+			for dstY := 0; dstY < dstH; dstY++ {
+				r, g, b, a := 0.0, 0.0, 0.0, 0.0
+				for x := startX; x <= endX; x++ {
+					weight := weights[x-startX]
+					i := dstY*src.Stride + x*4
+					r += float64(src.Pix[i+0]) * weight
+					g += float64(src.Pix[i+1]) * weight
+					b += float64(src.Pix[i+2]) * weight
+					a += float64(src.Pix[i+3]) * weight
+				}
+
+				r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
+				g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
+				b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
+				a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
+
+				j := dstY*dst.Stride + dstX*4
+				dst.Pix[j+0] = uint8(r + 0.5)
+				dst.Pix[j+1] = uint8(g + 0.5)
+				dst.Pix[j+2] = uint8(b + 0.5)
+				dst.Pix[j+3] = uint8(a + 0.5)
+			}
+		}
+
+	})
+
+	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))
+
+	dY := float64(srcH) / float64(dstH)
+	scaleY := math.Max(dY, 1.0)
+	rY := math.Ceil(scaleY * filter.Support)
+
+	parallel(dstH, func(partStart, partEnd int) {
+
+		weights := make([]float64, int(rY+2)*2)
+
+		for dstY := partStart; dstY < partEnd; dstY++ {
+
+			fY := (float64(dstY)+0.5)*dY - 0.5
+
+			startY := int(math.Ceil(fY - rY))
+			if startY < 0 {
+				startY = 0
+			}
+			endY := int(math.Floor(fY + rY))
+			if endY > srcH-1 {
+				endY = srcH - 1
+			}
+
+			// cache weights
+			weightSum := 0.0
+			for y := startY; y <= endY; y++ {
+				w := filter.Kernel((float64(y) - fY) / scaleY)
+				weightSum += w
+				weights[y-startY] = w
+			}
+
+			for dstX := 0; dstX < dstW; dstX++ {
+				r, g, b, a := 0.0, 0.0, 0.0, 0.0
+				for y := startY; y <= endY; y++ {
+					weight := weights[y-startY]
+					i := y*src.Stride + dstX*4
+					r += float64(src.Pix[i+0]) * weight
+					g += float64(src.Pix[i+1]) * weight
+					b += float64(src.Pix[i+2]) * weight
+					a += float64(src.Pix[i+3]) * weight
+				}
+
+				r = math.Min(math.Max(r/weightSum, 0.0), 255.0)
+				g = math.Min(math.Max(g/weightSum, 0.0), 255.0)
+				b = math.Min(math.Max(b/weightSum, 0.0), 255.0)
+				a = math.Min(math.Max(a/weightSum, 0.0), 255.0)
+
+				j := dstY*dst.Stride + dstX*4
+				dst.Pix[j+0] = uint8(r + 0.5)
+				dst.Pix[j+1] = uint8(g + 0.5)
+				dst.Pix[j+2] = uint8(b + 0.5)
+				dst.Pix[j+3] = uint8(a + 0.5)
+			}
+		}
+
+	})
+
+	return dst
+}
+
+// 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++ {
+			fy := (float64(dstY)+0.5)*dy - 0.5
+
+			for dstX := 0; dstX < dstW; dstX++ {
+				fx := (float64(dstX)+0.5)*dx - 0.5
+
+				srcX := int(math.Min(math.Max(math.Floor(fx+0.5), 0.0), float64(srcW)))
+				srcY := int(math.Min(math.Max(math.Floor(fy+0.5), 0.0), float64(srcH)))
+
+				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
+}
+
+// Fit scales down the image using the specified resample filter to fit the specified
+// maximum width and height and returns the transformed image.
+//
+// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+//
+// Usage example:
+//
+//		dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos)
+//
+func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
+	maxW, maxH := width, height
+
+	if maxW <= 0 || maxH <= 0 {
+		return &image.NRGBA{}
+	}
+
+	srcBounds := img.Bounds()
+	srcW := srcBounds.Dx()
+	srcH := srcBounds.Dy()
+
+	if srcW <= 0 || srcH <= 0 {
+		return &image.NRGBA{}
+	}
+
+	if srcW <= maxW && srcH <= maxH {
+		return Clone(img)
+	}
+
+	srcAspectRatio := float64(srcW) / float64(srcH)
+	maxAspectRatio := float64(maxW) / float64(maxH)
+
+	var newW, newH int
+	if srcAspectRatio > maxAspectRatio {
+		newW = maxW
+		newH = int(float64(newW) / srcAspectRatio)
+	} else {
+		newH = maxH
+		newW = int(float64(newH) * srcAspectRatio)
+	}
+
+	return Resize(img, newW, newH, filter)
+}
+
+// Thumbnail scales the image up or down using the specified resample filter, crops it
+// to the specified width and hight and returns the transformed image.
+//
+// Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
+// CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
+//
+// Usage example:
+//
+//		dstImage := imaging.Fit(srcImage, 100, 100, imaging.Lanczos)
+//
+func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA {
+	thumbW, thumbH := width, height
+
+	if thumbW <= 0 || thumbH <= 0 {
+		return &image.NRGBA{}
+	}
+
+	srcBounds := img.Bounds()
+	srcW := srcBounds.Dx()
+	srcH := srcBounds.Dy()
+
+	if srcW <= 0 || srcH <= 0 {
+		return &image.NRGBA{}
+	}
+
+	srcAspectRatio := float64(srcW) / float64(srcH)
+	thumbAspectRatio := float64(thumbW) / float64(thumbH)
+
+	var tmp image.Image
+	if srcAspectRatio > thumbAspectRatio {
+		tmp = Resize(img, 0, thumbH, filter)
+	} else {
+		tmp = Resize(img, thumbW, 0, filter)
+	}
+
+	return CropCenter(tmp, thumbW, thumbH)
+}

+ 0 - 0
filters.go → resample_filters.go


+ 46 - 0
sharpen.go

@@ -0,0 +1,46 @@
+package imaging
+
+import (
+	"errors"
+	"image"
+)
+
+// Sharpen produces a sharpened version of the image.
+// Sigma parameter must be positive and indicates how much the image will be sharpened.
+//
+// Usage example:
+//
+//		dstImage := imaging.Sharpen(srcImage, 3.5)
+//
+func Sharpen(img image.Image, sigma float64) *image.NRGBA {
+	if sigma <= 0 {
+		errors.New("sigma parameter must be positive")
+	}
+
+	src := toNRGBA(img)
+	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]) + (int(src.Pix[k]) - int(blurred.Pix[k]))
+					if val < 0 {
+						val = 0
+					} else if val > 255 {
+						val = 255
+					}
+					dst.Pix[k] = uint8(val)
+				}
+			}
+		}
+	})
+
+	return dst
+}

+ 145 - 0
transform.go

@@ -0,0 +1,145 @@
+package imaging
+
+import (
+	"image"
+)
+
+// 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 := 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 := 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])
+			}
+		}
+
+	})
+
+	return dst
+}
+
+// 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 := 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 := 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])
+			}
+		}
+
+	})
+
+	return dst
+}
+
+// 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
+	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 := 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])
+			}
+		}
+
+	})
+
+	return dst
+}
+
+// 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
+	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])
+			}
+		}
+
+	})
+
+	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
+	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])
+			}
+		}
+
+	})
+
+	return dst
+}