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.
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.
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();
}
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
}
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;
}
© 2022 - 2024 — McMap. All rights reserved.