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" "image/jpeg"
"io" "io"
"regexp" "regexp"
"github.com/nfnt/resize"
) )
type EncodeConfig struct { type EncodeConfig struct {
@ -27,7 +29,7 @@ func renderJpeg(w io.Writer, m image.Image, config EncodeConfig) error {
return jpeg.Encode(w, m, o) 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 q := MaxJpegQuality
for q > MinJpegQuality-1 { for q > MinJpegQuality-1 {
@ -58,6 +60,40 @@ func EncodeToLimits(bb *bytes.Buffer, img image.Image, bounds DimensionLimits) e
return nil 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 { func EncodeToBestSize(bb *bytes.Buffer, img image.Image, size ResizeDimension) error {
return EncodeToLimits(bb, img, DimensionSizeLimit[size]) 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 return nil, err
} }
dimension := BannerDim resizedImg := ShrinkOnly(BannerDim, croppedImg)
resizedImg := ShrinkOnly(dimension, croppedImg)
sizeLimits := GetBannerDimensionLimits() sizeLimits := GetBannerDimensionLimits()
@ -97,7 +96,7 @@ func GenerateBannerImage(filepath string, aX, aY, bX, bY int) (*IdentityImage, e
Width: resizedImg.Bounds().Dx(), Width: resizedImg.Bounds().Dx(),
Height: resizedImg.Bounds().Dy(), Height: resizedImg.Bounds().Dy(),
FileSize: bb.Len(), FileSize: bb.Len(),
ResizeTarget: int(dimension), ResizeTarget: int(BannerDim),
} }
return ii, nil 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) 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 { 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())))) finalSize := int(math.Min(float64(size), math.Min(float64(img.Bounds().Dx()), float64(img.Bounds().Dy()))))
return Resize(ResizeDimension(finalSize), img) 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 // 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 // calculations at _docs/image-center-crop-calculations.png
func CropCenter(img image.Image) (image.Image, error) { func CropCenter(img image.Image) (image.Image, error) {
var cropRect image.Rectangle var cropRect image.Rectangle

View File

@ -31,7 +31,7 @@ var (
// DimensionSizeLimit the size limits imposed on each resize dimension // 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 // 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: { SmallDim: {
Ideal: 2560, // Base on the largest sample image at quality 60% (2,554 bytes ∴ 1024 * 2.5) 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) 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 Ideal int
Max int Max int
} }
@ -63,8 +63,8 @@ type DimensionLimits struct {
type ImageType uint type ImageType uint
type ResizeDimension uint type ResizeDimension uint
func GetBannerDimensionLimits() DimensionLimits { func GetBannerDimensionLimits() FileSizeLimits {
return DimensionLimits{ return FileSizeLimits{
Ideal: 307200, // We want to save space and traffic but keep to maximum compression Ideal: 307200, // We want to save space and traffic but keep to maximum compression
Max: 460800, // Can't go bigger than 450 KB 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{}) 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 { if err != nil {
return nil, err return nil, err