Skip to content

Commit facb32e

Browse files
committed
refactor(AvatarAdjuster): adjuster now crops avatar too
1 parent bae91a6 commit facb32e

2 files changed

Lines changed: 30 additions & 99 deletions

File tree

app/src/main/java/friendly/android/AvatarAdjuster.kt

Lines changed: 24 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,122 +3,59 @@ package friendly.android
33
import android.content.Context
44
import android.graphics.Bitmap
55
import android.graphics.BitmapFactory
6-
import android.graphics.Matrix
76
import android.net.Uri
8-
import android.util.Log
97
import androidx.core.graphics.scale
10-
import androidx.exifinterface.media.ExifInterface
11-
import kotlinx.io.IOException
128
import java.io.ByteArrayInputStream
139
import java.io.ByteArrayOutputStream
1410
import java.io.InputStream
1511

16-
class ImageCompressor(
12+
private const val newAvatarDimension = 500
13+
private const val maxAvatarSize = 16_384L
14+
private const val startQuality = 80
15+
private const val qualityStep = 10
16+
private const val minQuality = 40
17+
18+
class AvatarAdjuster(
1719
private val uri: Uri,
1820
private val inputStream: InputStream,
19-
private val maxImageSize: Long,
20-
private val compressionQuality: Int,
21-
private val maxFileSizeBytes: Long,
22-
private val context: Context,
21+
context: Context,
2322
) {
2423
private val contentResolver = context.contentResolver
2524

26-
fun compress(): Pair<InputStream, Long> = compressImage(uri, inputStream)
25+
/**
26+
* Resizes, crops with aspect ration 1x1 and compresses avatar
27+
*/
28+
fun adjust(): Pair<InputStream, Long> = adjustAvatar(uri, inputStream)
2729

28-
private fun compressImage(
30+
private fun adjustAvatar(
2931
uri: Uri,
3032
inputStream: InputStream,
3133
): Pair<InputStream, Long> {
3234
val bitmap = BitmapFactory.decodeStream(inputStream)
3335
?: error("Failed to decode image")
34-
35-
val rotatedBitmap = handleImageRotation(uri, bitmap)
36-
37-
val widthIsLarger = rotatedBitmap.width > maxImageSize
38-
val heightIsLarger = rotatedBitmap.height > maxImageSize
39-
val isScaled = widthIsLarger || heightIsLarger
40-
41-
val scaledBitmap = if (isScaled) {
42-
scaleBitmap(rotatedBitmap)
43-
} else {
44-
rotatedBitmap
45-
}
36+
val croppedDimension = minOf(bitmap.width, bitmap.height)
37+
val croppedBitmap = Bitmap
38+
.createBitmap(bitmap, 0, 0, croppedDimension, croppedDimension)
39+
val scaledBitmap = croppedBitmap
40+
.scale(newAvatarDimension, newAvatarDimension)
4641

4742
val outputStream = ByteArrayOutputStream()
48-
var quality = compressionQuality
49-
43+
var quality = startQuality
5044
do {
5145
outputStream.reset()
5246
scaledBitmap.compress(
5347
Bitmap.CompressFormat.JPEG,
5448
quality,
5549
outputStream,
5650
)
57-
quality -= 5
58-
} while (outputStream.size() > maxFileSizeBytes && quality > 50)
51+
quality -= qualityStep
52+
} while (outputStream.size() > maxAvatarSize && quality > minQuality)
5953

6054
val byteArray = outputStream.toByteArray()
6155

62-
if (scaledBitmap != rotatedBitmap) scaledBitmap.recycle()
63-
if (rotatedBitmap != bitmap) rotatedBitmap.recycle()
64-
bitmap.recycle()
65-
66-
Log.d(
67-
"avatar",
68-
"Compressed image: ${byteArray.size} bytes (quality: $quality)",
69-
)
70-
71-
return Pair(ByteArrayInputStream(byteArray), byteArray.size.toLong())
72-
}
73-
74-
private fun handleImageRotation(uri: Uri, bitmap: Bitmap): Bitmap {
75-
val inputStream = contentResolver.openInputStream(uri) ?: return bitmap
76-
77-
return try {
78-
val exif = ExifInterface(inputStream)
79-
val orientation = exif.getAttributeInt(
80-
ExifInterface.TAG_ORIENTATION,
81-
ExifInterface.ORIENTATION_NORMAL,
82-
)
83-
84-
val rotation = when (orientation) {
85-
ExifInterface.ORIENTATION_ROTATE_90 -> 90f
86-
ExifInterface.ORIENTATION_ROTATE_180 -> 180f
87-
ExifInterface.ORIENTATION_ROTATE_270 -> 270f
88-
else -> 0f
89-
}
90-
91-
if (rotation != 0f) {
92-
val matrix = Matrix().apply { postRotate(rotation) }
93-
Bitmap.createBitmap(
94-
bitmap,
95-
0,
96-
0,
97-
bitmap.width,
98-
bitmap.height,
99-
matrix,
100-
true,
101-
)
102-
} else {
103-
bitmap
104-
}
105-
} catch (exception: IOException) {
106-
Log.w("avatar", "Failed to read EXIF data", exception)
107-
bitmap
108-
} finally {
109-
inputStream.close()
110-
}
111-
}
112-
113-
private fun scaleBitmap(bitmap: Bitmap): Bitmap {
114-
val ratio = minOf(
115-
maxImageSize.toFloat() / bitmap.width,
116-
maxImageSize.toFloat() / bitmap.height,
56+
return Pair(
57+
first = ByteArrayInputStream(outputStream.toByteArray()),
58+
second = byteArray.size.toLong(),
11759
)
118-
119-
val newWidth = (bitmap.width * ratio).toInt()
120-
val newHeight = (bitmap.height * ratio).toInt()
121-
122-
return bitmap.scale(newWidth, newHeight)
12360
}
12461
}

app/src/main/java/friendly/android/AvatarUploadUseCase.kt

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,6 @@ class AvatarUploadUseCase(
2121
private val client: FriendlyClient,
2222
private val context: Context,
2323
) {
24-
companion object {
25-
private const val MAX_IMAGE_SIZE = 2048L
26-
private const val COMPRESSION_QUALITY = 75
27-
private const val MAX_FILE_SIZE_BYTES = 2L * 1024L
28-
}
29-
3024
@JvmInline
3125
value class UploadingPercentage(val double: Double)
3226

@@ -49,18 +43,16 @@ class AvatarUploadUseCase(
4943
val inputStream = contentResolver
5044
.openInputStream(avatarUri)
5145
?: error("Couldn't read $avatarUri from the storage")
46+
5247
val fileName = getFileName(avatarUri)
5348
val fileDescriptorUploadResult = CompletableDeferred<UploadFileResult>()
54-
val compressor = ImageCompressor(
49+
val compressor = AvatarAdjuster(
5550
uri = avatarUri,
5651
inputStream = inputStream,
57-
maxImageSize = MAX_IMAGE_SIZE,
58-
compressionQuality = COMPRESSION_QUALITY,
59-
maxFileSizeBytes = MAX_FILE_SIZE_BYTES,
6052
context = context,
6153
)
6254

63-
val (compressedInputStream, compressedSize) = compressor.compress()
55+
val (compressedInputStream, compressedSize) = compressor.adjust()
6456

6557
compressedInputStream.use { inputStream ->
6658
val flow = channelFlow {
@@ -79,7 +71,9 @@ class AvatarUploadUseCase(
7971
UploadingResult.IOError(result.cause)
8072
}
8173

82-
is UploadFileResult.ServerError -> UploadingResult.ServerError
74+
is UploadFileResult.ServerError -> {
75+
UploadingResult.ServerError
76+
}
8377

8478
is UploadFileResult.Success -> {
8579
UploadingResult.Success(result.descriptor)

0 commit comments

Comments
 (0)