Compress bitmap to a specific byte size in Android
Asked Answered
S

4

4

Is there a way to compress Bitmap to a specific byte size? For example, 1.5MB. The matter is all the examples I have seen so far were resizing width and height, but my requirement is to resize the bytes. Is that possible? Also, what is the most straightforward and right way to compress the Bitmap? I am quite novice to this topic and would like to go right direction from the beginning.

Severance answered 19/8, 2018 at 16:57 Comment(0)
P
2

You can calculate the size of a bitmap quite easily by width * height * bytes per pixel = size

Where bytes per pixel is defined by your color model say RGBA_F16 is 8 bytes while ARGB_8888 is 4 bytes and so on. With this you should be able to figure out what width and height and color encoding you want for your image.

See https://developer.android.com/reference/android/graphics/Bitmap.Config for the bit values.

Also see https://developer.android.com/topic/performance/graphics/manage-memory for more about bitmap memory management.

Polyhedron answered 19/8, 2018 at 17:11 Comment(0)
P
2

Here's a helper class I created. This compresses the bitmap both by width/height then by max file size. It's not an exact science to shrink an image to 1.5mb, but what it does is if the image is larger than required, it compresses the bitmap using jpeg and reduces the quality by 80%. Once the file size is less than the required size, it returns the bitmap in a byte array.

public static byte[] getCompressedBitmapData(Bitmap bitmap, int maxFileSize, int maxDimensions) {
    Bitmap resizedBitmap;
    if (bitmap.getWidth() > maxDimensions || bitmap.getHeight() > maxDimensions) {
        resizedBitmap = getResizedBitmap(bitmap,
                                         maxDimensions);
    } else {
        resizedBitmap = bitmap;
    }

    byte[] bitmapData = getByteArray(resizedBitmap);

    while (bitmapData.length > maxFileSize) {
        bitmapData = getByteArray(resizedBitmap);
    }
    return bitmapData;
}

public static Bitmap getResizedBitmap(Bitmap image, int maxSize) {
    int width = image.getWidth();
    int height = image.getHeight();

    float bitmapRatio = (float) width / (float) height;
    if (bitmapRatio > 1) {
        width = maxSize;
        height = (int) (width / bitmapRatio);
    } else {
        height = maxSize;
        width = (int) (height * bitmapRatio);
    }
    return Bitmap.createScaledBitmap(image,
                                     width,
                                     height,
                                     true);
}

private static byte[] getByteArray(Bitmap bitmap) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();

    bitmap.compress(Bitmap.CompressFormat.JPEG,
                    80,
                    bos);

    return bos.toByteArray();
}
Poverty answered 19/8, 2018 at 17:17 Comment(3)
Does reducing the quality with through encoding to JPEG and subsequent decoding actually reduce the memory footprint of the bitmap? I dont understand how that would work, because it is the same number of pixels and bytes per color before and after, isn't it?Polyhedron
So no? I'm still confused what encoding and decoding achieves.Polyhedron
@Poverty thank you so much for your effort, my deadline was really tight and I implemented smth very simple. But I will take a look at your code later when I can and will come back to this topic, since it's very very important topicSeverance
I
1

This works for me. Scale area of original bitmap to 50% and compress bitmap until it's size < 200k

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.os.Environment
import android.support.media.ExifInterface //28.0.0

companion object {
        const val TAG = "MainActivity"
        internal val ROOT_FOLDER_CACHE_IMAGE =
            Environment.getExternalStorageDirectory().toString() + "/com.test/cache"
        const val _200KB = 200 * 1024
    }

private fun displayBitmapAfterCompressing() {
        val filePath = Environment.getExternalStorageDirectory().toString() + "/DCIM/Camera/test.jpg"
        //ImageView shows bitmap before compressing
        var inputStream = FileInputStream(filePath)
        original_image_view.setImageBitmap(BitmapFactory.decodeStream(inputStream))
        inputStream.close()

        //ImageView shows bitmap after compressing
        val newFilePath = resizeAndCompressFile(filePath)
        if (newFilePath != null) {
            inputStream = FileInputStream(newFilePath)
            compress_image_view.setImageBitmap(BitmapFactory.decodeStream(inputStream))
            inputStream.close()
        }
    }

    private fun resizeAndCompressFile(filePath: String): String? {
        val imageFile = File(filePath)
        if (imageFile.exists()) {
            val fileSize = imageFile.length()
            if (fileSize > 0) {
                return if (fileSize < _200KB) {
                    filePath
                } else {
                    resizeAndCompressBitmapTo200KB(filePath)
                }
            }
        }
        return null
    }

    private fun resizeAndCompressBitmapTo200KB(filePath: String): String? {
        val imageFile = File(filePath)
        val fileSize = imageFile.length()
        Log.d(TAG, "size of original file = $fileSize")

        if (fileSize > _200KB) {
            var qualityCompress = 80
            if (fileSize > 3145728) {// > 3MB
                qualityCompress = 55
            } else if (fileSize > 2097152) {// > 2MB
                qualityCompress = 60
            } else if (fileSize > 1560576) {// > 1.5MB
                qualityCompress = 65
            } else if (fileSize > 1048576) {// > 1MB
                qualityCompress = 70
            }

            var newFilePath: String?
            do {
                newFilePath = compressFileAndReturnNewPathOfNewFile(filePath, qualityCompress)
                qualityCompress -= 5

                //TODO test
                newFilePath?.let {
                    Log.d(
                        TAG,
                        "qualityCompress = " + qualityCompress + "size of new file = " + File(newFilePath).length()
                    )
                }
            } while (newFilePath != null && File(newFilePath).length() > _200KB)
            //copy attributes from old exif to new exif
            if (newFilePath != null) {
                copyExif(filePath, newFilePath)
            }

            return newFilePath
        }
        return filePath
    }

    private fun compressFileAndReturnNewPathOfNewFile(filePath: String, qualityCompress: Int): String? {
        try {
            val inputStream = FileInputStream(filePath)
            var compressBitmap = BitmapFactory.decodeStream(inputStream)
            //original width height
            val widthOriginal = compressBitmap.width
            val heightOriginal = compressBitmap.height

            //resize image 50% (keep original scale)
            val width50Percent: Int = (widthOriginal / 1.41421356237).toInt()
            val height50Percent: Int = (heightOriginal / 1.41421356237).toInt()
            //
            val scaleWidth: Float = width50Percent.toFloat() / widthOriginal
            val scaleHeight: Float = height50Percent.toFloat() / heightOriginal
            //
            //Must Rotate bitmap before upload them
            val matrix = Matrix()
            matrix.setRotate(getOrientation(filePath).toFloat())
            matrix.postScale(scaleWidth, scaleHeight);

            compressBitmap = Bitmap.createBitmap(
                compressBitmap, 0, 0, compressBitmap.width,
                compressBitmap.height, matrix, true
            )
            //make a new file directory inside the "sdcard" folder
            val mediaStorageDir = File(ROOT_FOLDER_CACHE_IMAGE)
            if (!mediaStorageDir.exists()) {
                mediaStorageDir.mkdirs()
            }
            var file = File(mediaStorageDir.absolutePath, "compress_image.jpeg")
            if (file.exists()) {
                file.deleteOnExit()
                file = File(mediaStorageDir.absolutePath, "compress_image.jpeg")
            }

            val fos = FileOutputStream(file)
            compressBitmap.compress(Bitmap.CompressFormat.JPEG, qualityCompress, fos)
            fos.flush()
            fos.close()

            inputStream.close()
            compressBitmap.recycle()
            compressBitmap = null
            // Use this for reading the data.
            /*val inputStream = FileInputStream(file.absolutePath)
            val buffer = ByteArray(file.length().toInt())
            inputStream.read(buffer)
            inputStream.close()
            return buffer*/
            return file.absolutePath

        } catch (e1: FileNotFoundException) {
            Log.e(TAG, "compressFileToByteArray(1)", e1)
        } catch (e2: IOException) {
            Log.e(TAG, "compressFileToByteArray(2)", e2)

        } catch (e3: Exception) {
            Log.e(TAG, "compressFileToByteArray(3)", e3)
        }
        return null
    }

    private fun copyExif(oldPath: String, newPath: String) {
        val attributes = arrayOf(
            ExifInterface.TAG_IMAGE_WIDTH,
            ExifInterface.TAG_IMAGE_LENGTH,
            ExifInterface.TAG_BITS_PER_SAMPLE,
            ExifInterface.TAG_COMPRESSION,
            ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
            ExifInterface.TAG_ORIENTATION,
            ExifInterface.TAG_SAMPLES_PER_PIXEL,
            ExifInterface.TAG_PLANAR_CONFIGURATION,
            ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
            ExifInterface.TAG_Y_CB_CR_POSITIONING,
            ExifInterface.TAG_X_RESOLUTION,
            ExifInterface.TAG_Y_RESOLUTION,
            ExifInterface.TAG_RESOLUTION_UNIT,
            ExifInterface.TAG_STRIP_OFFSETS,
            ExifInterface.TAG_ROWS_PER_STRIP,
            ExifInterface.TAG_STRIP_BYTE_COUNTS,
            ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
            ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
            ExifInterface.TAG_TRANSFER_FUNCTION,
            ExifInterface.TAG_WHITE_POINT,
            ExifInterface.TAG_PRIMARY_CHROMATICITIES,
            ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
            ExifInterface.TAG_REFERENCE_BLACK_WHITE,
            ExifInterface.TAG_DATETIME,
            ExifInterface.TAG_IMAGE_DESCRIPTION,
            ExifInterface.TAG_MAKE,
            ExifInterface.TAG_MODEL,
            ExifInterface.TAG_SOFTWARE,
            ExifInterface.TAG_ARTIST,
            ExifInterface.TAG_COPYRIGHT,
            ExifInterface.TAG_EXIF_VERSION,
            ExifInterface.TAG_FLASHPIX_VERSION,
            ExifInterface.TAG_COLOR_SPACE,
            ExifInterface.TAG_GAMMA,
            ExifInterface.TAG_PIXEL_X_DIMENSION,
            ExifInterface.TAG_PIXEL_Y_DIMENSION,
            ExifInterface.TAG_COMPONENTS_CONFIGURATION,
            ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
            ExifInterface.TAG_MAKER_NOTE,
            ExifInterface.TAG_USER_COMMENT,
            ExifInterface.TAG_RELATED_SOUND_FILE,
            ExifInterface.TAG_DATETIME_ORIGINAL,
            ExifInterface.TAG_DATETIME_DIGITIZED,
            ExifInterface.TAG_SUBSEC_TIME,
            ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
            ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
            ExifInterface.TAG_EXPOSURE_TIME,
            ExifInterface.TAG_F_NUMBER,
            ExifInterface.TAG_EXPOSURE_PROGRAM,
            ExifInterface.TAG_SPECTRAL_SENSITIVITY,
            ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
            ExifInterface.TAG_OECF,
            ExifInterface.TAG_SENSITIVITY_TYPE,
            ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
            ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
            ExifInterface.TAG_ISO_SPEED,
            ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY,
            ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ,
            ExifInterface.TAG_SHUTTER_SPEED_VALUE,
            ExifInterface.TAG_APERTURE_VALUE,
            ExifInterface.TAG_BRIGHTNESS_VALUE,
            ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
            ExifInterface.TAG_MAX_APERTURE_VALUE,
            ExifInterface.TAG_SUBJECT_DISTANCE,
            ExifInterface.TAG_METERING_MODE,
            ExifInterface.TAG_LIGHT_SOURCE,
            ExifInterface.TAG_FLASH,
            ExifInterface.TAG_SUBJECT_AREA,
            ExifInterface.TAG_FOCAL_LENGTH,
            ExifInterface.TAG_FLASH_ENERGY,
            ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
            ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
            ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
            ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
            ExifInterface.TAG_SUBJECT_LOCATION,
            ExifInterface.TAG_EXPOSURE_INDEX,
            ExifInterface.TAG_SENSING_METHOD,
            ExifInterface.TAG_FILE_SOURCE,
            ExifInterface.TAG_SCENE_TYPE,
            ExifInterface.TAG_CFA_PATTERN,
            ExifInterface.TAG_CUSTOM_RENDERED,
            ExifInterface.TAG_EXPOSURE_MODE,
            ExifInterface.TAG_WHITE_BALANCE,
            ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
            ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
            ExifInterface.TAG_SCENE_CAPTURE_TYPE,
            ExifInterface.TAG_GAIN_CONTROL,
            ExifInterface.TAG_CONTRAST,
            ExifInterface.TAG_SATURATION,
            ExifInterface.TAG_SHARPNESS,
            ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
            ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
            ExifInterface.TAG_IMAGE_UNIQUE_ID,
            ExifInterface.TAG_CAMARA_OWNER_NAME,
            ExifInterface.TAG_BODY_SERIAL_NUMBER,
            ExifInterface.TAG_LENS_SPECIFICATION,
            ExifInterface.TAG_LENS_MAKE,
            ExifInterface.TAG_LENS_MODEL,
            ExifInterface.TAG_LENS_SERIAL_NUMBER,
            ExifInterface.TAG_GPS_VERSION_ID,
            ExifInterface.TAG_GPS_LATITUDE_REF,
            ExifInterface.TAG_GPS_LATITUDE,
            ExifInterface.TAG_GPS_LONGITUDE_REF,
            ExifInterface.TAG_GPS_LONGITUDE,
            ExifInterface.TAG_GPS_ALTITUDE_REF,
            ExifInterface.TAG_GPS_ALTITUDE,
            ExifInterface.TAG_GPS_TIMESTAMP,
            ExifInterface.TAG_GPS_SATELLITES,
            ExifInterface.TAG_GPS_STATUS,
            ExifInterface.TAG_GPS_MEASURE_MODE,
            ExifInterface.TAG_GPS_DOP,
            ExifInterface.TAG_GPS_SPEED_REF,
            ExifInterface.TAG_GPS_SPEED,
            ExifInterface.TAG_GPS_TRACK_REF,
            ExifInterface.TAG_GPS_TRACK,
            ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
            ExifInterface.TAG_GPS_IMG_DIRECTION,
            ExifInterface.TAG_GPS_MAP_DATUM,
            ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
            ExifInterface.TAG_GPS_DEST_LATITUDE,
            ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
            ExifInterface.TAG_GPS_DEST_LONGITUDE,
            ExifInterface.TAG_GPS_DEST_BEARING_REF,
            ExifInterface.TAG_GPS_DEST_BEARING,
            ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
            ExifInterface.TAG_GPS_DEST_DISTANCE,
            ExifInterface.TAG_GPS_PROCESSING_METHOD,
            ExifInterface.TAG_GPS_AREA_INFORMATION,
            ExifInterface.TAG_GPS_DATESTAMP,
            ExifInterface.TAG_GPS_DIFFERENTIAL,
            ExifInterface.TAG_GPS_H_POSITIONING_ERROR,
            ExifInterface.TAG_INTEROPERABILITY_INDEX,
            ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
            ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
            ExifInterface.TAG_DNG_VERSION,
            ExifInterface.TAG_DEFAULT_CROP_SIZE,
            ExifInterface.TAG_ORF_THUMBNAIL_IMAGE,
            ExifInterface.TAG_ORF_PREVIEW_IMAGE_START,
            ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH,
            ExifInterface.TAG_ORF_ASPECT_FRAME,
            ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER,
            ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER,
            ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER,
            ExifInterface.TAG_RW2_SENSOR_TOP_BORDER,
            ExifInterface.TAG_RW2_ISO,
            ExifInterface.TAG_RW2_JPG_FROM_RAW,
            ExifInterface.TAG_NEW_SUBFILE_TYPE,
            ExifInterface.TAG_SUBFILE_TYPE
            /*
            There are private attributes
            ExifInterface.TAG_EXIF_IFD_POINTER,
            ExifInterface.TAG_GPS_INFO_IFD_POINTER,
            ExifInterface.TAG_INTEROPERABILITY_IFD_POINTER,
            ExifInterface.TAG_SUB_IFD_POINTER,
            ExifInterface.TAG_ORF_CAMERA_SETTINGS_IFD_POINTER,
            ExifInterface.TAG_ORF_IMAGE_PROCESSING_IFD_POINTER,
            ExifInterface.TAG_HAS_THUMBNAIL,
            ExifInterface.TAG_THUMBNAIL_LENGTH,
            ExifInterface.TAG_THUMBNAIL_DATA*/
        )
        val oldExif = ExifInterface(oldPath)
        val newExif = ExifInterface(newPath)
        attributes.forEach { attribute ->
            oldExif.getAttribute(attribute)?.let { value ->
                newExif.setAttribute(attribute, value)
            }
        }
        newExif.saveAttributes()
    }

    /**
     * Get orientation of bitmap.
     *
     * @param filePath : link of bitmap in sdcard
     * @return Orientation of bitmap
     */
    private fun getOrientation(filePath: String?): Int {
        var ori = 0
        if (filePath != null) {
            val exif: ExifInterface
            try {
                if (filePath.contains("file://")) {
                    exif = ExifInterface(filePath.substring(7))
                } else {
                    exif = ExifInterface(filePath)
                }
                val exifOrientation = exif.getAttributeInt(
                    ExifInterface.TAG_ORIENTATION,
                    ExifInterface.ORIENTATION_NORMAL
                )
                when (exifOrientation) {
                    ExifInterface.ORIENTATION_UNDEFINED -> {
                    }
                    ExifInterface.ORIENTATION_NORMAL -> {
                    }
                    ExifInterface.ORIENTATION_ROTATE_180 -> ori = 180
                    ExifInterface.ORIENTATION_ROTATE_90 -> ori = 90
                    ExifInterface.ORIENTATION_ROTATE_270 -> ori = 270
                    else -> {
                    }
                }
            } catch (e: IOException) {
                Log.e(TAG, "getOrientation(String filePath) method: ", e)
            }
        }
        return ori
    }
Intuitive answered 10/4, 2019 at 10:1 Comment(0)
C
1

See my answer at (Which doesn't use a while loop): How to reduce image size into 1MB

This method works if your current passed Bitmap is in the ARGB_8888 configuration (So 4 bytes per pixel. When it isn't ARGB_8888 you can convert it to that bitmap by using:

/**
 * Convert a Bitmap to a Bitmap that has 4 bytes per pixel
 * @param input The bitmap to convert to a 4 bytes per pixel Bitmap
 * 
 * @return The converted Bitmap. Note: The caller of this method is 
 * responsible for reycling the input
 */
public static Bitmap to4BytesPerPixelBitmap(@NonNull final Bitmap input){
    final Bitmap bitmap = Bitmap.createBitmap(input.width, input.height, Bitmap.Config.ARGB_8888);
    // Instantiate the canvas to draw on:
    final Canvas canvas = new Canvas(bitmap);
    canvas.drawBitmap(input, 0, 0, null);
    // Return the new bitmap:
    return bitmap;  
}   
Cohla answered 1/8, 2019 at 10:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.