Просмотр исходного кода

reorganization, optimization, parallelization, blur, sharpen

disintegration 11 лет назад
Родитель
Сommit
6429426f30
14 измененных файлов с 1412 добавлено и 946 удалено
  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
+}