android: compress images before media upload
This commit is contained in:
@@ -639,3 +639,11 @@
|
||||
- Added Android support for `GET /api/v1/chats/saved`.
|
||||
- Wired chats overflow `Saved` action to real backend request (instead of local title heuristic).
|
||||
- Saved chat is now upserted into local Room cache and opened via normal navigation flow.
|
||||
|
||||
### Step 100 - Android image compression before upload
|
||||
- Added pre-upload image compression in Android media pipeline (`NetworkMediaRepository`).
|
||||
- For non-GIF images:
|
||||
- decode + resize with max side `1920`,
|
||||
- re-encode as `image/jpeg` with quality `82`,
|
||||
- keep original bytes if compression does not reduce payload size.
|
||||
- Upload request and attachment metadata now use actual prepared payload (`fileName`, `fileType`, `fileSize`), matching web behavior.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ru.daemonlord.messenger.data.media.repository
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
@@ -15,6 +17,8 @@ import ru.daemonlord.messenger.di.RefreshClient
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.math.roundToInt
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -32,15 +36,20 @@ class NetworkMediaRepository @Inject constructor(
|
||||
bytes: ByteArray,
|
||||
): AppResult<Unit> = withContext(ioDispatcher) {
|
||||
try {
|
||||
val uploadPayload = prepareUploadPayload(
|
||||
fileName = fileName,
|
||||
mimeType = mimeType,
|
||||
bytes = bytes,
|
||||
)
|
||||
val uploadInfo = mediaApiService.requestUploadUrl(
|
||||
request = UploadUrlRequestDto(
|
||||
fileName = fileName,
|
||||
fileType = mimeType,
|
||||
fileSize = bytes.size.toLong(),
|
||||
fileName = uploadPayload.fileName,
|
||||
fileType = uploadPayload.mimeType,
|
||||
fileSize = uploadPayload.bytes.size.toLong(),
|
||||
)
|
||||
)
|
||||
|
||||
val body = bytes.toRequestBody(mimeType.toMediaTypeOrNull())
|
||||
val body = uploadPayload.bytes.toRequestBody(uploadPayload.mimeType.toMediaTypeOrNull())
|
||||
val uploadRequestBuilder = Request.Builder()
|
||||
.url(uploadInfo.uploadUrl)
|
||||
.put(body)
|
||||
@@ -61,8 +70,8 @@ class NetworkMediaRepository @Inject constructor(
|
||||
request = AttachmentCreateRequestDto(
|
||||
messageId = messageId,
|
||||
fileUrl = uploadInfo.fileUrl,
|
||||
fileType = mimeType,
|
||||
fileSize = bytes.size.toLong(),
|
||||
fileType = uploadPayload.mimeType,
|
||||
fileSize = uploadPayload.bytes.size.toLong(),
|
||||
)
|
||||
)
|
||||
AppResult.Success(Unit)
|
||||
@@ -70,4 +79,58 @@ class NetworkMediaRepository @Inject constructor(
|
||||
AppResult.Error(error.toAppError())
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareUploadPayload(
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
bytes: ByteArray,
|
||||
): UploadPayload {
|
||||
if (!mimeType.startsWith("image/", ignoreCase = true)) {
|
||||
return UploadPayload(fileName = fileName, mimeType = mimeType, bytes = bytes)
|
||||
}
|
||||
if (mimeType.equals("image/gif", ignoreCase = true)) {
|
||||
return UploadPayload(fileName = fileName, mimeType = mimeType, bytes = bytes)
|
||||
}
|
||||
val source = runCatching { BitmapFactory.decodeByteArray(bytes, 0, bytes.size) }.getOrNull()
|
||||
?: return UploadPayload(fileName = fileName, mimeType = mimeType, bytes = bytes)
|
||||
val maxSide = 1920
|
||||
val width = source.width
|
||||
val height = source.height
|
||||
val scale = (maxSide.toFloat() / maxOf(width, height).toFloat()).coerceAtMost(1f)
|
||||
val targetWidth = (width * scale).roundToInt().coerceAtLeast(1)
|
||||
val targetHeight = (height * scale).roundToInt().coerceAtLeast(1)
|
||||
|
||||
val scaled = if (targetWidth != width || targetHeight != height) {
|
||||
Bitmap.createScaledBitmap(source, targetWidth, targetHeight, true)
|
||||
} else {
|
||||
source
|
||||
}
|
||||
val output = ByteArrayOutputStream()
|
||||
val compressedOk = runCatching {
|
||||
scaled.compress(Bitmap.CompressFormat.JPEG, 82, output)
|
||||
}.getOrDefault(false)
|
||||
if (scaled !== source) {
|
||||
scaled.recycle()
|
||||
}
|
||||
source.recycle()
|
||||
if (!compressedOk) {
|
||||
return UploadPayload(fileName = fileName, mimeType = mimeType, bytes = bytes)
|
||||
}
|
||||
val compressedBytes = output.toByteArray()
|
||||
if (compressedBytes.size >= bytes.size) {
|
||||
return UploadPayload(fileName = fileName, mimeType = mimeType, bytes = bytes)
|
||||
}
|
||||
val baseName = fileName.substringBeforeLast('.').ifBlank { "image" }
|
||||
return UploadPayload(
|
||||
fileName = "$baseName-web.jpg",
|
||||
mimeType = "image/jpeg",
|
||||
bytes = compressedBytes,
|
||||
)
|
||||
}
|
||||
|
||||
private data class UploadPayload(
|
||||
val fileName: String,
|
||||
val mimeType: String,
|
||||
val bytes: ByteArray,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user