status-im/status-go

View on GitHub
images/encode.go

Summary

Maintainability
A
0 mins
Test Coverage
A
91%
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")
}