mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 23:52:26 -06: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
 | 
						|
}
 |