mirror of
https://github.com/SimpleMobileTools/Simple-SMS-Messenger.git
synced 2025-06-05 21:49:22 +02:00
Merge pull request #589 from Naveen3Singh/improve_compressor
Improve image compression
This commit is contained in:
@@ -33,6 +33,7 @@ import android.view.inputmethod.EditorInfo
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.LinearLayout.LayoutParams
|
import android.widget.LinearLayout.LayoutParams
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
@@ -1100,6 +1101,23 @@ class ThreadActivity : SimpleActivity() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mimeType = contentResolver.getType(uri)
|
||||||
|
if (mimeType == null) {
|
||||||
|
toast(R.string.unknown_error_occurred)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val isImage = mimeType.isImageMimeType()
|
||||||
|
val isGif = mimeType.isGifMimeType()
|
||||||
|
if (isGif || !isImage) {
|
||||||
|
// is it assumed that images will always be compressed below the max MMS size limit
|
||||||
|
val fileSize = getFileSizeFromUri(uri)
|
||||||
|
val mmsFileSizeLimit = config.mmsFileSizeLimit
|
||||||
|
if (mmsFileSizeLimit != FILE_SIZE_NONE && fileSize > mmsFileSizeLimit) {
|
||||||
|
toast(R.string.attachment_sized_exceeds_max_limit, length = Toast.LENGTH_LONG)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var adapter = getAttachmentsAdapter()
|
var adapter = getAttachmentsAdapter()
|
||||||
if (adapter == null) {
|
if (adapter == null) {
|
||||||
adapter = AttachmentsAdapter(
|
adapter = AttachmentsAdapter(
|
||||||
@@ -1115,17 +1133,12 @@ class ThreadActivity : SimpleActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thread_attachments_recyclerview.beVisible()
|
thread_attachments_recyclerview.beVisible()
|
||||||
val mimeType = contentResolver.getType(uri)
|
|
||||||
if (mimeType == null) {
|
|
||||||
toast(R.string.unknown_error_occurred)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val attachment = AttachmentSelection(
|
val attachment = AttachmentSelection(
|
||||||
id = id,
|
id = id,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
mimetype = mimeType,
|
mimetype = mimeType,
|
||||||
filename = getFilenameFromUri(uri),
|
filename = getFilenameFromUri(uri),
|
||||||
isPending = mimeType.isImageMimeType() && !mimeType.isGifMimeType()
|
isPending = isImage && !isGif
|
||||||
)
|
)
|
||||||
adapter.addAttachment(attachment)
|
adapter.addAttachment(attachment)
|
||||||
checkSendMessageAvailability()
|
checkSendMessageAvailability()
|
||||||
@@ -1228,8 +1241,9 @@ class ThreadActivity : SimpleActivity() {
|
|||||||
sendMessageCompat(text, addresses, subscriptionId, attachments)
|
sendMessageCompat(text, addresses, subscriptionId, attachments)
|
||||||
ensureBackgroundThread {
|
ensureBackgroundThread {
|
||||||
val messageIds = messages.map { it.id }
|
val messageIds = messages.map { it.id }
|
||||||
val message = getMessages(threadId, getImageResolutions = true, limit = 1).firstOrNull { it.id !in messageIds }
|
val messages = getMessages(threadId, getImageResolutions = true, limit = maxOf(1, attachments.size))
|
||||||
if (message != null) {
|
.filter { it.id !in messageIds }
|
||||||
|
for (message in messages) {
|
||||||
insertOrUpdateMessage(message)
|
insertOrUpdateMessage(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -260,8 +260,9 @@ class ThreadAdapter(
|
|||||||
holder.viewClicked(message)
|
holder.viewClicked(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_mesage_attachments_holder.removeAllViews()
|
|
||||||
if (message.attachment?.attachments?.isNotEmpty() == true) {
|
if (message.attachment?.attachments?.isNotEmpty() == true) {
|
||||||
|
thread_mesage_attachments_holder.beVisible()
|
||||||
|
thread_mesage_attachments_holder.removeAllViews()
|
||||||
for (attachment in message.attachment.attachments) {
|
for (attachment in message.attachment.attachments) {
|
||||||
val mimetype = attachment.mimetype
|
val mimetype = attachment.mimetype
|
||||||
when {
|
when {
|
||||||
@@ -272,6 +273,8 @@ class ThreadAdapter(
|
|||||||
|
|
||||||
thread_message_play_outline.beVisibleIf(mimetype.startsWith("video/"))
|
thread_message_play_outline.beVisibleIf(mimetype.startsWith("video/"))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
thread_mesage_attachments_holder.beGone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ import com.simplemobiletools.smsmessenger.extensions.getFileSizeFromUri
|
|||||||
import com.simplemobiletools.smsmessenger.extensions.isImageMimeType
|
import com.simplemobiletools.smsmessenger.extensions.isImageMimeType
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compress image to a given size based on
|
* Compress image to a given size based on
|
||||||
@@ -28,7 +29,11 @@ class ImageCompressor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun compressImage(uri: Uri, compressSize: Long, callback: (compressedFileUri: Uri?) -> Unit) {
|
private val minQuality = 30
|
||||||
|
private val minResolution = 56
|
||||||
|
private val scaleStepFactor = 0.6f // increase for more accurate file size at the cost increased computation
|
||||||
|
|
||||||
|
fun compressImage(uri: Uri, compressSize: Long, lossy: Boolean = compressSize < FILE_SIZE_1_MB, callback: (compressedFileUri: Uri?) -> Unit) {
|
||||||
ensureBackgroundThread {
|
ensureBackgroundThread {
|
||||||
try {
|
try {
|
||||||
val fileSize = context.getFileSizeFromUri(uri)
|
val fileSize = context.getFileSizeFromUri(uri)
|
||||||
@@ -36,28 +41,49 @@ class ImageCompressor(private val context: Context) {
|
|||||||
val mimeType = contentResolver.getType(uri)!!
|
val mimeType = contentResolver.getType(uri)!!
|
||||||
if (mimeType.isImageMimeType()) {
|
if (mimeType.isImageMimeType()) {
|
||||||
val byteArray = contentResolver.openInputStream(uri)?.readBytes()!!
|
val byteArray = contentResolver.openInputStream(uri)?.readBytes()!!
|
||||||
var destinationFile = File(outputDirectory, System.currentTimeMillis().toString().plus(mimeType.getExtensionFromMimeType()))
|
var imageFile = File(outputDirectory, System.currentTimeMillis().toString().plus(mimeType.getExtensionFromMimeType()))
|
||||||
destinationFile.writeBytes(byteArray)
|
imageFile.writeBytes(byteArray)
|
||||||
val sizeConstraint = SizeConstraint(compressSize)
|
val bitmap = loadBitmap(imageFile)
|
||||||
val bitmap = loadBitmap(destinationFile)
|
val format = if (lossy) {
|
||||||
|
Bitmap.CompressFormat.JPEG
|
||||||
|
} else {
|
||||||
|
imageFile.path.getCompressionFormat()
|
||||||
|
}
|
||||||
|
|
||||||
// if image weight > * 2 targeted size: cut down resolution by 2
|
// This quality approximation mostly works for smaller images but will fail with larger images.
|
||||||
if (fileSize > 2 * compressSize) {
|
val compressionRatio = compressSize / fileSize.toDouble()
|
||||||
val resConstraint = ResolutionConstraint(bitmap.width / 2, bitmap.height / 2)
|
val quality = maxOf((compressionRatio * 100).roundToInt(), minQuality)
|
||||||
while (resConstraint.isSatisfied(destinationFile).not()) {
|
imageFile = overWrite(imageFile, bitmap, format = format, quality = quality)
|
||||||
destinationFile = resConstraint.satisfy(destinationFile)
|
|
||||||
|
// Even the highest quality images start to look ugly if we use 10 as the minimum quality,
|
||||||
|
// so we better save some image quality and change resolution instead. This is time consuming
|
||||||
|
// and mostly needed for very large images. Since there's no reliable way to predict the
|
||||||
|
// required resolution, we'll just iterate and find the best result.
|
||||||
|
if (imageFile.length() > compressSize) {
|
||||||
|
var scaledWidth = bitmap.width
|
||||||
|
var scaledHeight = bitmap.height
|
||||||
|
|
||||||
|
while (imageFile.length() > compressSize) {
|
||||||
|
scaledWidth = (scaledWidth * scaleStepFactor).roundToInt()
|
||||||
|
scaledHeight = (scaledHeight * scaleStepFactor).roundToInt()
|
||||||
|
if (scaledHeight < minResolution && scaledWidth < minResolution) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile = decodeSampledBitmapFromFile(imageFile, scaledWidth, scaledHeight).run {
|
||||||
|
determineImageRotation(imageFile, bitmap = this).run {
|
||||||
|
overWrite(imageFile, bitmap = this, format = format, quality = quality)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// do compression
|
|
||||||
while (sizeConstraint.isSatisfied(destinationFile).not()) {
|
callback.invoke(context.getMyFileUri(imageFile))
|
||||||
destinationFile = sizeConstraint.satisfy(destinationFile)
|
|
||||||
}
|
|
||||||
callback.invoke(context.getMyFileUri(destinationFile))
|
|
||||||
} else {
|
} else {
|
||||||
callback.invoke(null)
|
callback.invoke(null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//no need to compress since the file is less than the compress size
|
// no need to compress since the file is less than the compress size
|
||||||
callback.invoke(uri)
|
callback.invoke(uri)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -107,77 +133,36 @@ class ImageCompressor(private val context: Context) {
|
|||||||
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class SizeConstraint(
|
private fun decodeSampledBitmapFromFile(imageFile: File, reqWidth: Int, reqHeight: Int): Bitmap {
|
||||||
private val maxFileSize: Long,
|
return BitmapFactory.Options().run {
|
||||||
private val stepSize: Int = 10,
|
inJustDecodeBounds = true
|
||||||
private val maxIteration: Int = 10,
|
BitmapFactory.decodeFile(imageFile.absolutePath, this)
|
||||||
private val minQuality: Int = 10
|
|
||||||
) {
|
|
||||||
private var iteration: Int = 0
|
|
||||||
|
|
||||||
fun isSatisfied(imageFile: File): Boolean {
|
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
|
||||||
// If size requirement is not met and maxIteration is reached
|
|
||||||
if (iteration >= maxIteration && imageFile.length() >= maxFileSize) {
|
|
||||||
throw Exception("Unable to compress image to targeted size")
|
|
||||||
}
|
|
||||||
return imageFile.length() <= maxFileSize
|
|
||||||
}
|
|
||||||
|
|
||||||
fun satisfy(imageFile: File): File {
|
inJustDecodeBounds = false
|
||||||
iteration++
|
BitmapFactory.decodeFile(imageFile.absolutePath, this)
|
||||||
val quality = (100 - iteration * stepSize).takeIf { it >= minQuality } ?: minQuality
|
|
||||||
return overWrite(imageFile, loadBitmap(imageFile), quality = quality)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ResolutionConstraint(private val width: Int, private val height: Int) {
|
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||||
|
// Raw height and width of image
|
||||||
|
val height = options.outHeight
|
||||||
|
val width = options.outWidth
|
||||||
|
var inSampleSize = 1
|
||||||
|
|
||||||
private fun decodeSampledBitmapFromFile(imageFile: File, reqWidth: Int, reqHeight: Int): Bitmap {
|
if (height > reqHeight || width > reqWidth) {
|
||||||
return BitmapFactory.Options().run {
|
|
||||||
inJustDecodeBounds = true
|
|
||||||
BitmapFactory.decodeFile(imageFile.absolutePath, this)
|
|
||||||
|
|
||||||
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
|
val halfHeight: Int = height / 2
|
||||||
|
val halfWidth: Int = width / 2
|
||||||
|
|
||||||
inJustDecodeBounds = false
|
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||||
BitmapFactory.decodeFile(imageFile.absolutePath, this)
|
// height and width larger than the requested height and width.
|
||||||
|
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
|
||||||
|
inSampleSize *= 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
return inSampleSize
|
||||||
// Raw height and width of image
|
|
||||||
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
|
||||||
var inSampleSize = 1
|
|
||||||
|
|
||||||
if (height > reqHeight || width > reqWidth) {
|
|
||||||
|
|
||||||
val halfHeight: Int = height / 2
|
|
||||||
val halfWidth: Int = width / 2
|
|
||||||
|
|
||||||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
|
||||||
// height and width larger than the requested height and width.
|
|
||||||
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
|
|
||||||
inSampleSize *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inSampleSize
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isSatisfied(imageFile: File): Boolean {
|
|
||||||
return BitmapFactory.Options().run {
|
|
||||||
inJustDecodeBounds = true
|
|
||||||
BitmapFactory.decodeFile(imageFile.absolutePath, this)
|
|
||||||
calculateInSampleSize(this, width, height) <= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun satisfy(imageFile: File): File {
|
|
||||||
return decodeSampledBitmapFromFile(imageFile, width, height).run {
|
|
||||||
determineImageRotation(imageFile, this).run {
|
|
||||||
overWrite(imageFile, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -40,7 +40,16 @@ fun Context.sendMessageCompat(text: String, addresses: List<String>, subId: Int?
|
|||||||
|
|
||||||
val isMms = attachments.isNotEmpty() || isLongMmsMessage(text, settings) || addresses.size > 1 && settings.group
|
val isMms = attachments.isNotEmpty() || isLongMmsMessage(text, settings) || addresses.size > 1 && settings.group
|
||||||
if (isMms) {
|
if (isMms) {
|
||||||
messagingUtils.sendMmsMessage(text, addresses, attachments, settings)
|
// we send all MMS attachments separately to reduces the chances of hitting provider MMS limit.
|
||||||
|
if (attachments.size > 1) {
|
||||||
|
for (i in 0 until attachments.lastIndex) {
|
||||||
|
val attachment = attachments[i]
|
||||||
|
messagingUtils.sendMmsMessage("", addresses, listOf(attachment), settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lastAttachment = attachments[attachments.lastIndex]
|
||||||
|
messagingUtils.sendMmsMessage(text, addresses, listOf(lastAttachment), settings)
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
messagingUtils.sendSmsMessage(text, addresses.toSet(), settings.subscriptionId, settings.deliveryReports)
|
messagingUtils.sendSmsMessage(text, addresses.toSet(), settings.subscriptionId, settings.deliveryReports)
|
||||||
|
@@ -42,9 +42,7 @@ class MmsSentReceiver : SendStatusReceiver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
|
override fun updateAppDatabase(context: Context, intent: Intent, receiverResultCode: Int) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
refreshMessages()
|
||||||
refreshMessages()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@@ -5,5 +5,4 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:paddingBottom="@dimen/medium_margin"
|
|
||||||
app:shapeAppearanceOverlay="@style/roundedImageView" />
|
app:shapeAppearanceOverlay="@style/roundedImageView" />
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
android:id="@+id/thread_message_holder"
|
android:id="@+id/thread_message_holder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/medium_margin"
|
android:layout_marginTop="@dimen/small_margin"
|
||||||
android:foreground="@drawable/selector"
|
android:foreground="@drawable/selector"
|
||||||
android:paddingStart="@dimen/activity_margin"
|
android:paddingStart="@dimen/activity_margin"
|
||||||
android:paddingEnd="@dimen/activity_margin">
|
android:paddingEnd="@dimen/activity_margin">
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
android:id="@+id/thread_mesage_attachments_holder"
|
android:id="@+id/thread_mesage_attachments_holder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginVertical="@dimen/tiny_margin"
|
||||||
android:divider="@drawable/linear_layout_vertical_divider"
|
android:divider="@drawable/linear_layout_vertical_divider"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:showDividers="middle" />
|
android:showDividers="middle" />
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/thread_mesage_attachments_holder"
|
android:layout_below="@+id/thread_mesage_attachments_holder"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginVertical="@dimen/tiny_margin"
|
||||||
android:autoLink="email|web"
|
android:autoLink="email|web"
|
||||||
android:background="@drawable/item_sent_background"
|
android:background="@drawable/item_sent_background"
|
||||||
android:padding="@dimen/normal_margin"
|
android:padding="@dimen/normal_margin"
|
||||||
|
Reference in New Issue
Block a user