Created
October 28, 2021 07:11
-
-
Save Skyyo/111e9fc0cb3f297e0ebff21f347b4131 to your computer and use it in GitHub Desktop.
#compression #image_compression #optimization
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object ImageCompressor { | |
/** | |
* @param context the application environment | |
* @param imageUri the input image uri. usually "content://..." | |
* @param imageFile file where the image was saved. For "photo from camera" scenarios. If it's | |
* null - we're creating the File inside the createFileAndCompress() | |
* @param compressFormat the output image file format | |
* @param maxWidth the output image max width | |
* @param maxHeight the output image max height | |
* @param useMaxScale determine whether to use the bigger dimension | |
* between [maxWidth] or [maxHeight] | |
* @param quality the output image compress quality | |
* @param minWidth the output image min width | |
* @param minHeight the output image min height | |
* | |
* @return output image [android.net.Uri] | |
*/ | |
fun compressUri( | |
context: Context, | |
imageUri: Uri, | |
imageFile: File?, | |
compressFormat: Bitmap.CompressFormat, | |
maxWidth: Float, | |
maxHeight: Float, | |
useMaxScale: Boolean, | |
quality: Int, | |
minWidth: Int, | |
minHeight: Int | |
): Uri? { | |
/** | |
* Decode uri bitmap from activity result using content provider | |
*/ | |
val bmOptions: BitmapFactory.Options = getBitmapOptions(context, imageUri) | |
/** | |
* Calculate scale factor of the bitmap relative to [maxWidth] and [maxHeight] | |
*/ | |
val scaleDownFactor: Float = calculateScaleDownFactor( | |
bmOptions, useMaxScale, maxWidth, maxHeight | |
) | |
/** | |
* Since [BitmapFactory.Options.inSampleSize] only accept value with power of 2, | |
* we calculate the nearest power of 2 to the previously calculated scaleDownFactor | |
* check doc [BitmapFactory.Options.inSampleSize] | |
*/ | |
setNearestInSampleSize(bmOptions, scaleDownFactor) | |
/** | |
* 2 things we do here with image matrix: | |
* - Adjust image rotation | |
* - Scale image matrix based on remaining [scaleDownFactor / bmOption.inSampleSize] | |
*/ | |
val matrix: Matrix = calculateImageMatrix( | |
context, imageUri, scaleDownFactor, bmOptions | |
) ?: return null | |
/** | |
* Create new bitmap based on defined bmOptions and calculated matrix | |
*/ | |
val newBitmap: Bitmap = generateNewBitmap( | |
context, imageUri, bmOptions, matrix | |
) ?: return null | |
val newBitmapWidth = newBitmap.width | |
val newBitmapHeight = newBitmap.height | |
/** | |
* Determine whether to scale up the image or not if the | |
* image width and height is below minimum dimension | |
*/ | |
val shouldScaleUp: Boolean = shouldScaleUp( | |
newBitmapWidth, newBitmapHeight, minWidth, minHeight | |
) | |
/** | |
* Calculate the final scaleUpFactor if the image need to be scaled up. | |
*/ | |
val scaleUpFactor: Float = calculateScaleUpFactor( | |
newBitmapWidth.toFloat(), newBitmapHeight.toFloat(), maxWidth, maxHeight, | |
minWidth, minHeight, shouldScaleUp | |
) | |
/** | |
* calculate the final width and height based on final scaleUpFactor | |
*/ | |
val finalWidth: Int = finalWidth(newBitmapWidth.toFloat(), scaleUpFactor) | |
val finalHeight: Int = finalHeight(newBitmapHeight.toFloat(), scaleUpFactor) | |
/** | |
* Generate the final bitmap, by scaling up if needed | |
*/ | |
val finalBitmap: Bitmap = scaleUpBitmapIfNeeded( | |
newBitmap, finalWidth, finalHeight, scaleUpFactor, shouldScaleUp | |
) | |
/** | |
* create file if we're given only URI & compress image. Use provided file if it's not null | |
*/ | |
val file = imageFile ?: createFile(context) | |
val compressedFilePath: String? = compressImage(finalBitmap, compressFormat, file, quality) | |
return compressedFilePath?.let { Uri.fromFile(File(compressedFilePath)) } | |
} | |
private fun getBitmapOptions( | |
context: Context, | |
imageUri: Uri | |
): BitmapFactory.Options { | |
val bmOptions = BitmapFactory.Options().apply { | |
inJustDecodeBounds = true | |
} | |
val input: InputStream? = context.contentResolver.openInputStream(imageUri) | |
BitmapFactory.decodeStream(input, null, bmOptions) | |
input?.close() | |
return bmOptions | |
} | |
private fun calculateScaleDownFactor( | |
bmOptions: BitmapFactory.Options, | |
useMaxScale: Boolean, | |
maxWidth: Float, | |
maxHeight: Float | |
): Float { | |
val photoW = bmOptions.outWidth.toFloat() | |
val photoH = bmOptions.outHeight.toFloat() | |
val widthRatio = photoW / maxWidth | |
val heightRatio = photoH / maxHeight | |
var scaleFactor = if (useMaxScale) { | |
max(widthRatio, heightRatio) | |
} else { | |
min(widthRatio, heightRatio) | |
} | |
if (scaleFactor < 1) { | |
scaleFactor = 1f | |
} | |
return scaleFactor | |
} | |
private fun setNearestInSampleSize( | |
bmOptions: BitmapFactory.Options, | |
scaleFactor: Float | |
) { | |
bmOptions.inJustDecodeBounds = false | |
bmOptions.inSampleSize = scaleFactor.toInt() | |
if (bmOptions.inSampleSize % 2 != 0) { // check if sample size is divisible by 2 | |
var sample = 1 | |
while (sample * 2 < bmOptions.inSampleSize) { | |
sample *= 2 | |
} | |
bmOptions.inSampleSize = sample | |
} | |
} | |
private fun calculateImageMatrix( | |
context: Context, | |
imageUri: Uri, | |
scaleFactor: Float, | |
bmOptions: BitmapFactory.Options | |
): Matrix? { | |
val input: InputStream = context.contentResolver.openInputStream(imageUri) ?: return null | |
val exif = ExifInterface(input) | |
val matrix = Matrix() | |
val orientation: Int = exif.getAttributeInt( | |
ExifInterface.TAG_ORIENTATION, | |
ExifInterface.ORIENTATION_NORMAL | |
) | |
when (orientation) { | |
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate( | |
90f | |
) | |
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate( | |
180f | |
) | |
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate( | |
270f | |
) | |
} | |
val remainingScaleFactor = scaleFactor / bmOptions.inSampleSize.toFloat() | |
if (remainingScaleFactor > 1) { | |
matrix.postScale(1.0f / remainingScaleFactor, 1.0f / remainingScaleFactor) | |
} | |
input.close() | |
return matrix | |
} | |
private fun generateNewBitmap( | |
context: Context, | |
imageUri: Uri, | |
bmOptions: BitmapFactory.Options, | |
matrix: Matrix | |
): Bitmap? { | |
var bitmap: Bitmap? = null | |
val inputStream: InputStream? = context.contentResolver.openInputStream(imageUri) | |
try { | |
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions) | |
if (bitmap != null) { | |
val matrixScaledBitmap: Bitmap = Bitmap.createBitmap( | |
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true | |
) | |
if (matrixScaledBitmap != bitmap) { | |
bitmap.recycle() | |
bitmap = matrixScaledBitmap | |
} | |
} | |
inputStream?.close() | |
} catch (e: Throwable) { | |
e.printStackTrace() | |
} | |
return bitmap | |
} | |
private fun shouldScaleUp( | |
photoW: Int, | |
photoH: Int, | |
minWidth: Int, | |
minHeight: Int | |
): Boolean { | |
return minWidth != 0 && minHeight != 0 && (photoW < minWidth || photoH < minHeight) | |
} | |
private fun calculateScaleUpFactor( | |
photoW: Float, | |
photoH: Float, | |
maxWidth: Float, | |
maxHeight: Float, | |
minWidth: Int, | |
minHeight: Int, | |
shouldScaleUp: Boolean | |
): Float { | |
var scaleUpFactor: Float = max(photoW / maxWidth, photoH / maxHeight) | |
if (shouldScaleUp) { | |
scaleUpFactor = if (photoW < minWidth && photoH > minHeight) { | |
photoW / minWidth | |
} else if (photoW > minWidth && photoH < minHeight) { | |
photoH / minHeight | |
} else { | |
max(photoW / minWidth, photoH / minHeight) | |
} | |
} | |
return scaleUpFactor | |
} | |
private fun finalWidth(photoW: Float, scaleUpFactor: Float): Int { | |
return (photoW / scaleUpFactor).toInt() | |
} | |
private fun finalHeight(photoH: Float, scaleUpFactor: Float): Int { | |
return (photoH / scaleUpFactor).toInt() | |
} | |
private fun scaleUpBitmapIfNeeded( | |
bitmap: Bitmap, | |
finalWidth: Int, | |
finalHeight: Int, | |
scaleUpFactor: Float, | |
shouldScaleUp: Boolean | |
): Bitmap { | |
val scaledBitmap: Bitmap = if (scaleUpFactor > 1 || shouldScaleUp) { | |
Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true) | |
} else { | |
bitmap | |
} | |
if (scaledBitmap != bitmap) { | |
bitmap.recycle() | |
} | |
return scaledBitmap | |
} | |
private fun compressImage( | |
bitmap: Bitmap, | |
compressFormat: Bitmap.CompressFormat?, | |
imageFile: File, | |
quality: Int, | |
): String? { | |
val stream = FileOutputStream(imageFile) | |
bitmap.compress(compressFormat, quality, stream) | |
stream.close() | |
bitmap.recycle() | |
return imageFile.absolutePath | |
} | |
private fun createFile(context: Context): File { | |
val fileName = context.applicationInfo.loadLabel(context.packageManager).toString() | |
return File.createTempFile( | |
fileName, | |
".jpg", | |
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) | |
// File("/storage/emulated/0/Download/") // can be used for testing the output | |
) | |
} | |
} | |
private const val MAX_PHOTO_SIZE = 1280f | |
private const val MIN_PHOTO_SIZE = 101 | |
private const val PHOTO_QUALITY = 80 | |
// use case 1 | |
// viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { | |
// val compressedUri = ImageCompressor.compressUri( | |
// context = requireContext(), | |
// imageUri = imageFileUri, | |
// imageFile = imageFile, | |
// compressFormat = Bitmap.CompressFormat.JPEG, | |
// maxWidth = MAX_PHOTO_SIZE, | |
// maxHeight = MAX_PHOTO_SIZE, | |
// useMaxScale = true, | |
// quality = PHOTO_QUALITY, | |
// minWidth = MIN_PHOTO_SIZE, | |
// minHeight = MIN_PHOTO_SIZE | |
// ) | |
// //TODO do smth with uri | |
// } | |
// use case 2 | |
// val chooseFromGalleryRequester = askForChooseFromGallery { uri -> | |
// viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { | |
// val compressedUri = ImageCompressor.compressUri( | |
// context = requireContext(), | |
// imageUri = uri, | |
// imageFile = null, | |
// compressFormat = Bitmap.CompressFormat.JPEG, | |
// maxWidth = MAX_PHOTO_SIZE, | |
// maxHeight = MAX_PHOTO_SIZE, | |
// useMaxScale = true, | |
// quality = PHOTO_QUALITY, | |
// minWidth = MIN_PHOTO_SIZE, | |
// minHeight = MIN_PHOTO_SIZE | |
// ) | |
// // TODO do smth with uri | |
// } | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment