@@ -15,6 +15,7 @@ import com.simplemobiletools.smsmessenger.extensions.getFileSizeFromUri
1515import com.simplemobiletools.smsmessenger.extensions.isImageMimeType
1616import java.io.File
1717import java.io.FileOutputStream
18+ import kotlin.math.roundToInt
1819
1920/* *
2021 * Compress image to a given size based on
@@ -28,36 +29,61 @@ class ImageCompressor(private val context: Context) {
2829 }
2930 }
3031
31- fun compressImage (uri : Uri , compressSize : Long , callback : (compressedFileUri: Uri ? ) -> Unit ) {
32+ private val minQuality = 30
33+ private val minResolution = 56
34+ private val scaleStepFactor = 0.6f // increase for more accurate file size at the cost increased computation
35+
36+ fun compressImage (uri : Uri , compressSize : Long , lossy : Boolean = compressSize < FILE_SIZE_1_MB , callback : (compressedFileUri: Uri ? ) -> Unit ) {
3237 ensureBackgroundThread {
3338 try {
3439 val fileSize = context.getFileSizeFromUri(uri)
3540 if (fileSize > compressSize) {
3641 val mimeType = contentResolver.getType(uri)!!
3742 if (mimeType.isImageMimeType()) {
3843 val byteArray = contentResolver.openInputStream(uri)?.readBytes()!!
39- var destinationFile = File (outputDirectory, System .currentTimeMillis().toString().plus(mimeType.getExtensionFromMimeType()))
40- destinationFile.writeBytes(byteArray)
41- val sizeConstraint = SizeConstraint (compressSize)
42- val bitmap = loadBitmap(destinationFile)
43-
44- // if image weight > * 2 targeted size: cut down resolution by 2
45- if (fileSize > 2 * compressSize) {
46- val resConstraint = ResolutionConstraint (bitmap.width / 2 , bitmap.height / 2 )
47- while (resConstraint.isSatisfied(destinationFile).not ()) {
48- destinationFile = resConstraint.satisfy(destinationFile)
49- }
44+ var imageFile = File (outputDirectory, System .currentTimeMillis().toString().plus(mimeType.getExtensionFromMimeType()))
45+ imageFile.writeBytes(byteArray)
46+ val bitmap = loadBitmap(imageFile)
47+ val format = if (lossy) {
48+ Bitmap .CompressFormat .JPEG
49+ } else {
50+ imageFile.path.getCompressionFormat()
5051 }
51- // do compression
52- while (sizeConstraint.isSatisfied(destinationFile).not ()) {
53- destinationFile = sizeConstraint.satisfy(destinationFile)
52+
53+ // This quality approximation mostly works for smaller images but will fail with larger images.
54+ val compressionRatio = compressSize / fileSize.toDouble()
55+ val quality = maxOf((compressionRatio * 100 ).roundToInt(), minQuality)
56+ imageFile = overWrite(imageFile, bitmap, format = format, quality = quality)
57+
58+ // Even the highest quality images start to look ugly if we use 10 as the minimum quality,
59+ // so we better save some image quality and change resolution instead. This is time consuming
60+ // and mostly needed for very large images. Since there's no reliable way to predict the
61+ // required resolution, we'll just iterate and find the best result.
62+ if (imageFile.length() > compressSize) {
63+ var scaledWidth = bitmap.width
64+ var scaledHeight = bitmap.height
65+
66+ while (imageFile.length() > compressSize) {
67+ scaledWidth = (scaledWidth * scaleStepFactor).roundToInt()
68+ scaledHeight = (scaledHeight * scaleStepFactor).roundToInt()
69+ if (scaledHeight < minResolution && scaledWidth < minResolution) {
70+ break
71+ }
72+
73+ imageFile = decodeSampledBitmapFromFile(imageFile, scaledWidth, scaledHeight).run {
74+ determineImageRotation(imageFile, bitmap = this ).run {
75+ overWrite(imageFile, bitmap = this , format = format, quality = quality)
76+ }
77+ }
78+ }
5479 }
55- callback.invoke(context.getMyFileUri(destinationFile))
80+
81+ callback.invoke(context.getMyFileUri(imageFile))
5682 } else {
5783 callback.invoke(null )
5884 }
5985 } else {
60- // no need to compress since the file is less than the compress size
86+ // no need to compress since the file is less than the compress size
6187 callback.invoke(uri)
6288 }
6389 } catch (e: Exception ) {
@@ -107,77 +133,36 @@ class ImageCompressor(private val context: Context) {
107133 return Bitmap .createBitmap(bitmap, 0 , 0 , bitmap.width, bitmap.height, matrix, true )
108134 }
109135
110- private inner class SizeConstraint (
111- private val maxFileSize : Long ,
112- private val stepSize : Int = 10 ,
113- private val maxIteration : Int = 10 ,
114- private val minQuality : Int = 10
115- ) {
116- private var iteration: Int = 0
117-
118- fun isSatisfied (imageFile : File ): Boolean {
119- // If size requirement is not met and maxIteration is reached
120- if (iteration >= maxIteration && imageFile.length() >= maxFileSize) {
121- throw Exception (" Unable to compress image to targeted size" )
122- }
123- return imageFile.length() <= maxFileSize
124- }
125-
126- fun satisfy (imageFile : File ): File {
127- iteration++
128- val quality = (100 - iteration * stepSize).takeIf { it >= minQuality } ? : minQuality
129- return overWrite(imageFile, loadBitmap(imageFile), quality = quality)
130- }
131- }
132-
133- private inner class ResolutionConstraint (private val width : Int , private val height : Int ) {
136+ private fun decodeSampledBitmapFromFile (imageFile : File , reqWidth : Int , reqHeight : Int ): Bitmap {
137+ return BitmapFactory .Options ().run {
138+ inJustDecodeBounds = true
139+ BitmapFactory .decodeFile(imageFile.absolutePath, this )
134140
135- private fun decodeSampledBitmapFromFile (imageFile : File , reqWidth : Int , reqHeight : Int ): Bitmap {
136- return BitmapFactory .Options ().run {
137- inJustDecodeBounds = true
138- BitmapFactory .decodeFile(imageFile.absolutePath, this )
141+ inSampleSize = calculateInSampleSize(this , reqWidth, reqHeight)
139142
140- inSampleSize = calculateInSampleSize(this , reqWidth, reqHeight)
141-
142- inJustDecodeBounds = false
143- BitmapFactory .decodeFile(imageFile.absolutePath, this )
144- }
143+ inJustDecodeBounds = false
144+ BitmapFactory .decodeFile(imageFile.absolutePath, this )
145145 }
146+ }
146147
147- private fun calculateInSampleSize (options : BitmapFactory .Options , reqWidth : Int , reqHeight : Int ): Int {
148- // Raw height and width of image
149- val (height: Int , width: Int ) = options.run { outHeight to outWidth }
150- var inSampleSize = 1
148+ private fun calculateInSampleSize (options : BitmapFactory .Options , reqWidth : Int , reqHeight : Int ): Int {
149+ // Raw height and width of image
150+ val height = options.outHeight
151+ val width = options.outWidth
152+ var inSampleSize = 1
151153
152- if (height > reqHeight || width > reqWidth) {
154+ if (height > reqHeight || width > reqWidth) {
153155
154- val halfHeight: Int = height / 2
155- val halfWidth: Int = width / 2
156+ val halfHeight: Int = height / 2
157+ val halfWidth: Int = width / 2
156158
157- // Calculate the largest inSampleSize value that is a power of 2 and keeps both
158- // height and width larger than the requested height and width.
159- while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
160- inSampleSize * = 2
161- }
159+ // Calculate the largest inSampleSize value that is a power of 2 and keeps both
160+ // height and width larger than the requested height and width.
161+ while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
162+ inSampleSize * = 2
162163 }
163-
164- return inSampleSize
165164 }
166165
167- fun isSatisfied (imageFile : File ): Boolean {
168- return BitmapFactory .Options ().run {
169- inJustDecodeBounds = true
170- BitmapFactory .decodeFile(imageFile.absolutePath, this )
171- calculateInSampleSize(this , width, height) <= 1
172- }
173- }
174-
175- fun satisfy (imageFile : File ): File {
176- return decodeSampledBitmapFromFile(imageFile, width, height).run {
177- determineImageRotation(imageFile, this ).run {
178- overWrite(imageFile, this )
179- }
180- }
181- }
166+ return inSampleSize
182167 }
183168}
0 commit comments