diff --git a/_assets/tests/IMG_1205.HEIC.jpg b/_assets/tests/IMG_1205.HEIC.jpg new file mode 100644 index 000000000..28c0e6241 Binary files /dev/null and b/_assets/tests/IMG_1205.HEIC.jpg differ diff --git a/images/encode.go b/images/encode.go index d590e77da..8d1832365 100644 --- a/images/encode.go +++ b/images/encode.go @@ -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]) } diff --git a/images/encode_test.go b/images/encode_test.go index 35bc25369..259f3ea01 100644 --- a/images/encode_test.go +++ b/images/encode_test.go @@ -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()) +} diff --git a/images/main.go b/images/main.go index 1e04b142a..bcee823b5 100644 --- a/images/main.go +++ b/images/main.go @@ -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 diff --git a/images/manipulation.go b/images/manipulation.go index a954291c0..43cf85973 100644 --- a/images/manipulation.go +++ b/images/manipulation.go @@ -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 diff --git a/images/meta.go b/images/meta.go index e412fd387..6cacc07b0 100644 --- a/images/meta.go +++ b/images/meta.go @@ -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 } diff --git a/protocol/messenger.go b/protocol/messenger.go index c65cd33cd..17745eb8f 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -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