| package imaging |
| |
| import ( |
| "encoding/binary" |
| "errors" |
| "image" |
| "image/draw" |
| "image/gif" |
| "image/jpeg" |
| "image/png" |
| "io" |
| "io/ioutil" |
| "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 formatFromExt = map[string]Format{ |
| "jpg": JPEG, |
| "jpeg": JPEG, |
| "png": PNG, |
| "tif": TIFF, |
| "tiff": TIFF, |
| "bmp": BMP, |
| "gif": GIF, |
| } |
| |
| // FormatFromExtension parses image format from extension: |
| // "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. |
| func FormatFromExtension(ext string) (Format, error) { |
| if f, ok := formatFromExt[strings.ToLower(strings.TrimPrefix(ext, "."))]; ok { |
| return f, nil |
| } |
| return -1, ErrUnsupportedFormat |
| } |
| |
| // FormatFromFilename parses image format from filename extension: |
| // "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. |
| func FormatFromFilename(filename string) (Format, error) { |
| ext := filepath.Ext(filename) |
| return FormatFromExtension(ext) |
| } |
| |
| 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{} |
| |
| type decodeConfig struct { |
| autoOrientation bool |
| } |
| |
| var defaultDecodeConfig = decodeConfig{ |
| autoOrientation: false, |
| } |
| |
| // DecodeOption sets an optional parameter for the Decode and Open functions. |
| type DecodeOption func(*decodeConfig) |
| |
| // AutoOrientation returns a DecodeOption that sets the auto-orientation mode. |
| // If auto-orientation is enabled, the image will be transformed after decoding |
| // according to the EXIF orientation tag (if present). By default it's disabled. |
| func AutoOrientation(enabled bool) DecodeOption { |
| return func(c *decodeConfig) { |
| c.autoOrientation = enabled |
| } |
| } |
| |
| // Decode reads an image from r. |
| func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) { |
| cfg := defaultDecodeConfig |
| for _, option := range opts { |
| option(&cfg) |
| } |
| |
| if !cfg.autoOrientation { |
| img, _, err := image.Decode(r) |
| return img, err |
| } |
| |
| var orient orientation |
| pr, pw := io.Pipe() |
| r = io.TeeReader(r, pw) |
| done := make(chan struct{}) |
| go func() { |
| defer close(done) |
| orient = readOrientation(pr) |
| io.Copy(ioutil.Discard, pr) |
| }() |
| |
| img, _, err := image.Decode(r) |
| pw.Close() |
| <-done |
| if err != nil { |
| return nil, err |
| } |
| |
| return fixOrientation(img, orient), nil |
| } |
| |
| // Open loads an image from file. |
| // |
| // Examples: |
| // |
| // // Load an image from file. |
| // img, err := imaging.Open("test.jpg") |
| // |
| // // Load an image and transform it depending on the EXIF orientation tag (if present). |
| // img, err := imaging.Open("test.jpg", imaging.AutoOrientation(true)) |
| // |
| func Open(filename string, opts ...DecodeOption) (image.Image, error) { |
| file, err := fs.Open(filename) |
| if err != nil { |
| return nil, err |
| } |
| defer file.Close() |
| return Decode(file, opts...) |
| } |
| |
| 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) { |
| f, err := FormatFromFilename(filename) |
| if err != nil { |
| return err |
| } |
| 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...) |
| } |
| |
| // orientation is an EXIF flag that specifies the transformation |
| // that should be applied to image to display it correctly. |
| type orientation int |
| |
| const ( |
| orientationUnspecified = 0 |
| orientationNormal = 1 |
| orientationFlipH = 2 |
| orientationRotate180 = 3 |
| orientationFlipV = 4 |
| orientationTranspose = 5 |
| orientationRotate270 = 6 |
| orientationTransverse = 7 |
| orientationRotate90 = 8 |
| ) |
| |
| // readOrientation tries to read the orientation EXIF flag from image data in r. |
| // If the EXIF data block is not found or the orientation flag is not found |
| // or any other error occures while reading the data, it returns the |
| // orientationUnspecified (0) value. |
| func readOrientation(r io.Reader) orientation { |
| const ( |
| markerSOI = 0xffd8 |
| markerAPP1 = 0xffe1 |
| exifHeader = 0x45786966 |
| byteOrderBE = 0x4d4d |
| byteOrderLE = 0x4949 |
| orientationTag = 0x0112 |
| ) |
| |
| // Check if JPEG SOI marker is present. |
| var soi uint16 |
| if err := binary.Read(r, binary.BigEndian, &soi); err != nil { |
| return orientationUnspecified |
| } |
| if soi != markerSOI { |
| return orientationUnspecified // Missing JPEG SOI marker. |
| } |
| |
| // Find JPEG APP1 marker. |
| for { |
| var marker, size uint16 |
| if err := binary.Read(r, binary.BigEndian, &marker); err != nil { |
| return orientationUnspecified |
| } |
| if err := binary.Read(r, binary.BigEndian, &size); err != nil { |
| return orientationUnspecified |
| } |
| if marker>>8 != 0xff { |
| return orientationUnspecified // Invalid JPEG marker. |
| } |
| if marker == markerAPP1 { |
| break |
| } |
| if size < 2 { |
| return orientationUnspecified // Invalid block size. |
| } |
| if _, err := io.CopyN(ioutil.Discard, r, int64(size-2)); err != nil { |
| return orientationUnspecified |
| } |
| } |
| |
| // Check if EXIF header is present. |
| var header uint32 |
| if err := binary.Read(r, binary.BigEndian, &header); err != nil { |
| return orientationUnspecified |
| } |
| if header != exifHeader { |
| return orientationUnspecified |
| } |
| if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { |
| return orientationUnspecified |
| } |
| |
| // Read byte order information. |
| var ( |
| byteOrderTag uint16 |
| byteOrder binary.ByteOrder |
| ) |
| if err := binary.Read(r, binary.BigEndian, &byteOrderTag); err != nil { |
| return orientationUnspecified |
| } |
| switch byteOrderTag { |
| case byteOrderBE: |
| byteOrder = binary.BigEndian |
| case byteOrderLE: |
| byteOrder = binary.LittleEndian |
| default: |
| return orientationUnspecified // Invalid byte order flag. |
| } |
| if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { |
| return orientationUnspecified |
| } |
| |
| // Skip the EXIF offset. |
| var offset uint32 |
| if err := binary.Read(r, byteOrder, &offset); err != nil { |
| return orientationUnspecified |
| } |
| if offset < 8 { |
| return orientationUnspecified // Invalid offset value. |
| } |
| if _, err := io.CopyN(ioutil.Discard, r, int64(offset-8)); err != nil { |
| return orientationUnspecified |
| } |
| |
| // Read the number of tags. |
| var numTags uint16 |
| if err := binary.Read(r, byteOrder, &numTags); err != nil { |
| return orientationUnspecified |
| } |
| |
| // Find the orientation tag. |
| for i := 0; i < int(numTags); i++ { |
| var tag uint16 |
| if err := binary.Read(r, byteOrder, &tag); err != nil { |
| return orientationUnspecified |
| } |
| if tag != orientationTag { |
| if _, err := io.CopyN(ioutil.Discard, r, 10); err != nil { |
| return orientationUnspecified |
| } |
| continue |
| } |
| if _, err := io.CopyN(ioutil.Discard, r, 6); err != nil { |
| return orientationUnspecified |
| } |
| var val uint16 |
| if err := binary.Read(r, byteOrder, &val); err != nil { |
| return orientationUnspecified |
| } |
| if val < 1 || val > 8 { |
| return orientationUnspecified // Invalid tag value. |
| } |
| return orientation(val) |
| } |
| return orientationUnspecified // Missing orientation tag. |
| } |
| |
| // fixOrientation applies a transform to img corresponding to the given orientation flag. |
| func fixOrientation(img image.Image, o orientation) image.Image { |
| switch o { |
| case orientationNormal: |
| case orientationFlipH: |
| img = FlipH(img) |
| case orientationFlipV: |
| img = FlipV(img) |
| case orientationRotate90: |
| img = Rotate90(img) |
| case orientationRotate180: |
| img = Rotate180(img) |
| case orientationRotate270: |
| img = Rotate270(img) |
| case orientationTranspose: |
| img = Transpose(img) |
| case orientationTransverse: |
| img = Transverse(img) |
| } |
| return img |
| } |