diff --git a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/di/ImageModule.kt b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/di/ImageModule.kt index af1cbc0f20..aa0efc5d06 100644 --- a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/di/ImageModule.kt +++ b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/di/ImageModule.kt @@ -12,11 +12,13 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import ru.tech.imageresizershrinker.core.data.image.AndroidImageCompressor +import ru.tech.imageresizershrinker.core.data.image.AndroidImageGetter import ru.tech.imageresizershrinker.core.data.image.AndroidImageManager import ru.tech.imageresizershrinker.core.data.image.draw.AndroidImageDrawApplier import ru.tech.imageresizershrinker.core.data.image.filters.applier.AndroidFilterMaskApplier import ru.tech.imageresizershrinker.core.data.image.filters.provider.AndroidFilterProvider import ru.tech.imageresizershrinker.core.domain.image.ImageCompressor +import ru.tech.imageresizershrinker.core.domain.image.ImageGetter import ru.tech.imageresizershrinker.core.domain.image.ImageManager import ru.tech.imageresizershrinker.core.domain.image.draw.ImageDrawApplier import ru.tech.imageresizershrinker.core.domain.image.filters.FilterMaskApplier @@ -37,6 +39,7 @@ object ImageModule { imageLoader: ImageLoader, filterProvider: FilterProvider, imageCompressor: ImageCompressor, + imageGetter: ImageGetter, settingsRepository: SettingsRepository ): ImageManager = AndroidImageManager( context = context, @@ -44,9 +47,17 @@ object ImageModule { imageLoader = imageLoader, filterProvider = filterProvider, settingsRepository = settingsRepository, - imageCompressor = imageCompressor + imageCompressor = imageCompressor, + imageGetter = imageGetter ) + @Singleton + @Provides + fun provideImageGetter( + imageLoader: ImageLoader, + @ApplicationContext context: Context, + ): ImageGetter = AndroidImageGetter(imageLoader, context) + @Singleton @Provides fun provideImageCompressor( diff --git a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageGetter.kt b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageGetter.kt new file mode 100644 index 0000000000..dc031c9fda --- /dev/null +++ b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageGetter.kt @@ -0,0 +1,204 @@ +package ru.tech.imageresizershrinker.core.data.image + +import android.content.ContentResolver +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.webkit.MimeTypeMap +import androidx.core.net.toUri +import androidx.exifinterface.media.ExifInterface +import coil.ImageLoader +import coil.request.ImageRequest +import coil.size.Size +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import ru.tech.imageresizershrinker.core.domain.image.ImageGetter +import ru.tech.imageresizershrinker.core.domain.image.Transformation +import ru.tech.imageresizershrinker.core.domain.model.ImageData +import ru.tech.imageresizershrinker.core.domain.model.ImageFormat +import ru.tech.imageresizershrinker.core.domain.model.ImageInfo +import java.util.Locale +import javax.inject.Inject + +class AndroidImageGetter @Inject constructor( + private val imageLoader: ImageLoader, + @ApplicationContext private val context: Context +) : ImageGetter { + + override suspend fun getImage( + uri: String, + originalSize: Boolean + ): ImageData? = withContext(Dispatchers.IO) { + return@withContext kotlin.runCatching { + imageLoader.execute( + ImageRequest + .Builder(context) + .data(uri) + .apply { + if (originalSize) size(Size.ORIGINAL) + } + .build() + ).drawable?.toBitmap() + }.getOrNull()?.let { bitmap -> + val fd = context.contentResolver.openFileDescriptor(uri.toUri(), "r") + val exif = fd?.fileDescriptor?.let { ExifInterface(it) } + fd?.close() + ImageData( + image = bitmap, + imageInfo = ImageInfo( + width = bitmap.width, + height = bitmap.height, + imageFormat = ImageFormat[getExtension(uri)] + ), + metadata = exif + ) + } + } + + override suspend fun getImage(data: Any, originalSize: Boolean): Bitmap? { + return runCatching { + imageLoader.execute( + ImageRequest + .Builder(context) + .data(data) + .apply { + if (originalSize) size(Size.ORIGINAL) + } + .build() + ).drawable?.toBitmap() + }.getOrNull() + } + + override suspend fun getImageWithTransformations( + uri: String, + transformations: List>, + originalSize: Boolean + ): ImageData? = withContext(Dispatchers.IO) { + val request = ImageRequest + .Builder(context) + .data(uri) + .transformations( + transformations.map(::toCoil) + ) + .apply { + if (originalSize) size(Size.ORIGINAL) + } + .build() + return@withContext runCatching { + imageLoader.execute(request).drawable?.toBitmap()?.let { bitmap -> + val fd = context.contentResolver.openFileDescriptor(uri.toUri(), "r") + val exif = fd?.fileDescriptor?.let { ExifInterface(it) } + fd?.close() + ImageData( + image = bitmap, + imageInfo = ImageInfo( + width = bitmap.width, + height = bitmap.height, + imageFormat = ImageFormat[getExtension(uri)] + ), + metadata = exif + ) + } + }.getOrNull() + } + + override fun getImageAsync( + uri: String, + originalSize: Boolean, + onGetImage: (ImageData) -> Unit, + onError: (Throwable) -> Unit + ) { + val bmp = kotlin.runCatching { + imageLoader.enqueue( + ImageRequest + .Builder(context) + .data(uri) + .apply { + if (originalSize) size(Size.ORIGINAL) + } + .target { drawable -> + drawable.toBitmap().let { bitmap -> + val fd = context.contentResolver.openFileDescriptor(uri.toUri(), "r") + val exif = fd?.fileDescriptor?.let { ExifInterface(it) } + fd?.close() + ImageData( + image = bitmap, + imageInfo = ImageInfo( + width = bitmap.width, + height = bitmap.height, + imageFormat = ImageFormat[getExtension(uri)] + ), + metadata = exif + ) + }.let(onGetImage) + }.build() + ) + } + bmp.exceptionOrNull()?.let(onError) + } + + override fun getExtension(uri: String): String? { + if (uri.endsWith(".jxl")) return "jxl" + return if (ContentResolver.SCHEME_CONTENT == uri.toUri().scheme) { + MimeTypeMap.getSingleton() + .getExtensionFromMimeType( + context.contentResolver.getType(uri.toUri()) + ) + } else { + MimeTypeMap.getFileExtensionFromUrl(uri).lowercase(Locale.getDefault()) + } + } + + private fun Drawable.toBitmap(): Bitmap { + val drawable = this + if (drawable is BitmapDrawable) { + if (drawable.bitmap != null) { + return drawable.bitmap + } + } + val bitmap: Bitmap = if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) { + Bitmap.createBitmap( + 1, + 1, + getSuitableConfig() + ) // Single color bitmap will be created of 1x1 pixel + } else { + Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + getSuitableConfig() + ) + } + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap + } + + private fun getSuitableConfig( + image: Bitmap? = null + ): Bitmap.Config = image?.config ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Bitmap.Config.RGBA_1010102 + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Bitmap.Config.RGBA_F16 + } else { + Bitmap.Config.ARGB_8888 + } + + private fun toCoil(transformation: Transformation): coil.transform.Transformation { + return object : coil.transform.Transformation { + override val cacheKey: String + get() = transformation.cacheKey + + override suspend fun transform( + input: Bitmap, + size: Size + ): Bitmap = transformation.transform(input, size) + } + } + +} \ No newline at end of file diff --git a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageManager.kt b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageManager.kt index 4d7f05b23c..68bc9021cf 100644 --- a/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageManager.kt +++ b/core/data/src/main/java/ru/tech/imageresizershrinker/core/data/image/AndroidImageManager.kt @@ -1,7 +1,6 @@ package ru.tech.imageresizershrinker.core.data.image import android.annotation.TargetApi -import android.content.ContentResolver import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -37,6 +36,7 @@ import ru.tech.imageresizershrinker.core.data.image.filters.SideFadeFilter import ru.tech.imageresizershrinker.core.data.image.filters.StackBlurFilter import ru.tech.imageresizershrinker.core.domain.ImageScaleMode import ru.tech.imageresizershrinker.core.domain.image.ImageCompressor +import ru.tech.imageresizershrinker.core.domain.image.ImageGetter import ru.tech.imageresizershrinker.core.domain.image.ImageManager import ru.tech.imageresizershrinker.core.domain.image.Transformation import ru.tech.imageresizershrinker.core.domain.image.filters.FadeSide @@ -60,7 +60,6 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream -import java.util.Locale import java.util.UUID import javax.inject.Inject import kotlin.math.abs @@ -76,7 +75,8 @@ class AndroidImageManager @Inject constructor( private val imageLoader: ImageLoader, private val filterProvider: FilterProvider, private val imageCompressor: ImageCompressor, - private val settingsRepository: SettingsRepository + private val settingsRepository: SettingsRepository, + private val imageGetter: ImageGetter ) : ImageManager { override fun getFilterProvider(): FilterProvider = filterProvider @@ -152,46 +152,18 @@ class AndroidImageManager @Inject constructor( override suspend fun getImage( uri: String, originalSize: Boolean - ): ImageData? = withContext(Dispatchers.IO) { - return@withContext kotlin.runCatching { - imageLoader.execute( - ImageRequest - .Builder(context) - .data(uri) - .apply { - if (originalSize) size(Size.ORIGINAL) - } - .build() - ).drawable?.toBitmap() - }.getOrNull()?.let { bitmap -> - val fd = context.contentResolver.openFileDescriptor(uri.toUri(), "r") - val exif = fd?.fileDescriptor?.let { ExifInterface(it) } - fd?.close() - ImageData( - image = bitmap, - imageInfo = ImageInfo( - width = bitmap.width, - height = bitmap.height, - imageFormat = ImageFormat[getExtension(uri)] - ), - metadata = exif - ) - } - } + ): ImageData? = imageGetter.getImage( + uri = uri, + originalSize = originalSize + ) - override suspend fun getImage(data: Any, originalSize: Boolean): Bitmap? { - return runCatching { - imageLoader.execute( - ImageRequest - .Builder(context) - .data(data) - .apply { - if (originalSize) size(Size.ORIGINAL) - } - .build() - ).drawable?.toBitmap() - }.getOrNull() - } + override suspend fun getImage( + data: Any, + originalSize: Boolean + ): Bitmap? = imageGetter.getImage( + data = data, + originalSize = originalSize + ) override suspend fun createFilteredPreview( image: Bitmap, @@ -210,35 +182,12 @@ class AndroidImageManager @Inject constructor( originalSize: Boolean, onGetImage: (ImageData) -> Unit, onError: (Throwable) -> Unit - ) { - val bmp = kotlin.runCatching { - imageLoader.enqueue( - ImageRequest - .Builder(context) - .data(uri) - .apply { - if (originalSize) size(Size.ORIGINAL) - } - .target { drawable -> - drawable.toBitmap()?.let { it -> - val fd = context.contentResolver.openFileDescriptor(uri.toUri(), "r") - val exif = fd?.fileDescriptor?.let { ExifInterface(it) } - fd?.close() - ImageData( - image = it, - imageInfo = ImageInfo( - width = it.width, - height = it.height, - imageFormat = ImageFormat[getExtension(uri)] - ), - metadata = exif - ) - }?.let(onGetImage) - }.build() - ) - } - bmp.exceptionOrNull()?.let(onError) - } + ) = imageGetter.getImageAsync( + uri = uri, + originalSize = originalSize, + onGetImage = onGetImage, + onError = onError + ) override suspend fun resize( image: Bitmap, @@ -851,34 +800,11 @@ class AndroidImageManager @Inject constructor( uri: String, transformations: List>, originalSize: Boolean - ): ImageData? = withContext(Dispatchers.IO) { - val request = ImageRequest - .Builder(context) - .data(uri) - .transformations( - transformations.map(::toCoil) - ) - .apply { - if (originalSize) size(Size.ORIGINAL) - } - .build() - return@withContext runCatching { - imageLoader.execute(request).drawable?.toBitmap()?.let { bitmap -> - val fd = context.contentResolver.openFileDescriptor(uri.toUri(), "r") - val exif = fd?.fileDescriptor?.let { ExifInterface(it) } - fd?.close() - ImageData( - image = bitmap, - imageInfo = ImageInfo( - width = bitmap.width, - height = bitmap.height, - imageFormat = ImageFormat[getExtension(uri)] - ), - metadata = exif - ) - } - }.getOrNull() - } + ): ImageData? = imageGetter.getImageWithTransformations( + uri = uri, + transformations = transformations, + originalSize = originalSize + ) override suspend fun scaleByMaxBytes( image: Bitmap, @@ -1293,7 +1219,7 @@ class AndroidImageManager @Inject constructor( } } - private fun Drawable.toBitmap(): Bitmap? { + private fun Drawable.toBitmap(): Bitmap { val drawable = this if (drawable is BitmapDrawable) { if (drawable.bitmap != null) { @@ -1319,17 +1245,7 @@ class AndroidImageManager @Inject constructor( return bitmap } - private fun getExtension(uri: String): String? { - if (uri.endsWith(".jxl")) return "jxl" - return if (ContentResolver.SCHEME_CONTENT == uri.toUri().scheme) { - MimeTypeMap.getSingleton() - .getExtensionFromMimeType( - context.contentResolver.getType(uri.toUri()) - ) - } else { - MimeTypeMap.getFileExtensionFromUrl(uri).lowercase(Locale.getDefault()) - } - } + private fun getExtension(uri: String): String? = imageGetter.getExtension(uri) private suspend fun ResizeType.CenterCrop.resizeWithCenterCrop( image: Bitmap, diff --git a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ImageGetter.kt b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ImageGetter.kt new file mode 100644 index 0000000000..debbf16766 --- /dev/null +++ b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ImageGetter.kt @@ -0,0 +1,31 @@ +package ru.tech.imageresizershrinker.core.domain.image + +import ru.tech.imageresizershrinker.core.domain.model.ImageData + +interface ImageGetter { + + suspend fun getImage( + uri: String, + originalSize: Boolean = true + ): ImageData? + + fun getImageAsync( + uri: String, + originalSize: Boolean = true, + onGetImage: (ImageData) -> Unit, + onError: (Throwable) -> Unit + ) + + suspend fun getImageWithTransformations( + uri: String, + transformations: List>, + originalSize: Boolean = true + ): ImageData? + + suspend fun getImage(data: Any, originalSize: Boolean = true): I? + + fun getExtension( + uri: String + ): String? + +} \ No newline at end of file diff --git a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ImageManager.kt b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ImageManager.kt index d2641c5313..4bd2e548cd 100644 --- a/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ImageManager.kt +++ b/core/domain/src/main/kotlin/ru/tech/imageresizershrinker/core/domain/image/ImageManager.kt @@ -41,11 +41,6 @@ interface ImageManager { size: IntegerSize ): I? - suspend fun getImage( - uri: String, - originalSize: Boolean = true - ): ImageData? - fun rotate(image: I, degrees: Float): I fun flip(image: I, isFlipped: Boolean): I @@ -72,13 +67,6 @@ interface ImageManager { onGetByteCount: (Int) -> Unit ): I - fun getImageAsync( - uri: String, - originalSize: Boolean = true, - onGetImage: (ImageData) -> Unit, - onError: (Throwable) -> Unit - ) - suspend fun compress( imageData: ImageData, onImageReadyToCompressInterceptor: suspend (I) -> I = { it }, @@ -110,6 +98,18 @@ interface ImageManager { maxBytes: Long ): ImageData? + suspend fun getImage( + uri: String, + originalSize: Boolean = true + ): ImageData? + + fun getImageAsync( + uri: String, + originalSize: Boolean = true, + onGetImage: (ImageData) -> Unit, + onError: (Throwable) -> Unit + ) + suspend fun getImageWithTransformations( uri: String, transformations: List>, @@ -122,6 +122,8 @@ interface ImageManager { originalSize: Boolean = true ): ImageData? + suspend fun getImage(data: Any, originalSize: Boolean = true): I? + suspend fun shareImage( imageData: ImageData, onComplete: () -> Unit, @@ -146,8 +148,6 @@ interface ImageManager { onComplete: () -> Unit ) - suspend fun getImage(data: Any, originalSize: Boolean = true): I? - fun removeBackgroundFromImage( image: I, onSuccess: (I) -> Unit, diff --git a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/viewModel/MainViewModel.kt b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/viewModel/MainViewModel.kt index 947b4e9af6..6661f03d0b 100644 --- a/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/viewModel/MainViewModel.kt +++ b/feature/main/src/main/java/ru/tech/imageresizershrinker/feature/main/presentation/viewModel/MainViewModel.kt @@ -9,7 +9,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector -import androidx.exifinterface.media.ExifInterface import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import coil.ImageLoader @@ -26,7 +25,7 @@ import kotlinx.coroutines.withContext import org.w3c.dom.Element import ru.tech.imageresizershrinker.core.domain.APP_RELEASES import ru.tech.imageresizershrinker.core.domain.ImageScaleMode -import ru.tech.imageresizershrinker.core.domain.image.ImageManager +import ru.tech.imageresizershrinker.core.domain.image.ImageGetter import ru.tech.imageresizershrinker.core.domain.model.CopyToClipboardMode import ru.tech.imageresizershrinker.core.domain.model.FontFam import ru.tech.imageresizershrinker.core.domain.model.NightMode @@ -48,7 +47,7 @@ import javax.xml.parsers.DocumentBuilderFactory class MainViewModel @Inject constructor( getSettingsStateFlowUseCase: GetSettingsStateFlowUseCase, val imageLoader: ImageLoader, - private val imageManager: ImageManager, + private val imageGetter: ImageGetter, private val fileController: FileController, private val getSettingsStateUseCase: GetSettingsStateUseCase, private val settingsRepository: SettingsRepository @@ -493,7 +492,7 @@ class MainViewModel @Inject constructor( setThemeStyle(0) if (settingsState.isInvertThemeColors) toggleInvertColors() } else { - imageManager.getImage(data = emojiUri) + imageGetter.getImage(data = emojiUri) ?.extractPrimaryColor() ?.let { primary -> val colorTuple = ColorTuple(primary)