Skip to content

Commit 0b1107d

Browse files
committed
✨ Feat: ImageGenerator 메모리 최적화 및 샘플링 로직 추가
1 parent db90af9 commit 0b1107d

File tree

1 file changed

+57
-14
lines changed

1 file changed

+57
-14
lines changed

core/ui/src/main/java/com/twix/ui/image/ImageGenerator.kt

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ class ImageGenerator(
1616
* 주어진 [Uri]로부터 이미지를 읽어 JPEG 형식의 [ByteArray]로 변환한다.
1717
*
1818
* 내부 동작 과정:
19-
* 1. [android.content.ContentResolver.openInputStream]으로 InputStream을 연다.
20-
* 2. [android.graphics.BitmapFactory.decodeStream]으로 Bitmap 디코딩
21-
* 3. JPEG(품질 90) 압축 후 ByteArray 반환
19+
* 1. EXIF 메타데이터로 회전 방향 확인
20+
* 2. [Uri]로부터 Bitmap 디코딩 (메모리 최적화를 위해 샘플링 적용)
21+
* 3. 필요 시 회전 처리 후 JPEG(품질 90) 압축하여 [ByteArray] 반환
2222
*
2323
* 실패 케이스:
2424
* - InputStream 열기 실패
25-
* - 디코딩 실패 (손상 이미지 등)
25+
* - 디코딩 실패 (손상된 이미지 등)
2626
* - 압축 실패
2727
*
2828
* @param imageUri 변환할 이미지 Uri (content:// 또는 file://)
@@ -40,9 +40,7 @@ class ImageGenerator(
4040
else -> bitmap
4141
}
4242

43-
/**
44-
* 회전된 새로운 비트맵이 생성되었다면 원본은 즉시 해제
45-
* */
43+
// 회전된 새 비트맵이 생성된 경우 원본 즉시 해제
4644
if (rotatedBitmap !== bitmap) bitmap.recycle()
4745
bitmapToByteArray(rotatedBitmap)
4846
} catch (e: Exception) {
@@ -51,23 +49,68 @@ class ImageGenerator(
5149
}
5250

5351
/**
54-
* [Uri] 로부터 실제 [Bitmap] 을 디코딩한다.
52+
* [Uri]로부터 [Bitmap]을 디코딩한다.
5553
*
56-
* 새로운 InputStream을 열어 [BitmapFactory.decodeStream] 으로 변환한다.
54+
* 메모리 사용량을 줄이기 위해 두 단계로 디코딩한다.
55+
* 1. [BitmapFactory.Options.inJustDecodeBounds]로 이미지 크기만 먼저 읽기
56+
* 2. [calculateInSampleSize]로 샘플 크기를 계산한 뒤 실제 디코딩
57+
*
58+
* @throws ImageProcessException.DecodeFailedException 디코딩 실패 시
5759
*/
58-
private fun uriToBitmap(imageUri: Uri): Bitmap =
60+
private fun uriToBitmap(imageUri: Uri): Bitmap {
61+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
5962
contentResolver.openInputStream(imageUri)?.use { inputStream ->
60-
BitmapFactory.decodeStream(inputStream)
63+
BitmapFactory.decodeStream(inputStream, null, bounds)
64+
}
65+
66+
val options =
67+
BitmapFactory.Options().apply {
68+
inSampleSize = calculateInSampleSize(bounds, 1920, 1080)
69+
}
70+
71+
return contentResolver.openInputStream(imageUri)?.use { inputStream ->
72+
BitmapFactory.decodeStream(inputStream, null, options)
6173
} ?: throw ImageProcessException.DecodeFailedException(imageUri)
74+
}
75+
76+
/**
77+
* 목표 해상도([reqWidth] x [reqHeight])에 맞는 최적의 [BitmapFactory.Options.inSampleSize]를 계산한다.
78+
*
79+
* 반환값은 2의 거듭제곱이며, 디코딩된 이미지가 목표 해상도보다 작아지지 않는 최대값을 반환한다.
80+
*
81+
* @param options outWidth, outHeight가 채워진 [BitmapFactory.Options]
82+
* @param reqWidth 목표 너비 (px)
83+
* @param reqHeight 목표 높이 (px)
84+
* @return 계산된 inSampleSize (최솟값 1)
85+
*/
86+
fun calculateInSampleSize(
87+
options: BitmapFactory.Options,
88+
reqWidth: Int,
89+
reqHeight: Int,
90+
): Int {
91+
val (height: Int, width: Int) = options.run { outHeight to outWidth }
92+
var inSampleSize = 1
93+
94+
if (height > reqHeight || width > reqWidth) {
95+
val halfHeight: Int = height / 2
96+
val halfWidth: Int = width / 2
97+
98+
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
99+
inSampleSize *= 2
100+
}
101+
}
102+
103+
return inSampleSize
104+
}
62105

63106
/**
64-
* [Bitmap] 을 JPEG 형식(품질 90)으로 압축하여 [ByteArray] 로 변환한다.
107+
* [Bitmap]을 JPEG 형식(품질 90)으로 압축하여 [ByteArray]로 변환한다.
65108
*
66-
* 압축 완료 후 메모리 절약을 위해 내부에서 [Bitmap.recycle] 을 호출한다.
67-
* 따라서 호출 이후 전달한 Bitmap은 재사용하면 안 된다.
109+
* 압축 완료 후 [Bitmap.recycle]을 호출하므로, 이후 해당 [Bitmap]을 재사용해선 안 된다.
68110
*
69111
* @param bitmap 압축 대상 Bitmap
70112
* @return JPEG 바이트 배열
113+
* @throws ImageProcessException.CompressionFailedException 압축 실패 시
71114
*/
72115
private fun bitmapToByteArray(bitmap: Bitmap): ByteArray {
73116
val outputStream = ByteArrayOutputStream()

0 commit comments

Comments
 (0)