Image resizing and compression

Image resizing and compression #

When sending an image to the Blicker reading endpoint, it is often important to quickly get a response back, as this allows for a more responsive meter reading application for the end user. A great contributing factor in the response time is often the resolution and file size of the images that are sent to the Blicker reading endpoint. Too large file sizes result in increased data transfer time, but also increased chances of connection dropouts. Luckily the image file size can often be greatly reduced with only a negligible decrease in readout performance. We therefore provide some guidelines on the resizing and compression of images before sending them to Blicker.

In this tutorial we explain how to perform image resizing and compression, and provide code snippets in multiple programming languages and frameworks showing how this can be implemented. Because of the tradeoff between image file size and the readout performance of Blicker, we also conduct some experiments to determine recommended parameters to be used for resizing and compression. Finally we provide full code examples in which both resizing and compression are performed, for completeness.

Resizing #

As explained on the page about the image request parameter, an image must be at least 320 pixels on each side. When an image is smaller than this, we do not advise to upscale it, as too small images often simply do not contain enough detail. Instead we advise to take a new, higher resolution image. However when the image is more than 2,000 pixels on any side, it will get downscaled by Blicker anyway. It is thus not advised to sent too large images either.

The algorithm for doing this can thus be summarized in pseudo-code as follows:

smallest_image_side := min(image_height, image_width)
largest_image_side := max(image_height, image_width)
resize_scale := 1

if smallest_image_side is smaller than 320 
    reject image
    
if largest_image_side is larger than 2000
    // This resize scale makes that the largest image side becomes 2000
    resize_scale := 2000 / largest_image_side
    
    // Avoid the smallest image side from becoming smaller than 320
    if (smallest_image_side * resize_scale) is smaller than 320
        resize_scale := 320 / smallest_image_side
        
resize image to (image_height * resize_scale, image_width * resize_scale)  

When downscaling images, any pixel in the smaller output image should be a good representation of multiple pixels in the larger input image. There are multiple algorithms to do this, which we call interpolation methods. Ideally, the used interpolation method results in a good visual representation of the original image, without needless loss of detail, while doing this fast. In the code snippets below we have chosen to use bilinear interpolation where possible, as it is relatively high-quality, and common interpolation method for both up- and downscaling, without having too high computational complexity.

import PIL.Image
import numpy as np


def get_resize_scale(
    image_height: int, image_width: int, min_side: int, max_side: int,
) -> float:
    smallest_image_side = min(image_height, image_width)
    largest_image_side = max(image_height, image_width)

    # Too small images do not contain enough detail
    if smallest_image_side < min_side:
        raise Exception("Image is too small")

    if largest_image_side > max_side:
        # Scale such that the largest image will be 2,000 pixels
        scale = max_side / largest_image_side

        # Prevent the image to become smaller than 320 pixels after downscaling for
        # images with extreme aspect ratios. In that case, allow the largest image side
        # to be larger than 2,000 pixels
        if round(smallest_image_side * scale) < min_side:
            scale = min_side / smallest_image_side
    else:
        # Do not scale image which are already smaller than 2,000 pixels
        scale = 1

    return scale


def resize_image(
    image: np.ndarray,
    min_side: int = 320,
    max_side: int = 2000,
    interpolation_method: int = PIL.Image.BILINEAR,
) -> np.ndarray:
    image_height, image_width = image.shape[:2]
    resize_scale = get_resize_scale(
        image_height=image_height,
        image_width=image_width,
        min_side=min_side,
        max_side=max_side,
    )

    if resize_scale == 1:
        return image

    output_shape = (
        int(round(image_width * resize_scale)),
        int(round(image_height * resize_scale)),
    )

    image = PIL.Image.fromarray(image)
    image = image.resize(output_shape, resample=interpolation_method)
    image = np.asarray(image)

    return image


def get_random_example_image() -> np.ndarray:
    # Generate example image with random pixels
    return np.random.randint(low=0, high=255, size=(4000, 3000, 3), dtype=np.uint8)


if __name__ == "__main__":
    image = get_random_example_image()
    print(image.shape)
    resized_image = resize_image(image=image)
    print(resized_image.shape)
function getResizeScale({
    imageHeight,
    imageWidth,
    minSide,
    maxSide
}) {
    const smallestImageSide = Math.min(imageHeight, imageWidth)
    const largestImageSide = Math.max(imageHeight, imageWidth)

    // Too small images do not contain enough detail 
    if (smallestImageSide < minSide) {
        throw new Error("Image is too small");
    }

    var scale;
    if (largestImageSide > maxSide) {
        // Scale such that the largest image will be 2,000 pixels
        scale = maxSide / largestImageSide;

        // Prevent the image to become smaller than 320 pixels after downscaling for
        // images with extreme aspect ratios. In that case, allow the largest image side
        // to be larger than 2,000 pixels
        if (Math.round(smallestImageSide * scale) < minSide) {
            scale = minSide / smallestImageSide;
        }
    } else {
        // Do not scale image which are already smaller than 2,000 pixels
        scale = 1;
    }

    return scale;
}

function resizeImage({
    imageDataURL,
    minSide = 320,
    maxSide = 2000,
    callback
}) {
    const image = new Image();

    image.onload = () => {
        const imageHeight = image.height;
        const imageWidth = image.width;

        const resizeScale = getResizeScale({
            imageHeight: imageHeight,
            imageWidth: imageWidth,
            minSide: minSide,
            maxSide: maxSide
        })

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        canvas.width = Math.round(imageWidth * resizeScale);
        canvas.height = Math.round(imageHeight * resizeScale);

        // Note that the interpolation method cannot be set explicitly.
        // Bilinear interpolation is used in most browsers.
        // Some browsers do allow `imageSmoothingQuality` to be set.
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        callback(canvas);
    };

    image.src = imageDataURL;
}

function getRandomExampleImage() {
    // Generate example image/canvas with random pixels
    const canvas = document.createElement("canvas");
    canvas.height = 4000;
    canvas.width = 3000;
    const ctx = canvas.getContext("2d");
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    function randomInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    for (var i = 0; i < imageData.data.length; i += 4) {
        imageData.data[i] = randomInt(0, 255);
        imageData.data[i + 1] = randomInt(0, 255);
        imageData.data[i + 2] = randomInt(0, 255);
        imageData.data[i + 3] = 255;
    }

    ctx.putImageData(imageData, 0, 0);
    document.body.appendChild(canvas);

    return canvas
}


const canvas = getRandomExampleImage();
console.log(canvas.height, canvas.width);
resizeImage({
    imageDataURL: canvas.toDataURL(),
    callback: resizedImage => console.log(resizedImage.height, resizedImage.width)
});
package com.example.myapplication;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;

import android.graphics.Bitmap;
import android.graphics.Color;

import java.util.Random;

class Example {

    public static void main(String[] args) {
        Bitmap image = getRandomExampleImage();
        System.out.println(image.getHeight() + " " + image.getWidth());
        try {
            Bitmap resizedImage = resizeImage(image, 320, 2000);
            System.out.println(resizedImage.getHeight() + " " + resizedImage.getWidth());
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }

    protected Bitmap getRandomExampleImage() {
        // Generate example image with random pixels
        int height = 4000;
        int width = 3000;
        int[] pixels = new int[height * width];
        Random randomGenerator = new Random();

        int i = 0;
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int a = randomGenerator.nextInt(256);
                int r = randomGenerator.nextInt(256);
                int g = randomGenerator.nextInt(256);
                int b = randomGenerator.nextInt(256);

                int pixel = Color.argb(a, r, g, b);
                pixels[i] = pixel;

                i++;
            }
        }

        return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
    }

    protected double getResizeScale(int imageHeight, int imageWidth, int minSide, int maxSide) throws Exception {
        int smallestImageSide = min(imageHeight, imageWidth);
        int largestImageSide = max(imageHeight, imageWidth);

        // Too small images do not contain enough detail
        if (smallestImageSide < minSide) {
            throw new Exception("Image is too small");
        }

        double scale;
        if (largestImageSide > maxSide) {
            // Scale such that the largest image will be 2,000 pixels
            scale = ((double) maxSide) / largestImageSide;

            // Prevent the image to become smaller than 320 pixels after downscaling for
            // images with extreme aspect ratios. In that case, allow the largest image side
            // to be larger than 2,000 pixels
            if (round(smallestImageSide * scale) < minSide) {
                scale = ((double) minSide) / smallestImageSide;
            }
        } else {
            // Do not scale image which are already smaller than 2,000 pixels
            scale = 1.0;
        }

        return scale;
    }
    
    protected Bitmap resizeImage(Bitmap image, int minSide, int maxSide) throws Exception {
        int imageHeight = image.getHeight();
        int imageWidth = image.getWidth();

        double resizeScale = getResizeScale(
                imageHeight, imageWidth, minSide, maxSide
        );

        if (resizeScale == 1) {
            return image;
        }

        int scaledHeight = (int) round(imageHeight * resizeScale);
        int scaledWidth = (int) round(imageWidth * resizeScale);

        // Note that when `filter` is set to true, bilinear interpolation is used.
        // Besides nearest neighbor interpolation, no other interpolation methods are available.
        return Bitmap.createScaledBitmap(image, scaledWidth, scaledHeight, true);
    }

}
import UIKit

enum ResizeError: Error {
    case tooSmallImageError
}


func getResizeScale(
    imageHeight: Int32,
    imageWidth:Int32,
    minSide: Int32,
    maxSide: Int32
) throws -> Float32  {
    let smallestImageSide = min(imageHeight, imageWidth)
    let largestImageSide = max(imageHeight, imageWidth)
    
    // Too small images do not contain enough detail
    if smallestImageSide < minSide {
        throw ResizeError.tooSmallImageError
    }
    
    var  scale: Float32
    if largestImageSide > maxSide {
        // Scale such that the largest image will be 2,000 pixels
        scale = Float32(maxSide) / Float32(largestImageSide)
        
        // Prevent the image to become smaller than 320 pixels after downscaling for
        // images with extreme aspect ratios. In that case, allow the largest image side
        // to be larger than 2,000 pixels
        if Int32(round(Float32(smallestImageSide) * scale)) < minSide {
            scale = Float32(minSide) / Float32(smallestImageSide)
        }
    } else {
        // Do not scale image which are already smaller than 2,000 pixels
        scale = 1.0
    }
    
    return scale
}

func resizeImage(
    image: UIImage,
    minSide: Int32,
    maxSide: Int32
) throws -> UIImage? {
    let imageHeight = Int32(image.size.height)
    let imageWidth = Int32(image.size.width)
    
    let resizeScale: Float32!
    do {
        resizeScale = try getResizeScale(
            imageHeight: imageHeight,
            imageWidth: imageWidth,
            minSide: minSide,
            maxSide: maxSide
        )
    } catch is ResizeError {
        throw ResizeError.tooSmallImageError
    }
    
    if resizeScale == 1 {
        return image
    }
    
    let outputHeight = Int(round(Float32(imageHeight) * resizeScale))
    let outputWidth = Int(round(Float32(imageWidth) * resizeScale))
    
    let newSize = CGSize(width: outputWidth, height: outputHeight)
    let rect = CGRect(origin: .zero, size: newSize)
    
    // Note that the interpolation method cannot be explicitly set, but that
    // `interpolationQuality` can be used to control interpolation quality
    UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
    let context = UIGraphicsGetCurrentContext()!
    context.interpolationQuality = .default
    image.draw(in: rect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return newImage
}

public struct PixelData {
    var a: UInt8
    var r: UInt8
    var g: UInt8
    var b: UInt8
}

func getRandomExampleImage(
    height: Int,
    width: Int
) -> UIImage? {
    // Generate example image with random pixels
    var pixels = [PixelData]()
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
    let bitsPerComponent = 8
    let bitsPerPixel = 32
    
    var i = 0
    for _ in 0..<width {
        for _ in 0..<height {
            i = i + 1
            // Note that this can be quite slow for larger images
            pixels.append(
                PixelData(
                    a: .random(in: 0...255),
                    r: .random(in: 0...255),
                    g: .random(in: 0...255),
                    b: .random(in: 0...255)
                )
            )
        }
    }
    
    guard let providerRef = CGDataProvider(
        data: NSData(
            bytes: &pixels,
            length: pixels.count * MemoryLayout<PixelData>.size
        )
        )
        else { return nil }
    
    guard let cgim = CGImage(
        width: width,
        height: height,
        bitsPerComponent: bitsPerComponent,
        bitsPerPixel: bitsPerPixel,
        bytesPerRow: width * MemoryLayout<PixelData>.size,
        space: rgbColorSpace,
        bitmapInfo: bitmapInfo,
        provider: providerRef,
        decode: nil,
        shouldInterpolate: true,
        intent: .defaultIntent
        )
        else { return nil }
    
    return UIImage(cgImage: cgim)
}


guard let image = getRandomExampleImage(height:4000, width:3000)
    else {
        print("Could not initialize image") ;
        exit(-1)
}
print(image.size)
guard let resizedImage = try? resizeImage(image: image, minSide: 320, maxSide: 2000)
    else {
        print("Could not resize image")
        exit(-1)
}
print(resizedImage.size)

Compression #

Even when large images are resized to a maximum side of 2,000 pixels, the resulting file size can still be quite large without compression. For example, storing a color image of 2,000 x 2,000 pixels in raw format already has a file size of 12MB. Luckily, most image formats such as JPEG, PNG or WebP support either lossless or lossy compression to reduce the file size of an image.

Lossless compression may result in smaller file sizes than the original, which either way can still be uncompressed to exactly the same pixels values as before compression. Lossy compression on the other hand usually removes high frequency details from images. The downside of this is that the original pixel values can no longer be exactly reconstructed. The upside of this however, is that the file size can often be reduced even more than with lossless compression, and the visual differences are often almost imperceptible for humans.

For the following code snippets, we use the JPEG image format with a lossy compression rate of 10% (90% quality) as it is one of the most used image formats, supported by many frameworks and libraries and has a high compression rate with high speed. Note that we reuse the function to generate an example image from the previous code snippet.

import PIL.Image
import io
import numpy as np


def compress_image(image: np.ndarray, quality: int = 90) -> bytes:
    image = PIL.Image.fromarray(image)

    buffer = io.BytesIO()
    image.save(buffer, format="JPEG", quality=quality)
    compressed_image = buffer.getvalue()

    return compressed_image


if __name__ == "__main__":
    image = get_random_example_image()
    compressed_image = compress_image(image=image)
    print(len(compressed_image))
function compressImage({
    imageFile,
    quality = 0.9,
    callback
}) {
    const fileName = imageFile.name;
    const reader = new FileReader();

    reader.onload = event => {
        resizeImage({
            imageDataURL: event.target.result,
            callback: canvas => {
                canvas.toBlob((blob) => {
                    const file = new File([blob], fileName, {
                        type: 'image/jpeg',
                        lastModified: Date.now()
                    });
                    callback(file);
                }, 'image/jpeg', quality);
            }
        })
    };

    reader.readAsDataURL(imageFile);
}


const canvas = getRandomExampleImage();

canvas.toBlob(blob => {
    console.log(blob.size);
    compressImage({
        imageFile: blob,
        callback: compressedImage => console.log(compressedImage.size)
    });
});
package com.example.myapplication;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;

import android.graphics.Bitmap;
import android.graphics.Color;

import java.io.ByteArrayOutputStream;
import java.util.Random;

class Example {

    public static void main(String[] args) {
        Bitmap image = getRandomExampleImage();
        ByteArrayOutputStream compressedImage = compressImage(image, 90);
        System.out.println(compressedImage.size());
    }

    protected ByteArrayOutputStream compressImage(Bitmap image, int quality) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        // Note that different compress formats interpret the quality differently. For PNG, the
        // quality is ignored. For WEBP_LOSSLESS a high quality results in a small file size (with
        // high compression)
        image.compress(Bitmap.CompressFormat.JPEG, quality, stream);

        return stream;
    }
}
import UIKit


func compressImage(
    image: UIImage,
    quality: CGFloat
) -> Data? {
    let data = image.jpegData(compressionQuality: quality)
    
    return data
}


guard let image = getRandomExampleImage(height:4000, width:3000)
    else {
        print("Could not initialize image") ;
        exit(-1)
}
guard let compressedImage = compressImage(image: image, quality: 0.9)
    else {
        print("Could not compress image")
        exit(-1)
}
print(compressedImage.count)

We have conducted multiple experiments to give some intuition about the effect of the parameters chosen in resizing and compression on the processing speed and readout performance. From this we have concluded the following recommendations:

  • Standard: In the average case scenario, we recommend to start with resizing images to a maximum of 2,000 pixels using bilinear interpolation where possible and compressing images using JPEG with a quality of 90%. For iOS and Android and JavaScript we recommend default interpolation parameters.
  • Optimized for speed: When processing speed is crucial, images could be resized to a maximum of 1,500 pixels using bilinear interpolation, and compressed using JPEG with a quality of 80%. For iOS, the interpolation quality may be set to low.
  • Optimized for readout performance: For maximum readout performance, it is recommended to resize to a maximum of 2,000 pixels with area/box interpolation where possible, and compress the image with WebP lossless compression. For iOS the interpolation quality may be set to high.

To provide insight in how these parameters were determined, we show the results of a set of experiments we performed.
Note that the results of these experiments can be very specific to the used libraries, device, programming language, etc. These results are therefore only intended to be used indicatively. For this reason, we have normalized most results, and do not show, for example, the absolute wall clock time, but instead show only relative differences.

Time to resize versus interpolation method #

We first look at the different interpolation methods. We downscale a representative 4,032 x 3,024 image to increasingly smaller sizes using different interpolation methods. Note that the time to resize, shown on the y-axis is normalized by the highest observed time to resize (Lanczos). On the x-axis we show the scale with which we resize with respect to the original image size. Since an image has, of course, two dimensions, this actually shows quadratic scaling in terms of the number of pixels. E.g., a scaling of 0.5 results in an image of 2,016 x 1,512, having a fourth of the original pixels.

Line plot showing the effect of different interpolation methods on the time to resize an image.

From this plot it is clear that Lanczos interpolation takes significantly more time than other methods. Although qualitative results can sometimes be better with Lanczos interpolation, it would be more reasonable for online processing to use either nearest neighbor, bilinear or bicubic interpolation.

Time to encode versus image format #

We also compare different image formats and compression methods in terms of time to encode an image. We again use a representative image and encode it using three different image formats. The image quality for JPEG can be set granularly. A high image quality generally results in a larger file size (low compression rate), but also takes more time to encode. For PNG there is less granular control over the compression rate. In general, a higher compression rate does result in more time to encode, but with negligible differences in image quality and file size. The third image format that we benchmark is WebP, which supports both lossy and lossless compression.

Line plot showing the effect of different image formats and compression rates on the time to encode an image.

From this plot it is clear that lossless compression generally takes a lot more time than lossy compression. Only when the compression rate is near-zero, are lossless compression methods comparable to lossy compression methods in terms of encoding time.

File size versus image format #

For the different image formats we also test how much the resulting encoded file size differs, again using different compression rates. We compare this to the original image size, if we were to use no compression at all.

Line plot showing the effect of different image formats and compression rates on the image file size, including the uncompressed size.

From this it is clear that any PNG compression results in a much lower file size than the uncompressed image size. However, although increased compression effort takes increasingly more time to encode, the file size is barely reduced. For WebP with lossless compression, increased compression effort also does not result in smaller file sizes. For lossy image formats, we do clearly see a more granular control over image file size and a lower file size in general.

Combined with the lower encoding time of lossy compression methods, this motivates to use lossy compression over lossless compression in realtime applications.

Effects on performance #

Finally we show how some of the resizing and compression parameters we used above have an effect on readout performance of the Blicker reading endpoint.

For the interpolation methods used earlier, we resize images to different resize targets. In the first experiment we resize all images that are larger than 2,000 pixels on any side to 2,000 pixels. Any image smaller than that is not resized. Next we do the same, but resize to a maximum image side of 1,750. We repeat this experiment for different maximum image sides. In the final experiment, we resize images in a slightly different way. Instead of the images having a maximum image side of, for example, 500 pixels at most, we now resize all images to have a minimum image side of 320 pixels. The other side could then still be larger than 320 or even 500 pixels. For all these experiments we determine how well Blicker performs in reading out displays, which is visualized in the following figure. Note that the x-axis is not scaled evenly and that we normalize the performance such that the highest observed performance is one.

Line plot showing the effect of different image sizes on the display readout performance.

We clearly see that resizing images to a minimum side of 320 pixels has detrimental effects on the performance. With nearest neighbor interpolation, we can even see a drop in performance of almost 7% compared to the baseline performance. Resizing to a maximum image side of 1,250 pixels or higher has less of an effect. Note that these numbers are only indicative though and can be highly influenced by the content of the images.

We now also show the loss in performance when varying the image quality or compression effort for the multiple image formats. In the following experiment, all images were first resized to a maximum side of 2,000 pixels using bilinear interpolation, before being compressed.

Line plot showing the effect of different image formats and compression rates on the display readout performance,

Both PNG and WebP support lossless compression. As expected, the performance does not degrade with lossless compression at all. For lossy compression in the WebP and JPEG formats, we clearly see that a loss in image quality results in a loss in readout performance. Especially an image quality below ~20% results in a great performance reduction of almost 45%. However, with a higher image quality, the performance degradation is less significant.

This is a selection of the experiments we performed to determine the recommended resizing and compression parameters. From the results it is clear that there is always a tradeoff to be made between multiple factors such as:

  • The time to resize and compress images on the device
  • The transfer time of the image file
  • The inference time of the Blicker reading endpoint
  • The readout performance of the Blicker reading endpoint

We hope that the suggested parameters and provided experimental results provide a good starting point to find a balance between these tradeoffs.

Full examples #

For convenience, we provide code snippets in multiple programming languages and frameworks in which images are both resized and compressed.

import PIL.Image
import io
import numpy as np


def compress_image(image: np.ndarray, quality: int = 90) -> bytes:
    image = PIL.Image.fromarray(image)

    buffer = io.BytesIO()
    image.save(buffer, format="JPEG", quality=quality)
    compressed_image = buffer.getvalue()

    return compressed_image


def get_resize_scale(
    image_height: int, image_width: int, min_side: int, max_side: int,
) -> float:
    smallest_image_side = min(image_height, image_width)
    largest_image_side = max(image_height, image_width)

    # Too small images do not contain enough detail
    if smallest_image_side < min_side:
        raise Exception("Image is too small")

    if largest_image_side > max_side:
        # Scale such that the largest image will be 2,000 pixels
        scale = max_side / largest_image_side

        # Prevent the image to become smaller than 320 pixels after downscaling for
        # images with extreme aspect ratios. In that case, allow the largest image side
        # to be larger than 2,000 pixels
        if round(smallest_image_side * scale) < min_side:
            scale = min_side / smallest_image_side
    else:
        # Do not scale image which are already smaller than 2,000 pixels
        scale = 1

    return scale


def resize_image(
    image: np.ndarray,
    min_side: int = 320,
    max_side: int = 2000,
    interpolation_method: int = PIL.Image.BILINEAR,
) -> np.ndarray:
    image_height, image_width = image.shape[:2]
    resize_scale = get_resize_scale(
        image_height=image_height,
        image_width=image_width,
        min_side=min_side,
        max_side=max_side,
    )

    if resize_scale == 1:
        return image

    output_shape = (
        int(round(image_width * resize_scale)),
        int(round(image_height * resize_scale)),
    )

    image = PIL.Image.fromarray(image)
    image = image.resize(output_shape, resample=interpolation_method)
    image = np.asarray(image)

    return image


def get_random_example_image() -> np.ndarray:
    # Generate example image with random pixels
    return np.random.randint(low=0, high=255, size=(4000, 3000, 3), dtype=np.uint8)


if __name__ == "__main__":
    image = get_random_example_image()
    resized_image = resize_image(image=image)
    compressed_image = compress_image(image=resized_image)
    print(len(compressed_image))
function compressImage({
    imageFile,
    quality = 0.9,
    callback
}) {
    const fileName = imageFile.name;
    const reader = new FileReader();

    reader.onload = event => {
        resizeImage({
            imageDataURL: event.target.result,
            callback: canvas => {
                canvas.toBlob((blob) => {
                    const file = new File([blob], fileName, {
                        type: 'image/jpeg',
                        lastModified: Date.now()
                    });
                    callback(file);
                }, 'image/jpeg', quality);
            }
        })
    };

    reader.readAsDataURL(imageFile);
}

function getResizeScale({
    imageHeight,
    imageWidth,
    minSide,
    maxSide
}) {
    const smallestImageSide = Math.min(imageHeight, imageWidth)
    const largestImageSide = Math.max(imageHeight, imageWidth)

    // Too small images do not contain enough detail 
    if (smallestImageSide < minSide) {
        throw new Error("Image is too small");
    }

    var scale;
    if (largestImageSide > maxSide) {
        // Scale such that the largest image will be 2,000 pixels
        scale = maxSide / largestImageSide;

        // Prevent the image to become smaller than 320 pixels after downscaling for
        // images with extreme aspect ratios. In that case, allow the largest image side
        // to be larger than 2,000 pixels
        if (Math.round(smallestImageSide * scale) < minSide) {
            scale = minSide / smallestImageSide;
        }
    } else {
        // Do not scale image which are already smaller than 2,000 pixels
        scale = 1;
    }

    return scale;
}

function resizeImage({
    imageDataURL,
    minSide = 320,
    maxSide = 2000,
    callback
}) {
    const image = new Image();

    image.onload = () => {
        const imageHeight = image.height;
        const imageWidth = image.width;

        const resizeScale = getResizeScale({
            imageHeight: imageHeight,
            imageWidth: imageWidth,
            minSide: minSide,
            maxSide: maxSide
        })

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        canvas.width = Math.round(imageWidth * resizeScale);
        canvas.height = Math.round(imageHeight * resizeScale);

        // Note that the interpolation method cannot be set explicitly.
        // Bilinear interpolation is used in most browsers.
        // Some browsers do allow `imageSmoothingQuality` to be set.
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        callback(canvas);
    };

    image.src = imageDataURL;
}

function getRandomExampleImage() {
    // Generate example image/canvas with random pixels
    const canvas = document.createElement("canvas");
    canvas.height = 4000;
    canvas.width = 3000;
    const ctx = canvas.getContext("2d");
    var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    function randomInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    for (var i = 0; i < imageData.data.length; i += 4) {
        imageData.data[i] = randomInt(0, 255);
        imageData.data[i + 1] = randomInt(0, 255);
        imageData.data[i + 2] = randomInt(0, 255);
        imageData.data[i + 3] = 255;
    }

    ctx.putImageData(imageData, 0, 0);
    document.body.appendChild(canvas);

    return canvas
}


const canvas = getRandomExampleImage();

resizeImage({
    imageDataURL: canvas.toDataURL(),
    callback: resizedImage => {
        resizedImage.toBlob(blob => {
            console.log(blob.size);
            compressImage({
                imageFile: blob,
                callback: compressedImage => console.log(compressedImage.size)
            });
        });
    }
});
package com.example.myapplication;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;

import android.graphics.Bitmap;
import android.graphics.Color;

import java.io.ByteArrayOutputStream;
import java.util.Random;

class Example {

    public static void main(String[] args) {
        Bitmap image = getRandomExampleImage();
        try {
            Bitmap resizedImage = resizeImage(image, 320, 2000);
            ByteArrayOutputStream compressedImage = compressImage(resizedImage, 90);
            System.out.println(compressedImage.size());
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }

    protected Bitmap getRandomExampleImage() {
        // Generate example image with random pixels
        int height = 4000;
        int width = 3000;
        int[] pixels = new int[height * width];
        Random randomGenerator = new Random();

        int i = 0;
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int a = randomGenerator.nextInt(256);
                int r = randomGenerator.nextInt(256);
                int g = randomGenerator.nextInt(256);
                int b = randomGenerator.nextInt(256);

                int pixel = Color.argb(a, r, g, b);
                pixels[i] = pixel;

                i++;
            }
        }

        return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
    }

    protected Bitmap resizeImage(Bitmap image, int minSide, int maxSide) throws Exception {
        int imageHeight = image.getHeight();
        int imageWidth = image.getWidth();

        double resizeScale = getResizeScale(
                imageHeight, imageWidth, minSide, maxSide
        );

        if (resizeScale == 1) {
            return image;
        }

        int scaledHeight = (int) round(imageHeight * resizeScale);
        int scaledWidth = (int) round(imageWidth * resizeScale);

        // Note that when `filter` is set to true, bilinear interpolation is used.
        // Besides nearest neighbor interpolation, no other interpolation methods are available.
        return Bitmap.createScaledBitmap(image, scaledWidth, scaledHeight, true);
    }

    protected double getResizeScale(int imageHeight, int imageWidth, int minSide, int maxSide) throws Exception {
        int smallestImageSide = min(imageHeight, imageWidth);
        int largestImageSide = max(imageHeight, imageWidth);

        // Too small images do not contain enough detail
        if (smallestImageSide < minSide) {
            throw new Exception("Image is too small");
        }

        double scale;
        if (largestImageSide > maxSide) {
            // Scale such that the largest image will be 2,000 pixels
            scale = ((double) maxSide) / largestImageSide;

            // Prevent the image to become smaller than 320 pixels after downscaling for
            // images with extreme aspect ratios. In that case, allow the largest image side
            // to be larger than 2,000 pixels
            if (round(smallestImageSide * scale) < minSide) {
                scale = ((double) minSide) / smallestImageSide;
            }
        } else {
            // Do not scale image which are already smaller than 2,000 pixels
            scale = 1.0;
        }

        return scale;
    }

    protected ByteArrayOutputStream compressImage(Bitmap image, int quality) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        // Note that different compress formats interpret the quality differently. For PNG, the
        // quality is ignored. For WEBP_LOSSLESS a high quality results in a small file size (with
        // high compression)
        image.compress(Bitmap.CompressFormat.JPEG, quality, stream);

        return stream;
    }

}
import UIKit

enum ResizeError: Error {
    case tooSmallImageError
}

func compressImage(
    image: UIImage,
    quality: CGFloat
) -> Data? {
    let data = image.jpegData(compressionQuality: quality)
    
    return data
}

func getResizeScale(
    imageHeight: Int32,
    imageWidth:Int32,
    minSide: Int32,
    maxSide: Int32
) throws -> Float32  {
    let smallestImageSide = min(imageHeight, imageWidth)
    let largestImageSide = max(imageHeight, imageWidth)
    
    // Too small images do not contain enough detail
    if smallestImageSide < minSide {
        throw ResizeError.tooSmallImageError
    }
    
    var  scale: Float32
    if largestImageSide > maxSide {
        // Scale such that the largest image will be 2,000 pixels
        scale = Float32(maxSide) / Float32(largestImageSide)
        
        // Prevent the image to become smaller than 320 pixels after downscaling for
        // images with extreme aspect ratios. In that case, allow the largest image side
        // to be larger than 2,000 pixels
        if Int32(round(Float32(smallestImageSide) * scale)) < minSide {
            scale = Float32(minSide) / Float32(smallestImageSide)
        }
    } else {
        // Do not scale image which are already smaller than 2,000 pixels
        scale = 1.0
    }
    
    return scale
}

func resizeImage(
    image: UIImage,
    minSide: Int32,
    maxSide: Int32
) throws -> UIImage? {
    let imageHeight = Int32(image.size.height)
    let imageWidth = Int32(image.size.width)
    
    let resizeScale: Float32!
    do {
        resizeScale = try getResizeScale(
            imageHeight: imageHeight,
            imageWidth: imageWidth,
            minSide: minSide,
            maxSide: maxSide
        )
    } catch is ResizeError {
        throw ResizeError.tooSmallImageError
    }
    
    if resizeScale == 1 {
        return image
    }
    
    let outputHeight = Int(round(Float32(imageHeight) * resizeScale))
    let outputWidth = Int(round(Float32(imageWidth) * resizeScale))
    
    let newSize = CGSize(width: outputWidth, height: outputHeight)
    let rect = CGRect(origin: .zero, size: newSize)
    
    // Note that the interpolation method cannot be explicitly set, but that
    // `interpolationQuality` can be used to control interpolation quality
    UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
    let context = UIGraphicsGetCurrentContext()!
    context.interpolationQuality = .default
    image.draw(in: rect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return newImage
}

public struct PixelData {
    var a: UInt8
    var r: UInt8
    var g: UInt8
    var b: UInt8
}

func getRandomExampleImage(
    height: Int,
    width: Int
) -> UIImage? {
    // Generate example image with random pixels
    var pixels = [PixelData]()
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
    let bitsPerComponent = 8
    let bitsPerPixel = 32
    
    var i = 0
    for _ in 0..<width {
        for _ in 0..<height {
            i = i + 1
            // Note that this can be quite slow for larger images
            pixels.append(
                PixelData(
                    a: .random(in: 0...255),
                    r: .random(in: 0...255),
                    g: .random(in: 0...255),
                    b: .random(in: 0...255)
                )
            )
        }
    }
    
    guard let providerRef = CGDataProvider(
        data: NSData(
            bytes: &pixels,
            length: pixels.count * MemoryLayout<PixelData>.size
        )
        )
        else { return nil }
    
    guard let cgim = CGImage(
        width: width,
        height: height,
        bitsPerComponent: bitsPerComponent,
        bitsPerPixel: bitsPerPixel,
        bytesPerRow: width * MemoryLayout<PixelData>.size,
        space: rgbColorSpace,
        bitmapInfo: bitmapInfo,
        provider: providerRef,
        decode: nil,
        shouldInterpolate: true,
        intent: .defaultIntent
        )
        else { return nil }
    
    return UIImage(cgImage: cgim)
}

guard let image = getRandomExampleImage(height:4000, width:3000)
    else {
        print("Could not initialize image") ;
        exit(-1)
}
guard let resizedImage = try? resizeImage(image: image, minSide: 320, maxSide: 2000)
    else {
        print("Could not resize image")
        exit(-1)
}
guard let compressedImage = compressImage(image: resizedImage, quality: 0.9)
    else {
        print("Could not compress image")
        exit(-1)
}
print(compressedImage.count)