Skip to content

Instantly share code, notes, and snippets.

@Skyyo
Created October 28, 2021 07:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Skyyo/111e9fc0cb3f297e0ebff21f347b4131 to your computer and use it in GitHub Desktop.
Save Skyyo/111e9fc0cb3f297e0ebff21f347b4131 to your computer and use it in GitHub Desktop.
#compression #image_compression #optimization
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