github.com/status-im/status-go@v1.1.0/images/encode.go (about)

     1  package images
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"image"
     9  	"image/jpeg"
    10  	"io"
    11  	"regexp"
    12  	"strings"
    13  
    14  	"github.com/nfnt/resize"
    15  )
    16  
    17  type EncodeConfig struct {
    18  	Quality int
    19  }
    20  
    21  func Encode(w io.Writer, img image.Image, config EncodeConfig) error {
    22  	// Currently a wrapper for renderJpeg, but this function is useful if multiple render formats are needed
    23  	return renderJpeg(w, img, config)
    24  }
    25  
    26  func renderJpeg(w io.Writer, m image.Image, config EncodeConfig) error {
    27  	o := new(jpeg.Options)
    28  	o.Quality = config.Quality
    29  
    30  	return jpeg.Encode(w, m, o)
    31  }
    32  
    33  type FileSizeError struct {
    34  	expected int
    35  	received int
    36  }
    37  
    38  func (e *FileSizeError) Error() string {
    39  	return fmt.Sprintf("image size after processing exceeds max, expected < '%d', received < '%d'", e.expected, e.received)
    40  }
    41  
    42  func EncodeToLimits(bb *bytes.Buffer, img image.Image, bounds FileSizeLimits) error {
    43  	q := MaxJpegQuality
    44  	for q > MinJpegQuality-1 {
    45  
    46  		err := Encode(bb, img, EncodeConfig{Quality: q})
    47  		if err != nil {
    48  			return err
    49  		}
    50  
    51  		if bounds.Ideal > bb.Len() {
    52  			return nil
    53  		}
    54  
    55  		if q == MinJpegQuality {
    56  			if bounds.Max > bb.Len() {
    57  				return nil
    58  			}
    59  			return &FileSizeError{expected: bounds.Max, received: bb.Len()}
    60  		}
    61  
    62  		bb.Reset()
    63  		q -= 2
    64  	}
    65  
    66  	return nil
    67  }
    68  
    69  // CompressToFileLimits takes an image.Image and analyses the pixel dimensions, if the longest side is greater
    70  // than the `longSideMax` image.Image will be resized, before compression begins.
    71  // Next the image.Image is repeatedly encoded and resized until the data fits within
    72  // the given FileSizeLimits. There is no limit on the number of times the cycle is performed, the image.Image
    73  // is reduced to 95% of its size at the end of every round the file size exceeds the given limits.
    74  func CompressToFileLimits(bb *bytes.Buffer, img image.Image, bounds FileSizeLimits) error {
    75  	longSideMax := 2000
    76  
    77  	// Do we need to do a pre-compression resize?
    78  	if img.Bounds().Max.X > img.Bounds().Max.Y {
    79  		// X is longer
    80  		if img.Bounds().Max.X > longSideMax {
    81  			img = resize.Resize(uint(longSideMax), 0, img, resize.Bilinear)
    82  		}
    83  	} else {
    84  		// Y is longer or equal
    85  		if img.Bounds().Max.Y > longSideMax {
    86  			img = resize.Resize(0, uint(longSideMax), img, resize.Bilinear)
    87  		}
    88  	}
    89  
    90  	for {
    91  		err := EncodeToLimits(bb, img, bounds)
    92  		if err == nil {
    93  			return nil
    94  		}
    95  		// If error is not a FileSizeError then we need to return it up
    96  		if fse := (*FileSizeError)(nil); !errors.As(err, &fse) {
    97  			return err
    98  		}
    99  
   100  		img = ResizeTo(95, img)
   101  	}
   102  }
   103  
   104  func EncodeToBestSize(bb *bytes.Buffer, img image.Image, size ResizeDimension) error {
   105  	return EncodeToLimits(bb, img, DimensionSizeLimit[size])
   106  }
   107  
   108  func GetPayloadDataURI(payload []byte) (string, error) {
   109  	if len(payload) == 0 {
   110  		return "", nil
   111  	}
   112  
   113  	mt, err := GetMimeType(payload)
   114  	if err != nil {
   115  		return "", err
   116  	}
   117  
   118  	b64 := base64.StdEncoding.EncodeToString(payload)
   119  
   120  	return "data:image/" + mt + ";base64," + b64, nil
   121  }
   122  
   123  func GetPayloadFromURI(uri string) ([]byte, error) {
   124  	re := regexp.MustCompile("^data:image/(.*?);base64,(.*?)$")
   125  	res := re.FindStringSubmatch(uri)
   126  	if len(res) != 3 {
   127  		return nil, errors.New("wrong uri format")
   128  	}
   129  	return base64.StdEncoding.DecodeString(res[2])
   130  }
   131  
   132  func IsPayloadDataURI(uri string) bool {
   133  	return strings.HasPrefix(uri, "data:image")
   134  }