From 89d395178c0ef3318751896efef387238e92329e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 22 Oct 2024 13:13:31 +0300 Subject: [PATCH] Support for AVIF images (cherry picked from commit c15a0ece3ea5287bccef2be4d179cc201d841a54) --- app/build.gradle | 9 +- .../org/koitharu/kotatsu/core/AppModule.kt | 4 + .../kotatsu/core/image/AvifImageDecoder.kt | 67 ++++++++++++ .../kotatsu/core/image/BaseCoilDecoder.kt | 50 +++++++++ .../kotatsu/core/image/BitmapDecoderCompat.kt | 77 +++++++++++++ .../{ui => }/image/RegionBitmapDecoder.kt | 102 +++++++----------- .../koitharu/kotatsu/core/util/ext/Coil.kt | 4 +- .../koitharu/kotatsu/core/util/ext/File.kt | 9 ++ .../org/koitharu/kotatsu/core/util/ext/IO.kt | 10 ++ .../kotatsu/reader/domain/PageLoader.kt | 20 ++-- 10 files changed, 268 insertions(+), 84 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt rename app/src/main/kotlin/org/koitharu/kotatsu/core/{ui => }/image/RegionBitmapDecoder.kt (73%) diff --git a/app/build.gradle b/app/build.gradle index 5613f92c1..24a2e576e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 677 - versionName = '7.6.4' + versionCode = 678 + versionName = '7.6.5' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -92,7 +92,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.core:core-ktx:1.13.1' - implementation 'androidx.activity:activity-ktx:1.9.2' + implementation 'androidx.activity:activity-ktx:1.9.3' implementation 'androidx.fragment:fragment-ktx:1.8.4' implementation 'androidx.transition:transition-ktx:1.5.1' implementation 'androidx.collection:collection-ktx:1.4.4' @@ -136,7 +136,8 @@ dependencies { implementation 'io.coil-kt:coil-base:2.7.0' implementation 'io.coil-kt:coil-svg:2.7.0' - implementation 'com.github.KotatsuApp:subsampling-scale-image-view:ac7360c5e3' + implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:d1d10a6975' implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'io.noties.markwon:core:4.6.2' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index cf47f00b2..cd1a77f51 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -27,6 +27,8 @@ import okhttp3.OkHttpClient import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.image.AvifImageDecoder +import org.koitharu.kotatsu.core.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.os.AppShortcutManager @@ -119,6 +121,8 @@ interface AppModule { ComponentRegistry.Builder() .add(SvgDecoder.Factory()) .add(CbzFetcher.Factory()) + .add(AvifImageDecoder.Factory()) + .add(RegionBitmapDecoder.Factory()) .add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory)) .add(MangaPageKeyer()) .add(pageFetcherFactory) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt new file mode 100644 index 000000000..9671baa73 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/AvifImageDecoder.kt @@ -0,0 +1,67 @@ +package org.koitharu.kotatsu.core.image + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.core.graphics.drawable.toDrawable +import coil.ImageLoader +import coil.decode.DecodeResult +import coil.decode.Decoder +import coil.decode.ImageSource +import coil.fetch.SourceResult +import coil.request.Options +import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException +import kotlinx.coroutines.sync.Semaphore +import org.aomedia.avif.android.AvifDecoder +import org.aomedia.avif.android.AvifDecoder.Info +import org.koitharu.kotatsu.core.util.ext.toByteBuffer + +class AvifImageDecoder(source: ImageSource, options: Options, parallelismLock: Semaphore) : + BaseCoilDecoder(source, options, parallelismLock) { + + override fun BitmapFactory.Options.decode(): DecodeResult { + val bytes = source.source().use { + it.inputStream().toByteBuffer() + } + val info = Info() + if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) { + throw ImageDecodeException( + null, + "avif", + "Requested to decode byte buffer which cannot be handled by AvifDecoder", + ) + } + val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 + val bitmap = Bitmap.createBitmap(info.width, info.height, config) + if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { + bitmap.recycle() + throw ImageDecodeException(null, "avif") + } + return DecodeResult( + drawable = bitmap.toDrawable(options.context.resources), + isSampled = false, + ) + } + + class Factory : Decoder.Factory { + + private val parallelismLock = Semaphore(DEFAULT_PARALLELISM) + + override fun create( + result: SourceResult, + options: Options, + imageLoader: ImageLoader + ): Decoder? = if (isApplicable(result)) { + AvifImageDecoder(result.source, options, parallelismLock) + } else { + null + } + + override fun equals(other: Any?) = other is Factory + + override fun hashCode() = javaClass.hashCode() + + private fun isApplicable(result: SourceResult): Boolean { + return result.mimeType == "image/avif" + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt new file mode 100644 index 000000000..fc621ad49 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BaseCoilDecoder.kt @@ -0,0 +1,50 @@ +package org.koitharu.kotatsu.core.image + +import android.graphics.BitmapFactory +import coil.decode.DecodeResult +import coil.decode.Decoder +import coil.decode.ImageSource +import coil.request.Options +import coil.size.Dimension +import coil.size.Scale +import coil.size.Size +import coil.size.isOriginal +import coil.size.pxOrElse +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import org.jetbrains.annotations.Blocking + +abstract class BaseCoilDecoder( + protected val source: ImageSource, + protected val options: Options, + private val parallelismLock: Semaphore, +) : Decoder { + + final override suspend fun decode(): DecodeResult = parallelismLock.withPermit { + runInterruptible { BitmapFactory.Options().decode() } + } + + @Blocking + protected abstract fun BitmapFactory.Options.decode(): DecodeResult + + protected companion object { + + const val DEFAULT_PARALLELISM = 4 + + inline fun Size.widthPx(scale: Scale, original: () -> Int): Int { + return if (isOriginal) original() else width.toPx(scale) + } + + inline fun Size.heightPx(scale: Scale, original: () -> Int): Int { + return if (isOriginal) original() else height.toPx(scale) + } + + fun Dimension.toPx(scale: Scale) = pxOrElse { + when (scale) { + Scale.FILL -> Int.MIN_VALUE + Scale.FIT -> Int.MAX_VALUE + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt new file mode 100644 index 000000000..80e9a4bdb --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/BitmapDecoderCompat.kt @@ -0,0 +1,77 @@ +package org.koitharu.kotatsu.core.image + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageDecoder +import android.os.Build +import android.webkit.MimeTypeMap +import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import org.aomedia.avif.android.AvifDecoder +import org.aomedia.avif.android.AvifDecoder.Info +import org.jetbrains.annotations.Blocking +import org.koitharu.kotatsu.core.util.ext.toByteBuffer +import java.io.File +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.file.Files + +object BitmapDecoderCompat { + + private const val FORMAT_AVIF = "avif" + + @Blocking + fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) { + FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) } + else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(file)) + } else { + checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath), format) + } + } + + @Blocking + fun decode(stream: InputStream, type: MediaType?): Bitmap { + val format = type?.subtype + if (format == FORMAT_AVIF) { + return decodeAvif(stream.toByteBuffer()) + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return checkBitmapNotNull(BitmapFactory.decodeStream(stream), format) + } + val byteBuffer = stream.toByteBuffer() + return if (AvifDecoder.isAvifImage(byteBuffer)) { + decodeAvif(byteBuffer) + } else { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(byteBuffer)) + } + } + + private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Files.probeContentType(file.toPath())?.toMediaTypeOrNull() + } else { + MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull() + } + + private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap = + bitmap ?: throw ImageDecodeException(null, format) + + private fun decodeAvif(bytes: ByteBuffer): Bitmap { + val info = Info() + if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) { + throw ImageDecodeException( + null, + FORMAT_AVIF, + "Requested to decode byte buffer which cannot be handled by AvifDecoder", + ) + } + val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 + val bitmap = Bitmap.createBitmap(info.width, info.height, config) + if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { + bitmap.recycle() + throw ImageDecodeException(null, FORMAT_AVIF) + } + return bitmap + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt similarity index 73% rename from app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt index 47d5461cb..d55c145a0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/RegionBitmapDecoder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/image/RegionBitmapDecoder.kt @@ -1,4 +1,4 @@ -package org.koitharu.kotatsu.core.ui.image +package org.koitharu.kotatsu.core.image import android.graphics.Bitmap import android.graphics.BitmapFactory @@ -13,27 +13,14 @@ import coil.decode.Decoder import coil.decode.ImageSource import coil.fetch.SourceResult import coil.request.Options -import coil.size.Dimension -import coil.size.Scale -import coil.size.Size -import coil.size.isOriginal -import coil.size.pxOrElse -import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit import kotlin.math.roundToInt class RegionBitmapDecoder( - private val source: ImageSource, - private val options: Options, - private val parallelismLock: Semaphore, -) : Decoder { + source: ImageSource, options: Options, parallelismLock: Semaphore +) : BaseCoilDecoder(source, options, parallelismLock) { - override suspend fun decode() = parallelismLock.withPermit { - runInterruptible { BitmapFactory.Options().decode() } - } - - private fun BitmapFactory.Options.decode(): DecodeResult { + override fun BitmapFactory.Options.decode(): DecodeResult { val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { BitmapRegionDecoder.newInstance(source.source().inputStream()) } else { @@ -55,29 +42,6 @@ class RegionBitmapDecoder( } } - private fun BitmapFactory.Options.configureConfig() { - var config = options.config - - inMutable = false - - if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) { - inPreferredColorSpace = options.colorSpace - } - inPremultiplied = options.premultipliedAlpha - - // Decode the image as RGB_565 as an optimization if allowed. - if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") { - config = Bitmap.Config.RGB_565 - } - - // High color depth images must be decoded as either RGBA_F16 or HARDWARE. - if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) { - config = Bitmap.Config.RGBA_F16 - } - - inPreferredConfig = config - } - /** Compute and set the scaling properties for [BitmapFactory.Options]. */ private fun BitmapFactory.Options.configureScale(srcWidth: Int, srcHeight: Int): Rect { val dstWidth = options.size.widthPx(options.scale) { srcWidth } @@ -142,18 +106,41 @@ class RegionBitmapDecoder( return rect } - class Factory( - maxParallelism: Int = DEFAULT_MAX_PARALLELISM, - ) : Decoder.Factory { + private fun BitmapFactory.Options.configureConfig() { + var config = options.config - @Suppress("NEWER_VERSION_IN_SINCE_KOTLIN") - @SinceKotlin("999.9") // Only public in Java. - constructor() : this() + inMutable = false - private val parallelismLock = Semaphore(maxParallelism) + if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) { + inPreferredColorSpace = options.colorSpace + } + inPremultiplied = options.premultipliedAlpha - override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder { - return RegionBitmapDecoder(result.source, options, parallelismLock) + // Decode the image as RGB_565 as an optimization if allowed. + if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") { + config = Bitmap.Config.RGB_565 + } + + // High color depth images must be decoded as either RGBA_F16 or HARDWARE. + if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) { + config = Bitmap.Config.RGBA_F16 + } + + inPreferredConfig = config + } + + class Factory : Decoder.Factory { + + private val parallelismLock = Semaphore(DEFAULT_PARALLELISM) + + override fun create( + result: SourceResult, + options: Options, + imageLoader: ImageLoader + ): Decoder? = if (options.parameters.value(PARAM_REGION) == true) { + RegionBitmapDecoder(result.source, options, parallelismLock) + } else { + null } override fun equals(other: Any?) = other is Factory @@ -164,22 +151,7 @@ class RegionBitmapDecoder( companion object { const val PARAM_SCROLL = "scroll" + const val PARAM_REGION = "region" const val SCROLL_UNDEFINED = -1 - private const val DEFAULT_MAX_PARALLELISM = 4 - - private inline fun Size.widthPx(scale: Scale, original: () -> Int): Int { - return if (isOriginal) original() else width.toPx(scale) - } - - private inline fun Size.heightPx(scale: Scale, original: () -> Int): Int { - return if (isOriginal) original() else height.toPx(scale) - } - - private fun Dimension.toPx(scale: Scale) = pxOrElse { - when (scale) { - Scale.FILL -> Int.MIN_VALUE - Scale.FIT -> Int.MAX_VALUE - } - } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt index 580b5aeed..19ffb7481 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt @@ -14,8 +14,8 @@ import coil.request.SuccessResult import coil.util.CoilUtils import com.google.android.material.progressindicator.BaseProgressIndicator import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable -import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener import org.koitharu.kotatsu.parsers.model.MangaSource import com.google.android.material.R as materialR @@ -63,7 +63,7 @@ fun ImageRequest.Builder.indicator(indicators: List>): fun ImageRequest.Builder.decodeRegion( scroll: Int = RegionBitmapDecoder.SCROLL_UNDEFINED, -): ImageRequest.Builder = decoderFactory(RegionBitmapDecoder.Factory()) +): ImageRequest.Builder = setParameter(RegionBitmapDecoder.PARAM_REGION, true) .setParameter(RegionBitmapDecoder.PARAM_SCROLL, scroll) @Suppress("SpellCheckingInspection") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index a653d40c9..ca7840a9b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -7,9 +7,12 @@ import android.os.Build import android.os.Environment import android.os.storage.StorageManager import android.provider.OpenableColumns +import android.webkit.MimeTypeMap import androidx.core.database.getStringOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.fs.FileSequence @@ -38,6 +41,12 @@ fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).use { output -> output.bufferedReader().use(BufferedReader::readText) } +val ZipEntry.mimeType: MediaType? + get() { + val ext = name.substringAfterLast('.') + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)?.toMediaTypeOrNull() + } + fun File.getStorageName(context: Context): String = runCatching { val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt index d41e0ba38..41cf24f06 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/IO.kt @@ -10,6 +10,9 @@ import okio.BufferedSink import okio.Source import org.koitharu.kotatsu.core.util.CancellableSource import org.koitharu.kotatsu.core.util.progress.ProgressResponseBody +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.nio.ByteBuffer fun ResponseBody.withProgress(progressState: MutableStateFlow): ResponseBody { return ProgressResponseBody(this, progressState) @@ -23,3 +26,10 @@ suspend fun Source.cancellable(): Source { suspend fun BufferedSink.writeAllCancellable(source: Source) = withContext(Dispatchers.IO) { writeAll(source.cancellable()) } + +fun InputStream.toByteBuffer(): ByteBuffer { + val outStream = ByteArrayOutputStream(available()) + copyTo(outStream) + val bytes = outStream.toByteArray() + return ByteBuffer.allocateDirect(bytes.size).put(bytes).position(0) as ByteBuffer +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 00cbb7ad4..8406ff161 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -1,12 +1,10 @@ package org.koitharu.kotatsu.reader.domain +import android.content.ContentResolver.MimeTypeInfo import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.ImageDecoder import android.graphics.Rect import android.net.Uri -import android.os.Build +import android.webkit.MimeTypeMap import androidx.annotation.AnyThread import androidx.collection.LongSparseArray import androidx.collection.set @@ -61,6 +59,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.core.image.BitmapDecoderCompat +import org.koitharu.kotatsu.core.util.ext.mimeType import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import java.util.LinkedList import java.util.concurrent.atomic.AtomicInteger @@ -144,8 +144,8 @@ class PageLoader @Inject constructor( ZipFile(uri.schemeSpecificPart).use { zip -> val entry = zip.getEntry(uri.fragment) context.ensureRamAtLeast(entry.size * 2) - zip.getInputStream(zip.getEntry(uri.fragment)).use { - checkBitmapNotNull(BitmapFactory.decodeStream(it)) + zip.getInputStream(entry).use { + BitmapDecoderCompat.decode(it, entry.mimeType) } } } @@ -154,11 +154,7 @@ class PageLoader @Inject constructor( val file = uri.toFile() runInterruptible(Dispatchers.IO) { context.ensureRamAtLeast(file.length() * 2) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoder.decodeBitmap(ImageDecoder.createSource(file)) - } else { - checkBitmapNotNull(BitmapFactory.decodeFile(file.absolutePath)) - } + BitmapDecoderCompat.decode(file) }.use { image -> image.compressToPNG(file) } @@ -253,8 +249,6 @@ class PageLoader @Inject constructor( return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES) } - private fun checkBitmapNotNull(bitmap: Bitmap?): Bitmap = checkNotNull(bitmap) { "Cannot decode bitmap" } - private fun Deferred.isValid(): Boolean { return getCompletionResultOrNull()?.map { uri -> uri.exists() && uri.isTargetNotEmpty()