Fix avif image decoding

This commit is contained in:
Koitharu
2025-04-13 10:47:37 +03:00
parent 3725a6e58f
commit 31586cf48f
7 changed files with 49 additions and 21 deletions

View File

@@ -75,6 +75,8 @@ android {
'-opt-in=kotlin.contracts.ExperimentalContracts', '-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=coil3.annotation.ExperimentalCoilApi', '-opt-in=coil3.annotation.ExperimentalCoilApi',
'-opt-in=coil3.annotation.InternalCoilApi', '-opt-in=coil3.annotation.InternalCoilApi',
'-Xjspecify-annotations=strict',
'-Xtype-enhancement-improvements-strict-mode',
] ]
} }
room { room {

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.image package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asImage import coil3.asImage
import coil3.decode.DecodeResult import coil3.decode.DecodeResult
@@ -32,7 +33,7 @@ class AvifImageDecoder(
) )
} }
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 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) val bitmap = createBitmap(info.width, info.height, config)
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
bitmap.recycle() bitmap.recycle()
throw ImageDecodeException(null, "avif") throw ImageDecodeException(null, "avif")

View File

@@ -2,15 +2,19 @@ package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.graphics.createBitmap
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import okio.IOException
import org.aomedia.avif.android.AvifDecoder import org.aomedia.avif.android.AvifDecoder
import org.aomedia.avif.android.AvifDecoder.Info import org.aomedia.avif.android.AvifDecoder.Info
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.util.MimeTypes import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.MimeType import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toByteBuffer import org.koitharu.kotatsu.core.util.ext.toByteBuffer
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -51,6 +55,19 @@ object BitmapDecoderCompat {
} }
} }
@Blocking
fun createRegionDecoder(inoutStream: InputStream): BitmapRegionDecoder? = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(inoutStream)
} else {
@Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(inoutStream, false)
}
} catch (e: IOException) {
e.printStackTraceDebug()
null
}
@Blocking @Blocking
fun probeMimeType(file: File): MimeType? { fun probeMimeType(file: File): MimeType? {
return MimeTypes.probeMimeType(file) ?: detectBitmapType(file) return MimeTypes.probeMimeType(file) ?: detectBitmapType(file)
@@ -62,7 +79,7 @@ object BitmapDecoderCompat {
inJustDecodeBounds = true inJustDecodeBounds = true
} }
BitmapFactory.decodeFile(file.path, options)?.recycle() BitmapFactory.decodeFile(file.path, options)?.recycle()
return options.outMimeType?.toMimeTypeOrNull() options.outMimeType?.toMimeTypeOrNull()
}.getOrNull() }.getOrNull()
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap = private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
@@ -78,7 +95,7 @@ object BitmapDecoderCompat {
) )
} }
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565 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) val bitmap = createBitmap(info.width, info.height, config)
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) { if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
bitmap.recycle() bitmap.recycle()
throw ImageDecodeException(null, FORMAT_AVIF) throw ImageDecodeException(null, FORMAT_AVIF)

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect import android.graphics.Rect
import android.os.Build import android.os.Build
import coil3.Extras import coil3.Extras
@@ -11,7 +10,6 @@ import coil3.asImage
import coil3.decode.DecodeResult import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils import coil3.decode.DecodeUtils
import coil3.decode.Decoder import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.getExtra import coil3.getExtra
import coil3.request.Options import coil3.request.Options
@@ -25,24 +23,31 @@ import coil3.size.Scale
import coil3.size.Size import coil3.size.Size
import coil3.size.isOriginal import coil3.size.isOriginal
import coil3.size.pxOrElse import coil3.size.pxOrElse
import kotlinx.coroutines.runInterruptible
import kotlin.math.roundToInt import kotlin.math.roundToInt
class RegionBitmapDecoder( class RegionBitmapDecoder(
private val source: ImageSource, private val fetchResult: SourceFetchResult,
private val options: Options, private val options: Options,
private val imageLoader: ImageLoader,
) : Decoder { ) : Decoder {
override suspend fun decode(): DecodeResult = runInterruptible { override suspend fun decode(): DecodeResult? {
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val regionDecoder = BitmapDecoderCompat.createRegionDecoder(fetchResult.source.source().inputStream())
BitmapRegionDecoder.newInstance(source.source().inputStream()) if (regionDecoder == null) {
} else { val fallbackDecoder = imageLoader.components.newDecoder(
@Suppress("DEPRECATION") result = fetchResult,
BitmapRegionDecoder.newInstance(source.source().inputStream(), false) options = options,
imageLoader = imageLoader,
startIndex = 0,
)?.first
return if (fallbackDecoder == null || fallbackDecoder is RegionBitmapDecoder) {
null
} else {
fallbackDecoder.decode()
}
} }
checkNotNull(regionDecoder)
val bitmapOptions = BitmapFactory.Options() val bitmapOptions = BitmapFactory.Options()
try { return try {
val rect = bitmapOptions.configureScale(regionDecoder.width, regionDecoder.height) val rect = bitmapOptions.configureScale(regionDecoder.width, regionDecoder.height)
bitmapOptions.configureConfig() bitmapOptions.configureConfig()
val bitmap = regionDecoder.decodeRegion(rect, bitmapOptions) val bitmap = regionDecoder.decodeRegion(rect, bitmapOptions)
@@ -149,7 +154,7 @@ class RegionBitmapDecoder(
result: SourceFetchResult, result: SourceFetchResult,
options: Options, options: Options,
imageLoader: ImageLoader imageLoader: ImageLoader
): Decoder = RegionBitmapDecoder(result.source, options) ): Decoder = RegionBitmapDecoder(result, options, imageLoader)
override fun equals(other: Any?) = other is Factory override fun equals(other: Any?) = other is Factory

View File

@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.reader.domain
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.annotation.CheckResult
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
import androidx.core.net.toFile import androidx.core.net.toFile
@@ -30,7 +32,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.use import okio.use
@@ -184,9 +185,10 @@ class PageLoader @Inject constructor(
return loadPageAsync(page, force).await() return loadPageAsync(page, force).await()
} }
@CheckResult
suspend fun convertBimap(uri: Uri): Uri = convertLock.withLock { suspend fun convertBimap(uri: Uri): Uri = convertLock.withLock {
if (uri.isZipUri()) { if (uri.isZipUri()) {
val bitmap = runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
ZipFile(uri.schemeSpecificPart).use { zip -> ZipFile(uri.schemeSpecificPart).use { zip ->
val entry = zip.getEntry(uri.fragment) val entry = zip.getEntry(uri.fragment)
context.ensureRamAtLeast(entry.size * 2) context.ensureRamAtLeast(entry.size * 2)
@@ -194,8 +196,9 @@ class PageLoader @Inject constructor(
BitmapDecoderCompat.decode(it, MimeTypes.getMimeTypeFromExtension(entry.name)) BitmapDecoderCompat.decode(it, MimeTypes.getMimeTypeFromExtension(entry.name))
} }
} }
}.use { image ->
cache.put(uri.toString(), image).toUri()
} }
cache.put(uri.toString(), bitmap).toUri()
} else { } else {
val file = uri.toFile() val file = uri.toFile()
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {

View File

@@ -119,7 +119,7 @@ class PageViewModel(
} else { } else {
null null
} }
state.value = PageState.Loaded(uri.toImageSource(cachedBounds), isConverted = true) state.value = PageState.Loaded(newUri.toImageSource(cachedBounds), isConverted = true)
} catch (ce: CancellationException) { } catch (ce: CancellationException) {
throw ce throw ce
} catch (e2: Throwable) { } catch (e2: Throwable) {

View File

@@ -10,7 +10,7 @@ collections = "1.5.0"
#noinspection NewerVersionAvailable,GradleDependency - 2.5.3 cause crashes #noinspection NewerVersionAvailable,GradleDependency - 2.5.3 cause crashes
conscrypt = "2.5.2" conscrypt = "2.5.2"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
coreKtx = "1.15.0" coreKtx = "1.16.0"
coroutines = "1.10.2" coroutines = "1.10.2"
desugar = "2.1.5" desugar = "2.1.5"
diskLruCache = "1.5" diskLruCache = "1.5"