android: compress images before media upload
This commit is contained in:
@@ -639,3 +639,11 @@
|
|||||||
- Added Android support for `GET /api/v1/chats/saved`.
|
- Added Android support for `GET /api/v1/chats/saved`.
|
||||||
- Wired chats overflow `Saved` action to real backend request (instead of local title heuristic).
|
- 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.
|
- 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
|
package ru.daemonlord.messenger.data.media.repository
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
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.AppError
|
||||||
import ru.daemonlord.messenger.domain.common.AppResult
|
import ru.daemonlord.messenger.domain.common.AppResult
|
||||||
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -32,15 +36,20 @@ class NetworkMediaRepository @Inject constructor(
|
|||||||
bytes: ByteArray,
|
bytes: ByteArray,
|
||||||
): AppResult<Unit> = withContext(ioDispatcher) {
|
): AppResult<Unit> = withContext(ioDispatcher) {
|
||||||
try {
|
try {
|
||||||
|
val uploadPayload = prepareUploadPayload(
|
||||||
|
fileName = fileName,
|
||||||
|
mimeType = mimeType,
|
||||||
|
bytes = bytes,
|
||||||
|
)
|
||||||
val uploadInfo = mediaApiService.requestUploadUrl(
|
val uploadInfo = mediaApiService.requestUploadUrl(
|
||||||
request = UploadUrlRequestDto(
|
request = UploadUrlRequestDto(
|
||||||
fileName = fileName,
|
fileName = uploadPayload.fileName,
|
||||||
fileType = mimeType,
|
fileType = uploadPayload.mimeType,
|
||||||
fileSize = bytes.size.toLong(),
|
fileSize = uploadPayload.bytes.size.toLong(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val body = bytes.toRequestBody(mimeType.toMediaTypeOrNull())
|
val body = uploadPayload.bytes.toRequestBody(uploadPayload.mimeType.toMediaTypeOrNull())
|
||||||
val uploadRequestBuilder = Request.Builder()
|
val uploadRequestBuilder = Request.Builder()
|
||||||
.url(uploadInfo.uploadUrl)
|
.url(uploadInfo.uploadUrl)
|
||||||
.put(body)
|
.put(body)
|
||||||
@@ -61,8 +70,8 @@ class NetworkMediaRepository @Inject constructor(
|
|||||||
request = AttachmentCreateRequestDto(
|
request = AttachmentCreateRequestDto(
|
||||||
messageId = messageId,
|
messageId = messageId,
|
||||||
fileUrl = uploadInfo.fileUrl,
|
fileUrl = uploadInfo.fileUrl,
|
||||||
fileType = mimeType,
|
fileType = uploadPayload.mimeType,
|
||||||
fileSize = bytes.size.toLong(),
|
fileSize = uploadPayload.bytes.size.toLong(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
AppResult.Success(Unit)
|
AppResult.Success(Unit)
|
||||||
@@ -70,4 +79,58 @@ class NetworkMediaRepository @Inject constructor(
|
|||||||
AppResult.Error(error.toAppError())
|
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