Skip to content

Commit 758d538

Browse files
authored
Merge pull request #589 from Naveen3Singh/improve_compressor
Improve image compression
2 parents 2992d77 + a27790e commit 758d538

File tree

7 files changed

+104
-94
lines changed

7 files changed

+104
-94
lines changed

app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import android.view.inputmethod.EditorInfo
3333
import android.widget.LinearLayout
3434
import android.widget.LinearLayout.LayoutParams
3535
import android.widget.RelativeLayout
36+
import android.widget.Toast
3637
import androidx.annotation.StringRes
3738
import androidx.constraintlayout.widget.ConstraintLayout
3839
import androidx.core.content.res.ResourcesCompat
@@ -1100,6 +1101,23 @@ class ThreadActivity : SimpleActivity() {
11001101
return
11011102
}
11021103

1104+
val mimeType = contentResolver.getType(uri)
1105+
if (mimeType == null) {
1106+
toast(R.string.unknown_error_occurred)
1107+
return
1108+
}
1109+
val isImage = mimeType.isImageMimeType()
1110+
val isGif = mimeType.isGifMimeType()
1111+
if (isGif || !isImage) {
1112+
// is it assumed that images will always be compressed below the max MMS size limit
1113+
val fileSize = getFileSizeFromUri(uri)
1114+
val mmsFileSizeLimit = config.mmsFileSizeLimit
1115+
if (mmsFileSizeLimit != FILE_SIZE_NONE && fileSize > mmsFileSizeLimit) {
1116+
toast(R.string.attachment_sized_exceeds_max_limit, length = Toast.LENGTH_LONG)
1117+
return
1118+
}
1119+
}
1120+
11031121
var adapter = getAttachmentsAdapter()
11041122
if (adapter == null) {
11051123
adapter = AttachmentsAdapter(
@@ -1115,17 +1133,12 @@ class ThreadActivity : SimpleActivity() {
11151133
}
11161134

11171135
thread_attachments_recyclerview.beVisible()
1118-
val mimeType = contentResolver.getType(uri)
1119-
if (mimeType == null) {
1120-
toast(R.string.unknown_error_occurred)
1121-
return
1122-
}
11231136
val attachment = AttachmentSelection(
11241137
id = id,
11251138
uri = uri,
11261139
mimetype = mimeType,
11271140
filename = getFilenameFromUri(uri),
1128-
isPending = mimeType.isImageMimeType() && !mimeType.isGifMimeType()
1141+
isPending = isImage && !isGif
11291142
)
11301143
adapter.addAttachment(attachment)
11311144
checkSendMessageAvailability()
@@ -1228,8 +1241,9 @@ class ThreadActivity : SimpleActivity() {
12281241
sendMessageCompat(text, addresses, subscriptionId, attachments)
12291242
ensureBackgroundThread {
12301243
val messageIds = messages.map { it.id }
1231-
val message = getMessages(threadId, getImageResolutions = true, limit = 1).firstOrNull { it.id !in messageIds }
1232-
if (message != null) {
1244+
val messages = getMessages(threadId, getImageResolutions = true, limit = maxOf(1, attachments.size))
1245+
.filter { it.id !in messageIds }
1246+
for (message in messages) {
12331247
insertOrUpdateMessage(message)
12341248
}
12351249
}

app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/ThreadAdapter.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,9 @@ class ThreadAdapter(
260260
holder.viewClicked(message)
261261
}
262262

263-
thread_mesage_attachments_holder.removeAllViews()
264263
if (message.attachment?.attachments?.isNotEmpty() == true) {
264+
thread_mesage_attachments_holder.beVisible()
265+
thread_mesage_attachments_holder.removeAllViews()
265266
for (attachment in message.attachment.attachments) {
266267
val mimetype = attachment.mimetype
267268
when {
@@ -272,6 +273,8 @@ class ThreadAdapter(
272273

273274
thread_message_play_outline.beVisibleIf(mimetype.startsWith("video/"))
274275
}
276+
} else {
277+
thread_mesage_attachments_holder.beGone()
275278
}
276279
}
277280
}

app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ImageCompressor.kt

Lines changed: 64 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.simplemobiletools.smsmessenger.extensions.getFileSizeFromUri
1515
import com.simplemobiletools.smsmessenger.extensions.isImageMimeType
1616
import java.io.File
1717
import 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
}

app/src/main/kotlin/com/simplemobiletools/smsmessenger/messaging/Messaging.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ fun Context.sendMessageCompat(text: String, addresses: List<String>, subId: Int?
4040

4141
val isMms = attachments.isNotEmpty() || isLongMmsMessage(text, settings) || addresses.size > 1 && settings.group
4242
if (isMms) {
43-
messagingUtils.sendMmsMessage(text, addresses, attachments, settings)
43+
// we send all MMS attachments separately to reduces the chances of hitting provider MMS limit.
44+
if (attachments.size > 1) {
45+
for (i in 0 until attachments.lastIndex) {
46+
val attachment = attachments[i]
47+
messagingUtils.sendMmsMessage("", addresses, listOf(attachment), settings)
48+
}
49+
}
50+
51+
val lastAttachment = attachments[attachments.lastIndex]
52+
messagingUtils.sendMmsMessage(text, addresses, listOf(lastAttachment), settings)
4453
} else {
4554
try {
4655
messagingUtils.sendSmsMessage(text, addresses.toSet(), settings.subscriptionId, settings.deliveryReports)

app/src/main/kotlin/com/simplemobiletools/smsmessenger/receivers/MmsSentReceiver.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ class MmsSentReceiver : SendStatusReceiver() {
4242
}
4343

4444
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
45-
if (resultCode == Activity.RESULT_OK) {
46-
refreshMessages()
47-
}
45+
refreshMessages()
4846
}
4947

5048
companion object {

app/src/main/res/layout/item_attachment_image.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,4 @@
55
android:layout_width="wrap_content"
66
android:layout_height="wrap_content"
77
android:adjustViewBounds="true"
8-
android:paddingBottom="@dimen/medium_margin"
98
app:shapeAppearanceOverlay="@style/roundedImageView" />

app/src/main/res/layout/item_sent_message.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
android:id="@+id/thread_message_holder"
66
android:layout_width="match_parent"
77
android:layout_height="wrap_content"
8-
android:layout_marginTop="@dimen/medium_margin"
8+
android:layout_marginTop="@dimen/small_margin"
99
android:foreground="@drawable/selector"
1010
android:paddingStart="@dimen/activity_margin"
1111
android:paddingEnd="@dimen/activity_margin">
@@ -23,6 +23,7 @@
2323
android:id="@+id/thread_mesage_attachments_holder"
2424
android:layout_width="match_parent"
2525
android:layout_height="wrap_content"
26+
android:layout_marginVertical="@dimen/tiny_margin"
2627
android:divider="@drawable/linear_layout_vertical_divider"
2728
android:orientation="vertical"
2829
android:showDividers="middle" />
@@ -44,6 +45,7 @@
4445
android:layout_height="wrap_content"
4546
android:layout_below="@+id/thread_mesage_attachments_holder"
4647
android:layout_alignParentEnd="true"
48+
android:layout_marginVertical="@dimen/tiny_margin"
4749
android:autoLink="email|web"
4850
android:background="@drawable/item_sent_background"
4951
android:padding="@dimen/normal_margin"

0 commit comments

Comments
 (0)