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 }