Implemented more comprehensive file compression to handle large files

This commit is contained in:
Samuel Hawksby-Robinson 2022-09-02 14:59:52 +01:00
parent 2f15730003
commit 45b287370a
7 changed files with 63 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View file

@ -9,6 +9,8 @@ import (
"image/jpeg"
"io"
"regexp"
"github.com/nfnt/resize"
)
type EncodeConfig struct {
@ -27,7 +29,7 @@ func renderJpeg(w io.Writer, m image.Image, config EncodeConfig) error {
return jpeg.Encode(w, m, o)
}
func EncodeToLimits(bb *bytes.Buffer, img image.Image, bounds DimensionLimits) error {
func EncodeToLimits(bb *bytes.Buffer, img image.Image, bounds FileSizeLimits) error {
q := MaxJpegQuality
for q > MinJpegQuality-1 {
@ -58,6 +60,40 @@ func EncodeToLimits(bb *bytes.Buffer, img image.Image, bounds DimensionLimits) e
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 err.Error()[:50] != "image size after processing exceeds max, expect < " {
return err
}
img = ResizeTo(95, img)
}
}
func EncodeToBestSize(bb *bytes.Buffer, img image.Image, size ResizeDimension) error {
return EncodeToLimits(bb, img, DimensionSizeLimit[size])
}

View file

@ -90,3 +90,13 @@ func TestEncodeToBestSize(t *testing.T) {
}
}
}
func TestCompressToFileLimits(t *testing.T) {
img, err := Decode(path + "IMG_1205.HEIC.jpg")
require.NoError(t, err)
bb := bytes.NewBuffer([]byte{})
err = CompressToFileLimits(bb, img, FileSizeLimits{50000, 350000})
require.NoError(t, err)
require.Equal(t, 291645, bb.Len())
}

View file

@ -80,8 +80,7 @@ func GenerateBannerImage(filepath string, aX, aY, bX, bY int) (*IdentityImage, e
return nil, err
}
dimension := BannerDim
resizedImg := ShrinkOnly(dimension, croppedImg)
resizedImg := ShrinkOnly(BannerDim, croppedImg)
sizeLimits := GetBannerDimensionLimits()
@ -97,7 +96,7 @@ func GenerateBannerImage(filepath string, aX, aY, bX, bY int) (*IdentityImage, e
Width: resizedImg.Bounds().Dx(),
Height: resizedImg.Bounds().Dy(),
FileSize: bb.Len(),
ResizeTarget: int(dimension),
ResizeTarget: int(BannerDim),
}
return ii, nil

View file

@ -28,6 +28,13 @@ func Resize(size ResizeDimension, img image.Image) image.Image {
return resize.Resize(width, height, img, resize.Bilinear)
}
func ResizeTo(percent int, img image.Image) image.Image {
width := uint(img.Bounds().Max.X * percent / 100)
height := uint(img.Bounds().Max.Y * percent / 100)
return resize.Resize(width, height, img, resize.Bilinear)
}
func ShrinkOnly(size ResizeDimension, img image.Image) image.Image {
finalSize := int(math.Min(float64(size), math.Min(float64(img.Bounds().Dx()), float64(img.Bounds().Dy()))))
return Resize(ResizeDimension(finalSize), img)
@ -50,9 +57,9 @@ func Crop(img image.Image, rect image.Rectangle) (image.Image, error) {
})
}
// CropImage takes an image, usually downloaded from a URL
// CropCenter takes an image, usually downloaded from a URL
// If the image is square, the full image is returned
// It the image is rectangular, the largest central square is returned
// If the image is rectangular, the largest central square is returned
// calculations at _docs/image-center-crop-calculations.png
func CropCenter(img image.Image) (image.Image, error) {
var cropRect image.Rectangle

View file

@ -31,7 +31,7 @@ var (
// DimensionSizeLimit the size limits imposed on each resize dimension
// Figures are based on the following sample data https://github.com/status-im/status-mobile/issues/11047#issuecomment-694970473
DimensionSizeLimit = map[ResizeDimension]DimensionLimits{
DimensionSizeLimit = map[ResizeDimension]FileSizeLimits{
SmallDim: {
Ideal: 2560, // Base on the largest sample image at quality 60% (2,554 bytes ∴ 1024 * 2.5)
Max: 5632, // Base on the largest sample image at quality 80% + 50% margin (3,683 bytes * 1.5 ≈ 5500 ∴ 1024 * 5.5)
@ -55,7 +55,7 @@ var (
}
)
type DimensionLimits struct {
type FileSizeLimits struct {
Ideal int
Max int
}
@ -63,8 +63,8 @@ type DimensionLimits struct {
type ImageType uint
type ResizeDimension uint
func GetBannerDimensionLimits() DimensionLimits {
return DimensionLimits{
func GetBannerDimensionLimits() FileSizeLimits {
return FileSizeLimits{
Ideal: 307200, // We want to save space and traffic but keep to maximum compression
Max: 460800, // Can't go bigger than 450 KB
}

View file

@ -2631,7 +2631,7 @@ func (m *Messenger) OpenAndAdjustImage(inputImage userimage.CroppedImage, crop b
}
bb := bytes.NewBuffer([]byte{})
err = userimage.EncodeToLimits(bb, img, userimage.DimensionLimits{Ideal: idealTargetImageSize, Max: resizeTargetImageSize})
err = userimage.CompressToFileLimits(bb, img, userimage.FileSizeLimits{Ideal: idealTargetImageSize, Max: resizeTargetImageSize})
if err != nil {
return nil, err