package com.rosetta.messenger.utils import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import android.net.Uri import android.util.Base64 import androidx.exifinterface.media.ExifInterface import com.vanniktech.blurhash.BlurHash import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream private const val TAG = "MediaUtils" /** * Утилиты для работы с медиафайлами * Конвертация изображений в Base64, генерация blurhash */ object MediaUtils { // Максимальный размер изображения для отправки (сжимаем большие изображения) private const val MAX_IMAGE_SIZE = 1920 private const val IMAGE_QUALITY = 85 // Максимальный размер файла в МБ // Android ограничение: файл + base64 + шифрование = ~3x памяти // 20 МБ файл = ~60 МБ RAM, безопасно для большинства устройств const val MAX_FILE_SIZE_MB = 20 /** * Конвертировать изображение из Uri в Base64 PNG * Автоматически сжимает большие изображения и учитывает EXIF ориентацию */ suspend fun uriToBase64Image(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) { try { // Читаем EXIF ориентацию val orientation = getExifOrientation(context, uri) val boundsOptions = BitmapFactory.Options().apply { inJustDecodeBounds = true } context.contentResolver.openInputStream(uri)?.use { inputStream -> BitmapFactory.decodeStream(inputStream, null, boundsOptions) } ?: return@withContext null if (boundsOptions.outWidth <= 0 || boundsOptions.outHeight <= 0) { return@withContext null } val decodeOptions = BitmapFactory.Options().apply { inSampleSize = calculateInSampleSize( boundsOptions.outWidth, boundsOptions.outHeight, MAX_IMAGE_SIZE * 2 ) inPreferredConfig = Bitmap.Config.ARGB_8888 } var bitmap = context.contentResolver.openInputStream(uri)?.use { inputStream -> BitmapFactory.decodeStream(inputStream, null, decodeOptions) } ?: return@withContext null // Применяем EXIF ориентацию (поворот/отражение) bitmap = applyExifOrientation(bitmap, orientation) // Масштабируем если слишком большое val scaledBitmap = scaleDownBitmap(bitmap, MAX_IMAGE_SIZE) if (scaledBitmap != bitmap) { bitmap.recycle() } // Конвертируем в PNG Base64 val outputStream = ByteArrayOutputStream() scaledBitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream) val bytes = outputStream.toByteArray() val base64 = "data:image/png;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP) scaledBitmap.recycle() base64 } catch (e: Exception) { null } catch (e: OutOfMemoryError) { null } } /** * Читает EXIF ориентацию из изображения */ private fun getExifOrientation(context: Context, uri: Uri): Int { return try { context.contentResolver.openInputStream(uri)?.use { inputStream -> val exif = ExifInterface(inputStream) exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) } ?: ExifInterface.ORIENTATION_NORMAL } catch (e: Exception) { ExifInterface.ORIENTATION_NORMAL } } /** * Применяет EXIF ориентацию к Bitmap (поворот/отражение) */ private fun applyExifOrientation(bitmap: Bitmap, orientation: Int): Bitmap { val matrix = Matrix() when (orientation) { ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f) ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f) ExifInterface.ORIENTATION_TRANSPOSE -> { matrix.postRotate(90f) matrix.preScale(-1f, 1f) } ExifInterface.ORIENTATION_TRANSVERSE -> { matrix.postRotate(270f) matrix.preScale(-1f, 1f) } else -> return bitmap // ORIENTATION_NORMAL или неизвестный } return try { val rotatedBitmap = Bitmap.createBitmap( bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true ) if (rotatedBitmap != bitmap) { bitmap.recycle() } rotatedBitmap } catch (e: Exception) { bitmap } } /** * Генерировать Blurhash для изображения */ suspend fun generateBlurhash(context: Context, uri: Uri): String = withContext(Dispatchers.IO) { try { val inputStream = context.contentResolver.openInputStream(uri) ?: return@withContext "" // Декодируем в маленький размер для blurhash val options = BitmapFactory.Options().apply { inSampleSize = 8 // Уменьшаем в 8 раз для быстрого расчета } val bitmap = BitmapFactory.decodeStream(inputStream, null, options) inputStream.close() if (bitmap == null) { return@withContext "" } // Генерируем blurhash val blurhash = BlurHash.encode(bitmap, 4, 3) bitmap.recycle() blurhash ?: "" } catch (e: Exception) { "" } } /** * Конвертировать файл из Uri в Base64 */ suspend fun uriToBase64File(context: Context, uri: Uri): String? = withContext(Dispatchers.IO) { try { val inputStream = context.contentResolver.openInputStream(uri) ?: return@withContext null val bytes = inputStream.readBytes() inputStream.close() val mimeType = context.contentResolver.getType(uri) ?: "application/octet-stream" val base64 = "data:$mimeType;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP) base64 } catch (e: Exception) { null } } /** * Получить размер файла из Uri */ fun getFileSize(context: Context, uri: Uri): Long { return try { context.contentResolver.openInputStream(uri)?.use { it.available().toLong() } ?: 0L } catch (e: Exception) { 0L } } /** * Получить имя файла из Uri */ fun getFileName(context: Context, uri: Uri): String { var name = "file" try { context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> val nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) if (nameIndex != -1 && cursor.moveToFirst()) { name = cursor.getString(nameIndex) ?: "file" } } } catch (e: Exception) { } return name } /** * Масштабировать bitmap до максимального размера */ private fun scaleDownBitmap(bitmap: Bitmap, maxSize: Int): Bitmap { val width = bitmap.width val height = bitmap.height if (width <= maxSize && height <= maxSize) { return bitmap } val ratio = width.toFloat() / height.toFloat() val newWidth: Int val newHeight: Int if (width > height) { newWidth = maxSize newHeight = (maxSize / ratio).toInt() } else { newHeight = maxSize newWidth = (maxSize * ratio).toInt() } return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true) } private fun calculateInSampleSize(width: Int, height: Int, maxDimension: Int): Int { var sample = 1 while ((width / (sample * 2)) >= maxDimension || (height / (sample * 2)) >= maxDimension) { sample *= 2 } return sample.coerceAtLeast(1) } /** * Конвертировать Bitmap в Base64 PNG */ fun bitmapToBase64(bitmap: Bitmap): String { val outputStream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, IMAGE_QUALITY, outputStream) val bytes = outputStream.toByteArray() return "data:image/png;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP) } /** * Генерировать Blurhash из Bitmap */ fun generateBlurhashFromBitmap(bitmap: Bitmap): String { return try { // Уменьшаем для быстрого расчета val scaledBitmap = Bitmap.createScaledBitmap(bitmap, 32, 32, true) val hash = BlurHash.encode(scaledBitmap, 4, 3) if (scaledBitmap != bitmap) { scaledBitmap.recycle() } hash ?: "" } catch (e: Exception) { "" } } /** * Получить размеры изображения из Uri без полной загрузки в память * Учитывает EXIF ориентацию для правильных width/height */ fun getImageDimensions(context: Context, uri: Uri): Pair { return try { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true // Не загружаем в память, только размеры } context.contentResolver.openInputStream(uri)?.use { inputStream -> BitmapFactory.decodeStream(inputStream, null, options) } var width = options.outWidth var height = options.outHeight // Учитываем EXIF ориентацию - если поворот 90 или 270, меняем местами width/height val orientation = getExifOrientation(context, uri) if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270 || orientation == ExifInterface.ORIENTATION_TRANSPOSE || orientation == ExifInterface.ORIENTATION_TRANSVERSE) { val temp = width width = height height = temp } // Учитываем масштабирование (как в uriToBase64Image) if (width > MAX_IMAGE_SIZE || height > MAX_IMAGE_SIZE) { val ratio = width.toFloat() / height.toFloat() if (width > height) { width = MAX_IMAGE_SIZE height = (MAX_IMAGE_SIZE / ratio).toInt() } else { height = MAX_IMAGE_SIZE width = (MAX_IMAGE_SIZE * ratio).toInt() } } Pair(width, height) } catch (e: Exception) { Pair(0, 0) } } /** * Получить размеры изображения из Base64 строки */ fun getImageDimensionsFromBase64(base64: String): Pair { return try { // Убираем data URI prefix если есть val base64Data = if (base64.contains(",")) { base64.substringAfter(",") } else { base64 } val bytes = Base64.decode(base64Data, Base64.DEFAULT) val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options) Pair(options.outWidth, options.outHeight) } catch (e: Exception) { Pair(0, 0) } } }