Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 비트맵 리사이징 #142

Merged
merged 4 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sopt.motivoo.data.repository

import android.graphics.Bitmap
import okhttp3.RequestBody.Companion.toRequestBody
import sopt.motivoo.data.datasource.remote.HomeDataSource
import sopt.motivoo.data.model.request.home.RequestMissionTodayDto
import sopt.motivoo.domain.entity.error.ResponseHandler
Expand All @@ -10,7 +9,7 @@ import sopt.motivoo.domain.entity.home.MissionChoiceData
import sopt.motivoo.domain.entity.home.MissionImageData
import sopt.motivoo.domain.error.UserErrorHandler
import sopt.motivoo.domain.repository.HomeRepository
import java.io.ByteArrayOutputStream
import sopt.motivoo.util.BitmapRequestBody
import javax.inject.Inject

class HomeRepositoryImpl @Inject constructor(
Expand Down Expand Up @@ -53,9 +52,7 @@ class HomeRepositoryImpl @Inject constructor(
}

override suspend fun uploadPhoto(url: String, bitmap: Bitmap): Unit? = try {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 80, outputStream)
val requestBody = outputStream.toByteArray().toRequestBody()
val requestBody = BitmapRequestBody(bitmap).create(50)
homeDataSource.uploadPhoto(url, requestBody)
} catch (e: Exception) {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import sopt.motivoo.R
import sopt.motivoo.databinding.BottomSheetHomeBinding
import sopt.motivoo.presentation.home.HomePictureState
import sopt.motivoo.presentation.home.viewmodel.HomeViewModel
import sopt.motivoo.util.BitmapUtil
import sopt.motivoo.util.Constants.S3_BUCKET_NAME
import sopt.motivoo.util.UriManager
import sopt.motivoo.util.extension.createUriToBitmap

@AndroidEntryPoint
class HomeBottomSheetFragment : BottomSheetDialogFragment() {
Expand All @@ -39,6 +39,7 @@ class HomeBottomSheetFragment : BottomSheetDialogFragment() {
private val viewModel: HomeViewModel by viewModels()

var pictureUri: Uri? = null
private lateinit var bitmapUtil: BitmapUtil

private val isCameraPermissionResult =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
Expand All @@ -60,8 +61,10 @@ class HomeBottomSheetFragment : BottomSheetDialogFragment() {
registerForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
if (isSuccess) {
binding.pvLoading.visibility = View.VISIBLE
pictureUri?.let {
viewModel.getMissionImage(S3_BUCKET_NAME, requireContext().createUriToBitmap(it))
pictureUri?.let { uri ->
bitmapUtil.createUriToBitmap(uri, size = 4)?.let { bitmap ->
viewModel.getMissionImage(S3_BUCKET_NAME, bitmap)
}
}
}
}
Expand Down Expand Up @@ -92,6 +95,7 @@ class HomeBottomSheetFragment : BottomSheetDialogFragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bitmapUtil = BitmapUtil(requireContext())
setLayoutSize()
collectHomePictureState()
onClickTakePicture()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,36 @@ import sopt.motivoo.R
import sopt.motivoo.databinding.DialogHomePhotoBinding
import sopt.motivoo.presentation.home.HomePictureState
import sopt.motivoo.presentation.home.viewmodel.HomeViewModel
import sopt.motivoo.util.BitmapUtil
import sopt.motivoo.util.Constants.S3_BUCKET_NAME
import sopt.motivoo.util.binding.BindingDialogFragment
import sopt.motivoo.util.extension.createUriToBitmap
import sopt.motivoo.util.extension.showToast

@AndroidEntryPoint
class HomePhotoDialogFragment :
BindingDialogFragment<DialogHomePhotoBinding>(R.layout.dialog_home_photo) {

private val viewModel: HomeViewModel by viewModels()
private lateinit var photoUri: Uri
private lateinit var bitmapUtil: BitmapUtil

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setLayoutSizeRatio(widthPercent = 1f, heightPercent = 1f)
bitmapUtil = BitmapUtil(requireContext())

val safeArgs: HomePhotoDialogFragmentArgs by navArgs()
photoUri = safeArgs.photoUri

binding.ivPhoto.load(photoUri)

binding.tvConfirm.setOnClickListener {
binding.pvLoading.visibility = View.VISIBLE
viewModel.getMissionImage(
S3_BUCKET_NAME,
requireContext().createUriToBitmap(safeArgs.photoUri)
)
bitmapUtil.createUriToBitmap(photoUri, size = 4)?.let { bitmap ->
binding.pvLoading.visibility = View.VISIBLE
viewModel.getMissionImage(
S3_BUCKET_NAME, bitmap
)
} ?: requireContext().showToast("createUriToBitmap is null")
}

viewLifecycleOwner.lifecycleScope.launch {
Expand Down
24 changes: 24 additions & 0 deletions app/src/main/java/sopt/motivoo/util/BitmapRequestBody.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package sopt.motivoo.util

import android.graphics.Bitmap
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.ByteArrayOutputStream

class BitmapRequestBody(private val bitmap: Bitmap) {

/**
* byte size = output.toByteArray().size
*/
fun create(quality: Int = 100): RequestBody {
val output = ByteArrayOutputStream()
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, output)
} catch (e: Exception) {
e.message
} finally {
output.close()
}
return output.toByteArray().toRequestBody()
}
}
92 changes: 92 additions & 0 deletions app/src/main/java/sopt/motivoo/util/BitmapUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package sopt.motivoo.util

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import android.provider.MediaStore

class BitmapUtil(private val context: Context) {
private fun loadOrientation(uri: Uri): Int {
var orientation = 0
val stream = context.contentResolver.openInputStream(uri) ?: return 0
try {
val exifInterface = ExifInterface(stream)
orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
} catch (e: Exception) {
e.message
} finally {
stream.close()
}
return orientation
}

/**
* @param bounds : if ture, assign bitmap in memory. if false, no assign bitmap in memory
* @param size : return image ratio
*/
private fun decodeUriToBitmap(uri: Uri, bounds: Boolean = false, size: Int = 1): Bitmap? {
var bitmap: Bitmap? = null
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = bounds
inSampleSize = size
}

val stream = context.contentResolver.openInputStream(uri)
try {
bitmap = BitmapFactory.decodeStream(stream, null, options)
} catch (e: Exception) {
e.message
} finally {
stream?.close()
}

return bitmap
}

private fun rotateBitmap(orientation: Int, bitmap: Bitmap?): Bitmap? = when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
else -> bitmap
}

private fun rotateImage(bitmap: Bitmap?, angle: Float): Bitmap? {
val matrix = Matrix().apply { postRotate(angle) }
return bitmap?.let {
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
}
Comment on lines +54 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지 회전은 왜 필요한건가욤??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

글고 궁금한게 리사이징 해도 이미지 해상도는 크게 변화 없나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@l2zh

이미지 회전은 왜 필요한건가욤??

이유는 단순합니다. 카메라로 사진 촬영 시, 기기내부 센서의 방향에 사진이 저장되는데 해당 사진 URI를 비트맵으로 전환하는 내부 메타데이터로 회전된 뱡향이 적용된 사진이 보여지게 됩니다.
그냥 똑바로 안찍어서 회전되어보입니다..ㅋㅋ
(ImageDecoder를 사용하면 상관없지만, BitmapFactory를 사용할 때 생기는 이슈입니다.)

글고 궁금한게 리사이징 해도 이미지 해상도는 크게 변화 없나요?

이미지의 해상도는 낮아질 것 같습니다. 하지만 크게 변화가 없다고 생각합니다. 이유는 압축 파일의 File Extension 을 JPEG로 사용하였기에 한 번의 압축은 이미지의 품질 저하를 많이 일으키지 않습니다.
하지만 이미지를 로드할 때, 큰 ImageView 에 작은 용량 사진을 넣으면 품질 저하가 나타날지도..? 라는 생각인데 요건 테스트를 진행해보려고 합니당.
(당연한 이야기 같기도..)
일단 제가 크게 파일의 용량을 줄인 이유는 우리가 진행하는 서비스의 사진은 작게 보이기 때문입니다! 🐬

이준희 시험 화이팅~ 🔥 🔥

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@l2zh
추가적으로 테스트해봤습니다.
기본적인 사진촬영과 카메라 사진은 1~3MB 합니다. 이에 대한 위 로직은 사진읜 품질저하를 일으키지 않습니다. 해상도를 1/4 비율로 낮추긴했지만, 크게 품질저하를 느끼지 못했습니다.

하지만 스크린샷이나 어디서 가져온 캡처본은 크기가 작습니다.
테스트한 123KB 캡처본으로 한 결과, 우리 서비스에서는 드라마틱한 품질 저하를 볼 수 없긴 합니다만 품질 저하는 확실히 있습니다.

BitmapFactory.Options().apply { 
  inSampleSize = 4 
}

// 변경한 부분
inSampleSize = 2

로 하면서 해상도의 비율을 1/4 로 줄였지만, 1/2로 줄여도 괜찮을 것 같아 수정했습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 그렇군요! 감사합니다~
형도 시험 화이팅...ㅎㅎ


/**
* Use BitmapFactory Bitmap Resize
*/
fun createUriToBitmap(uri: Uri, bounds: Boolean = false, size: Int = 1): Bitmap? {
val orientation = loadOrientation(uri)
val bitmap = decodeUriToBitmap(uri, bounds, size)

return rotateBitmap(orientation, bitmap)
}

/**
* Use ImageDecoder Bitmap Resize, easy convert uri to bitmap
*/
fun createUriToBitmap(uri: Uri): Bitmap =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source =
ImageDecoder.createSource(context.contentResolver, uri)
ImageDecoder.decodeBitmap(source)
} else {
MediaStore.Images.Media.getBitmap(
context.contentResolver,
uri
)
}
}
16 changes: 0 additions & 16 deletions app/src/main/java/sopt/motivoo/util/extension/ContextExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,9 @@ import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView
Expand Down Expand Up @@ -92,18 +88,6 @@ fun Context.sendNotification(
}.build()
}

fun Context.createUriToBitmap(photoUri: Uri): Bitmap =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source =
ImageDecoder.createSource(contentResolver, photoUri)
ImageDecoder.decodeBitmap(source)
} else {
MediaStore.Images.Media.getBitmap(
contentResolver,
photoUri
)
}

fun Context.checkNetworkState(): Boolean {
val connectivityManager =
this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
Expand Down
Loading