mirror of
https://github.com/willnorris/imageproxy.git
synced 2025-01-06 22:40:34 -05:00
b5984d2822
no specific features I'm looking to add, just keeping thing up to date. Unit tests and my manual testing seems like everything is still working as expected.
380 lines
8.9 KiB
Go
380 lines
8.9 KiB
Go
// 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).
|
|
package imaging
|
|
|
|
import (
|
|
"errors"
|
|
"image"
|
|
"image/color"
|
|
"image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/image/bmp"
|
|
"golang.org/x/image/tiff"
|
|
)
|
|
|
|
// Format is an image file format.
|
|
type Format int
|
|
|
|
// Image file formats.
|
|
const (
|
|
JPEG Format = iota
|
|
PNG
|
|
GIF
|
|
TIFF
|
|
BMP
|
|
)
|
|
|
|
func (f Format) String() string {
|
|
switch f {
|
|
case JPEG:
|
|
return "JPEG"
|
|
case PNG:
|
|
return "PNG"
|
|
case GIF:
|
|
return "GIF"
|
|
case TIFF:
|
|
return "TIFF"
|
|
case BMP:
|
|
return "BMP"
|
|
default:
|
|
return "Unsupported"
|
|
}
|
|
}
|
|
|
|
var (
|
|
// ErrUnsupportedFormat means the given image format (or file extension) is unsupported.
|
|
ErrUnsupportedFormat = errors.New("imaging: unsupported image format")
|
|
)
|
|
|
|
// Decode reads an image from r.
|
|
func Decode(r io.Reader) (image.Image, error) {
|
|
img, _, err := image.Decode(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return toNRGBA(img), nil
|
|
}
|
|
|
|
// Open loads an image from file
|
|
func Open(filename string) (image.Image, error) {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
img, err := Decode(file)
|
|
return img, err
|
|
}
|
|
|
|
// Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP).
|
|
func Encode(w io.Writer, img image.Image, format Format) error {
|
|
var err error
|
|
switch format {
|
|
case JPEG:
|
|
var rgba *image.RGBA
|
|
if nrgba, ok := img.(*image.NRGBA); ok {
|
|
if nrgba.Opaque() {
|
|
rgba = &image.RGBA{
|
|
Pix: nrgba.Pix,
|
|
Stride: nrgba.Stride,
|
|
Rect: nrgba.Rect,
|
|
}
|
|
}
|
|
}
|
|
if rgba != nil {
|
|
err = jpeg.Encode(w, rgba, &jpeg.Options{Quality: 95})
|
|
} else {
|
|
err = jpeg.Encode(w, img, &jpeg.Options{Quality: 95})
|
|
}
|
|
|
|
case PNG:
|
|
err = png.Encode(w, img)
|
|
case GIF:
|
|
err = gif.Encode(w, img, &gif.Options{NumColors: 256})
|
|
case TIFF:
|
|
err = tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
|
|
case BMP:
|
|
err = bmp.Encode(w, img)
|
|
default:
|
|
err = ErrUnsupportedFormat
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Save saves the image to file with the specified filename.
|
|
// The format is determined from the filename extension: "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
|
|
func Save(img image.Image, filename string) (err error) {
|
|
formats := map[string]Format{
|
|
".jpg": JPEG,
|
|
".jpeg": JPEG,
|
|
".png": PNG,
|
|
".tif": TIFF,
|
|
".tiff": TIFF,
|
|
".bmp": BMP,
|
|
".gif": GIF,
|
|
}
|
|
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
f, ok := formats[ext]
|
|
if !ok {
|
|
return ErrUnsupportedFormat
|
|
}
|
|
|
|
file, err := os.Create(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
return Encode(file, img, f)
|
|
}
|
|
|
|
// New creates a new image with the specified width and height, and fills it with the specified color.
|
|
func New(width, height int, fillColor color.Color) *image.NRGBA {
|
|
if width <= 0 || height <= 0 {
|
|
return &image.NRGBA{}
|
|
}
|
|
|
|
dst := image.NewNRGBA(image.Rect(0, 0, width, height))
|
|
c := color.NRGBAModel.Convert(fillColor).(color.NRGBA)
|
|
|
|
if c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 {
|
|
return dst
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Clone returns a copy of the given image.
|
|
func Clone(img image.Image) *image.NRGBA {
|
|
srcBounds := img.Bounds()
|
|
srcMinX := srcBounds.Min.X
|
|
srcMinY := srcBounds.Min.Y
|
|
|
|
dstBounds := srcBounds.Sub(srcBounds.Min)
|
|
dstW := dstBounds.Dx()
|
|
dstH := dstBounds.Dy()
|
|
dst := image.NewNRGBA(dstBounds)
|
|
|
|
switch src := img.(type) {
|
|
|
|
case *image.NRGBA:
|
|
rowSize := srcBounds.Dx() * 4
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
si := src.PixOffset(srcMinX, srcMinY+dstY)
|
|
copy(dst.Pix[di:di+rowSize], src.Pix[si:si+rowSize])
|
|
}
|
|
})
|
|
|
|
case *image.NRGBA64:
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
si := src.PixOffset(srcMinX, srcMinY+dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
dst.Pix[di+0] = src.Pix[si+0]
|
|
dst.Pix[di+1] = src.Pix[si+2]
|
|
dst.Pix[di+2] = src.Pix[si+4]
|
|
dst.Pix[di+3] = src.Pix[si+6]
|
|
di += 4
|
|
si += 8
|
|
}
|
|
}
|
|
})
|
|
|
|
case *image.RGBA:
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
si := src.PixOffset(srcMinX, srcMinY+dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
a := src.Pix[si+3]
|
|
dst.Pix[di+3] = a
|
|
|
|
switch a {
|
|
case 0:
|
|
dst.Pix[di+0] = 0
|
|
dst.Pix[di+1] = 0
|
|
dst.Pix[di+2] = 0
|
|
case 0xff:
|
|
dst.Pix[di+0] = src.Pix[si+0]
|
|
dst.Pix[di+1] = src.Pix[si+1]
|
|
dst.Pix[di+2] = src.Pix[si+2]
|
|
default:
|
|
var tmp uint16
|
|
tmp = uint16(src.Pix[si+0]) * 0xff / uint16(a)
|
|
dst.Pix[di+0] = uint8(tmp)
|
|
tmp = uint16(src.Pix[si+1]) * 0xff / uint16(a)
|
|
dst.Pix[di+1] = uint8(tmp)
|
|
tmp = uint16(src.Pix[si+2]) * 0xff / uint16(a)
|
|
dst.Pix[di+2] = uint8(tmp)
|
|
}
|
|
|
|
di += 4
|
|
si += 4
|
|
}
|
|
}
|
|
})
|
|
|
|
case *image.RGBA64:
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
si := src.PixOffset(srcMinX, srcMinY+dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
a := src.Pix[si+6]
|
|
dst.Pix[di+3] = a
|
|
|
|
switch a {
|
|
case 0:
|
|
dst.Pix[di+0] = 0
|
|
dst.Pix[di+1] = 0
|
|
dst.Pix[di+2] = 0
|
|
case 0xff:
|
|
dst.Pix[di+0] = src.Pix[si+0]
|
|
dst.Pix[di+1] = src.Pix[si+2]
|
|
dst.Pix[di+2] = src.Pix[si+4]
|
|
default:
|
|
var tmp uint16
|
|
tmp = uint16(src.Pix[si+0]) * 0xff / uint16(a)
|
|
dst.Pix[di+0] = uint8(tmp)
|
|
tmp = uint16(src.Pix[si+2]) * 0xff / uint16(a)
|
|
dst.Pix[di+1] = uint8(tmp)
|
|
tmp = uint16(src.Pix[si+4]) * 0xff / uint16(a)
|
|
dst.Pix[di+2] = uint8(tmp)
|
|
}
|
|
|
|
di += 4
|
|
si += 8
|
|
}
|
|
}
|
|
})
|
|
|
|
case *image.Gray:
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
si := src.PixOffset(srcMinX, srcMinY+dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
c := src.Pix[si]
|
|
dst.Pix[di+0] = c
|
|
dst.Pix[di+1] = c
|
|
dst.Pix[di+2] = c
|
|
dst.Pix[di+3] = 0xff
|
|
di += 4
|
|
si += 1
|
|
}
|
|
}
|
|
})
|
|
|
|
case *image.Gray16:
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
si := src.PixOffset(srcMinX, srcMinY+dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
c := src.Pix[si]
|
|
dst.Pix[di+0] = c
|
|
dst.Pix[di+1] = c
|
|
dst.Pix[di+2] = c
|
|
dst.Pix[di+3] = 0xff
|
|
di += 4
|
|
si += 2
|
|
}
|
|
}
|
|
})
|
|
|
|
case *image.YCbCr:
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
srcX := srcMinX + dstX
|
|
srcY := srcMinY + dstY
|
|
siy := src.YOffset(srcX, srcY)
|
|
sic := src.COffset(srcX, srcY)
|
|
r, g, b := color.YCbCrToRGB(src.Y[siy], src.Cb[sic], src.Cr[sic])
|
|
dst.Pix[di+0] = r
|
|
dst.Pix[di+1] = g
|
|
dst.Pix[di+2] = b
|
|
dst.Pix[di+3] = 0xff
|
|
di += 4
|
|
}
|
|
}
|
|
})
|
|
|
|
case *image.Paletted:
|
|
plen := len(src.Palette)
|
|
pnew := make([]color.NRGBA, plen)
|
|
for i := 0; i < plen; i++ {
|
|
pnew[i] = color.NRGBAModel.Convert(src.Palette[i]).(color.NRGBA)
|
|
}
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
si := src.PixOffset(srcMinX, srcMinY+dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
c := pnew[src.Pix[si]]
|
|
dst.Pix[di+0] = c.R
|
|
dst.Pix[di+1] = c.G
|
|
dst.Pix[di+2] = c.B
|
|
dst.Pix[di+3] = c.A
|
|
di += 4
|
|
si += 1
|
|
}
|
|
}
|
|
})
|
|
|
|
default:
|
|
parallel(dstH, func(partStart, partEnd int) {
|
|
for dstY := partStart; dstY < partEnd; dstY++ {
|
|
di := dst.PixOffset(0, dstY)
|
|
for dstX := 0; dstX < dstW; dstX++ {
|
|
c := color.NRGBAModel.Convert(img.At(srcMinX+dstX, srcMinY+dstY)).(color.NRGBA)
|
|
dst.Pix[di+0] = c.R
|
|
dst.Pix[di+1] = c.G
|
|
dst.Pix[di+2] = c.B
|
|
dst.Pix[di+3] = c.A
|
|
di += 4
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
return dst
|
|
}
|
|
|
|
// toNRGBA converts any image type to *image.NRGBA with min-point at (0, 0).
|
|
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)
|
|
}
|