package com.suno.android.common_data.use_case import android.content.ContentResolver import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import android.net.Uri import android.util.Base64 import androidx.core.graphics.scale import androidx.exifinterface.media.ExifInterface import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_data.entities.ImageUploadConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.io.InputStream import javax.inject.Inject import kotlin.math.min private const val DOWNSCALE_THRESHOLD = 0.5f private const val UPSCALE_THRESHOLD = 1.0f class UriToBase64DataUseCase @Inject constructor( loggerFactory: SunoLogger.Factory, private val contentResolver: ContentResolver, ) { private val logger = loggerFactory.create(this@UriToBase64DataUseCase) suspend operator fun invoke( uri: Uri, config: ImageUploadConfig? = null, ): String? = withContext(Dispatchers.IO) { try { val inputStream: InputStream? = contentResolver.openInputStream(uri) var bitmap = BitmapFactory.decodeStream(inputStream) inputStream?.close() // Flag for image constraints, crop and reduce quality if needed bitmap = config?.let { processImageWithConfig(bitmap, config) } ?: bitmap // Rotate image if photo is using EXIF orientation data bitmap = handleImageOrientation(uri, bitmap) val outputStream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.JPEG, config?.quality ?: 80, outputStream) val byteArray = outputStream.toByteArray() bitmap.recycle() // Convert to base64 with data URL prefix val base64String = Base64.encodeToString(byteArray, Base64.NO_WRAP) "data:image/jpeg;base64,$base64String" } catch (e: Exception) { logger.e(e) null } } private fun processImageWithConfig( bitmap: Bitmap, config: ImageUploadConfig, ): Bitmap { // If crop dimensions are specified, scale to fit the crop dimensions return if (config.cropWidth != null && config.cropHeight != null) { val scaledBitmap = scaleToFitCropDimensions(bitmap, config.cropWidth, config.cropHeight) cropToCenter(scaledBitmap, config.cropWidth, config.cropHeight) } else { // Fallback: scale down if larger than max dimensions (for cover images without crop) val originalWidth = bitmap.width val originalHeight = bitmap.height if (originalWidth > config.maxWidth || originalHeight > config.maxHeight) { val scaleX = config.maxWidth.toFloat() / originalWidth val scaleY = config.maxHeight.toFloat() / originalHeight val scale = min(scaleX, scaleY) val newWidth = (originalWidth * scale).toInt() val newHeight = (originalHeight * scale).toInt() bitmap.scale(newWidth, newHeight) } else { bitmap } } } private fun scaleToFitCropDimensions( bitmap: Bitmap, cropWidth: Int, cropHeight: Int, ): Bitmap { val originalWidth = bitmap.width val originalHeight = bitmap.height // Scale to ensure the image is at least as large as the crop dimensions // Use the larger scale factor so we can crop from the center val scaleX = cropWidth.toFloat() / originalWidth val scaleY = cropHeight.toFloat() / originalHeight val scale = maxOf(scaleX, scaleY) // Only scale if we need to make it larger, or if it's much larger and needs to be scaled down return if (scale > UPSCALE_THRESHOLD || (scale < DOWNSCALE_THRESHOLD)) { val newWidth = (originalWidth * scale).toInt() val newHeight = (originalHeight * scale).toInt() bitmap.scale(newWidth, newHeight) } else { bitmap } } private fun cropToCenter( bitmap: Bitmap, targetWidth: Int, targetHeight: Int, ): Bitmap { val sourceWidth = bitmap.width val sourceHeight = bitmap.height // If already the right size, return as-is if (sourceWidth == targetWidth && sourceHeight == targetHeight) { return bitmap } // Calculate crop dimensions centered on the image val startX = maxOf(0, (sourceWidth - targetWidth) / 2) val startY = maxOf(0, (sourceHeight - targetHeight) / 2) val cropWidth = min(targetWidth, sourceWidth) val cropHeight = min(targetHeight, sourceHeight) return Bitmap.createBitmap(bitmap, startX, startY, cropWidth, cropHeight) } private fun handleImageOrientation( uri: Uri, bitmap: Bitmap, ): Bitmap { return try { val orientation = getImageOrientation(uri) if (orientation == 0) { return bitmap } val matrix = Matrix() matrix.postRotate(orientation.toFloat()) Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) } catch (e: Exception) { logger.e(e) { "Failed to handle image orientation for URI: $uri" } bitmap } } private fun getImageOrientation( uri: Uri, ): Int { return try { val inputStream = contentResolver.openInputStream(uri) ?: return 0 val exifInterface = ExifInterface(inputStream) inputStream.close() when (exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) { ExifInterface.ORIENTATION_ROTATE_90 -> 90 ExifInterface.ORIENTATION_ROTATE_180 -> 180 ExifInterface.ORIENTATION_ROTATE_270 -> 270 else -> 0 } } catch (e: Exception) { logger.e(e) { "Failed to get EXIF orientation for URI: $uri" } 0 } } }