mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 05:12:25 -05:00 
			
		
		
		
	* use disintegration/imaging instead of nfnt/resize * update tests * use disintegration lib for thumbing (if necessary)
		
			
				
	
	
		
			444 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			444 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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"
 | |
| )
 | |
| 
 | |
| 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...)
 | |
| }
 | |
| 
 | |
| // Format is an image file format.
 | |
| type Format int
 | |
| 
 | |
| // Image file formats.
 | |
| const (
 | |
| 	JPEG Format = iota
 | |
| 	PNG
 | |
| 	GIF
 | |
| 	TIFF
 | |
| 	BMP
 | |
| )
 | |
| 
 | |
| var formatExts = map[string]Format{
 | |
| 	"jpg":  JPEG,
 | |
| 	"jpeg": JPEG,
 | |
| 	"png":  PNG,
 | |
| 	"gif":  GIF,
 | |
| 	"tif":  TIFF,
 | |
| 	"tiff": TIFF,
 | |
| 	"bmp":  BMP,
 | |
| }
 | |
| 
 | |
| var formatNames = map[Format]string{
 | |
| 	JPEG: "JPEG",
 | |
| 	PNG:  "PNG",
 | |
| 	GIF:  "GIF",
 | |
| 	TIFF: "TIFF",
 | |
| 	BMP:  "BMP",
 | |
| }
 | |
| 
 | |
| func (f Format) String() string {
 | |
| 	return formatNames[f]
 | |
| }
 | |
| 
 | |
| // ErrUnsupportedFormat means the given image format is not supported.
 | |
| var ErrUnsupportedFormat = errors.New("imaging: unsupported image format")
 | |
| 
 | |
| // FormatFromExtension parses image format from filename extension:
 | |
| // "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported.
 | |
| func FormatFromExtension(ext string) (Format, error) {
 | |
| 	if f, ok := formatExts[strings.ToLower(strings.TrimPrefix(ext, "."))]; ok {
 | |
| 		return f, nil
 | |
| 	}
 | |
| 	return -1, ErrUnsupportedFormat
 | |
| }
 | |
| 
 | |
| // FormatFromFilename parses image format from filename:
 | |
| // "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)
 | |
| }
 | |
| 
 | |
| 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)
 | |
| 	}
 | |
| 
 | |
| 	switch format {
 | |
| 	case JPEG:
 | |
| 		if nrgba, ok := img.(*image.NRGBA); ok && nrgba.Opaque() {
 | |
| 			rgba := &image.RGBA{
 | |
| 				Pix:    nrgba.Pix,
 | |
| 				Stride: nrgba.Stride,
 | |
| 				Rect:   nrgba.Rect,
 | |
| 			}
 | |
| 			return jpeg.Encode(w, rgba, &jpeg.Options{Quality: cfg.jpegQuality})
 | |
| 		}
 | |
| 		return jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality})
 | |
| 
 | |
| 	case PNG:
 | |
| 		encoder := png.Encoder{CompressionLevel: cfg.pngCompressionLevel}
 | |
| 		return encoder.Encode(w, img)
 | |
| 
 | |
| 	case GIF:
 | |
| 		return gif.Encode(w, img, &gif.Options{
 | |
| 			NumColors: cfg.gifNumColors,
 | |
| 			Quantizer: cfg.gifQuantizer,
 | |
| 			Drawer:    cfg.gifDrawer,
 | |
| 		})
 | |
| 
 | |
| 	case TIFF:
 | |
| 		return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
 | |
| 
 | |
| 	case BMP:
 | |
| 		return bmp.Encode(w, img)
 | |
| 	}
 | |
| 
 | |
| 	return ErrUnsupportedFormat
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 	}
 | |
| 	err = Encode(file, img, f, opts...)
 | |
| 	errc := file.Close()
 | |
| 	if err == nil {
 | |
| 		err = errc
 | |
| 	}
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 |