package images import ( "bytes" "encoding/base64" "errors" "fmt" "image" "image/jpeg" "io" "regexp" "strings" "github.com/nfnt/resize" ) type EncodeConfig struct { Quality int } func Encode(w io.Writer, img image.Image, config EncodeConfig) error { // Currently a wrapper for renderJpeg, but this function is useful if multiple render formats are needed return renderJpeg(w, img, config) } func renderJpeg(w io.Writer, m image.Image, config EncodeConfig) error { o := new(jpeg.Options) o.Quality = config.Quality return jpeg.Encode(w, m, o) } type FileSizeError struct { expected int received int } func (e *FileSizeError) Error() string { return fmt.Sprintf("image size after processing exceeds max, expected < '%d', received < '%d'", e.expected, e.received) } func EncodeToLimits(bb *bytes.Buffer, img image.Image, bounds FileSizeLimits) error { q := MaxJpegQuality for q > MinJpegQuality-1 { err := Encode(bb, img, EncodeConfig{Quality: q}) if err != nil { return err } if bounds.Ideal > bb.Len() { return nil } if q == MinJpegQuality { if bounds.Max > bb.Len() { return nil } return &FileSizeError{expected: bounds.Max, received: bb.Len()} } bb.Reset() q -= 2 } return nil } // CompressToFileLimits takes an image.Image and analyses the pixel dimensions, if the longest side is greater // than the `longSideMax` image.Image will be resized, before compression begins. // Next the image.Image is repeatedly encoded and resized until the data fits within // the given FileSizeLimits. There is no limit on the number of times the cycle is performed, the image.Image // is reduced to 95% of its size at the end of every round the file size exceeds the given limits. func CompressToFileLimits(bb *bytes.Buffer, img image.Image, bounds FileSizeLimits) error { longSideMax := 2000 // Do we need to do a pre-compression resize? if img.Bounds().Max.X > img.Bounds().Max.Y { // X is longer if img.Bounds().Max.X > longSideMax { img = resize.Resize(uint(longSideMax), 0, img, resize.Bilinear) } } else { // Y is longer or equal if img.Bounds().Max.Y > longSideMax { img = resize.Resize(0, uint(longSideMax), img, resize.Bilinear) } } for { err := EncodeToLimits(bb, img, bounds) if err == nil { return nil } // If error is not a FileSizeError then we need to return it up if fse := (*FileSizeError)(nil); !errors.As(err, &fse) { return err } img = ResizeTo(95, img) } } func EncodeToBestSize(bb *bytes.Buffer, img image.Image, size ResizeDimension) error { return EncodeToLimits(bb, img, DimensionSizeLimit[size]) } func GetPayloadDataURI(payload []byte) (string, error) { if len(payload) == 0 { return "", nil } mt, err := GetMimeType(payload) if err != nil { return "", err } b64 := base64.StdEncoding.EncodeToString(payload) return "data:image/" + mt + ";base64," + b64, nil } func GetPayloadFromURI(uri string) ([]byte, error) { re := regexp.MustCompile("^data:image/(.*?);base64,(.*?)$") res := re.FindStringSubmatch(uri) if len(res) != 3 { return nil, errors.New("wrong uri format") } return base64.StdEncoding.DecodeString(res[2]) } func IsPayloadDataURI(uri string) bool { return strings.HasPrefix(uri, "data:image") }