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 }