281 lines
6.3 KiB
Go
281 lines
6.3 KiB
Go
|
package imaging
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"image"
|
||
|
"image/color"
|
||
|
"image/draw"
|
||
|
"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")
|
||
|
)
|
||
|
|
||
|
type fileSystem interface {
|
||
|
Create(string) (io.WriteCloser, error)
|
||
|
Open(string) (io.ReadCloser, error)
|
||
|
}
|
||
|
|
||
|
type localFS struct{}
|
||
|
|
||
|
func (localFS) Create(name string) (io.WriteCloser, error) { return os.Create(name) }
|
||
|
func (localFS) Open(name string) (io.ReadCloser, error) { return os.Open(name) }
|
||
|
|
||
|
var fs fileSystem = localFS{}
|
||
|
|
||
|
// Decode reads an image from r.
|
||
|
func Decode(r io.Reader) (image.Image, error) {
|
||
|
img, _, err := image.Decode(r)
|
||
|
return img, err
|
||
|
}
|
||
|
|
||
|
// Open loads an image from file
|
||
|
func Open(filename string) (image.Image, error) {
|
||
|
file, err := fs.Open(filename)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer file.Close()
|
||
|
return Decode(file)
|
||
|
}
|
||
|
|
||
|
type encodeConfig struct {
|
||
|
jpegQuality int
|
||
|
gifNumColors int
|
||
|
gifQuantizer draw.Quantizer
|
||
|
gifDrawer draw.Drawer
|
||
|
pngCompressionLevel png.CompressionLevel
|
||
|
}
|
||
|
|
||
|
var defaultEncodeConfig = encodeConfig{
|
||
|
jpegQuality: 95,
|
||
|
gifNumColors: 256,
|
||
|
gifQuantizer: nil,
|
||
|
gifDrawer: nil,
|
||
|
pngCompressionLevel: png.DefaultCompression,
|
||
|
}
|
||
|
|
||
|
// EncodeOption sets an optional parameter for the Encode and Save functions.
|
||
|
type EncodeOption func(*encodeConfig)
|
||
|
|
||
|
// JPEGQuality returns an EncodeOption that sets the output JPEG quality.
|
||
|
// Quality ranges from 1 to 100 inclusive, higher is better. Default is 95.
|
||
|
func JPEGQuality(quality int) EncodeOption {
|
||
|
return func(c *encodeConfig) {
|
||
|
c.jpegQuality = quality
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// GIFNumColors returns an EncodeOption that sets the maximum number of colors
|
||
|
// used in the GIF-encoded image. It ranges from 1 to 256. Default is 256.
|
||
|
func GIFNumColors(numColors int) EncodeOption {
|
||
|
return func(c *encodeConfig) {
|
||
|
c.gifNumColors = numColors
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// GIFQuantizer returns an EncodeOption that sets the quantizer that is used to produce
|
||
|
// a palette of the GIF-encoded image.
|
||
|
func GIFQuantizer(quantizer draw.Quantizer) EncodeOption {
|
||
|
return func(c *encodeConfig) {
|
||
|
c.gifQuantizer = quantizer
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// GIFDrawer returns an EncodeOption that sets the drawer that is used to convert
|
||
|
// the source image to the desired palette of the GIF-encoded image.
|
||
|
func GIFDrawer(drawer draw.Drawer) EncodeOption {
|
||
|
return func(c *encodeConfig) {
|
||
|
c.gifDrawer = drawer
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// PNGCompressionLevel returns an EncodeOption that sets the compression level
|
||
|
// of the PNG-encoded image. Default is png.DefaultCompression.
|
||
|
func PNGCompressionLevel(level png.CompressionLevel) EncodeOption {
|
||
|
return func(c *encodeConfig) {
|
||
|
c.pngCompressionLevel = level
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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, opts ...EncodeOption) error {
|
||
|
cfg := defaultEncodeConfig
|
||
|
for _, option := range opts {
|
||
|
option(&cfg)
|
||
|
}
|
||
|
|
||
|
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: cfg.jpegQuality})
|
||
|
} else {
|
||
|
err = jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality})
|
||
|
}
|
||
|
|
||
|
case PNG:
|
||
|
enc := png.Encoder{CompressionLevel: cfg.pngCompressionLevel}
|
||
|
err = enc.Encode(w, img)
|
||
|
|
||
|
case GIF:
|
||
|
err = gif.Encode(w, img, &gif.Options{
|
||
|
NumColors: cfg.gifNumColors,
|
||
|
Quantizer: cfg.gifQuantizer,
|
||
|
Drawer: cfg.gifDrawer,
|
||
|
})
|
||
|
|
||
|
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.
|
||
|
//
|
||
|
// Examples:
|
||
|
//
|
||
|
// // Save the image as PNG.
|
||
|
// err := imaging.Save(img, "out.png")
|
||
|
//
|
||
|
// // Save the image as JPEG with optional quality parameter set to 80.
|
||
|
// err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80))
|
||
|
//
|
||
|
func Save(img image.Image, filename string, opts ...EncodeOption) (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 := fs.Create(filename)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
defer func() {
|
||
|
cerr := file.Close()
|
||
|
if err == nil {
|
||
|
err = cerr
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
return Encode(file, img, f, opts...)
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// Fill the first row.
|
||
|
i := 0
|
||
|
for x := 0; x < width; x++ {
|
||
|
dst.Pix[i+0] = c.R
|
||
|
dst.Pix[i+1] = c.G
|
||
|
dst.Pix[i+2] = c.B
|
||
|
dst.Pix[i+3] = c.A
|
||
|
i += 4
|
||
|
}
|
||
|
|
||
|
// Copy the first row to other rows.
|
||
|
size := width * 4
|
||
|
parallel(1, height, func(ys <-chan int) {
|
||
|
for y := range ys {
|
||
|
i = y * dst.Stride
|
||
|
copy(dst.Pix[i:i+size], dst.Pix[0:size])
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return dst
|
||
|
}
|
||
|
|
||
|
// Clone returns a copy of the given image.
|
||
|
func Clone(img image.Image) *image.NRGBA {
|
||
|
src := newScanner(img)
|
||
|
dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
|
||
|
size := src.w * 4
|
||
|
parallel(0, src.h, func(ys <-chan int) {
|
||
|
for y := range ys {
|
||
|
i := y * dst.Stride
|
||
|
src.scan(0, y, src.w, y+1, dst.Pix[i:i+size])
|
||
|
}
|
||
|
})
|
||
|
return dst
|
||
|
}
|