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

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import coil3.ImageLoader
import coil3.asImage
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 bitmap = Bitmap.createBitmap(info.width, info.height, config)
val bitmap = createBitmap(info.width, info.height, config)
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
bitmap.recycle()
throw ImageDecodeException(null, "avif")

View File

@@ -2,15 +2,19 @@ package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.ImageDecoder
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.graphics.createBitmap
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import okio.IOException
import org.aomedia.avif.android.AvifDecoder
import org.aomedia.avif.android.AvifDecoder.Info
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.util.MimeTypes
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.toMimeTypeOrNull
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
fun probeMimeType(file: File): MimeType? {
return MimeTypes.probeMimeType(file) ?: detectBitmapType(file)
@@ -62,7 +79,7 @@ object BitmapDecoderCompat {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
return options.outMimeType?.toMimeTypeOrNull()
options.outMimeType?.toMimeTypeOrNull()
}.getOrNull()
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 bitmap = Bitmap.createBitmap(info.width, info.height, config)
val bitmap = createBitmap(info.width, info.height, config)
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
bitmap.recycle()
throw ImageDecodeException(null, FORMAT_AVIF)

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.os.Build
import coil3.Extras
@@ -11,7 +10,6 @@ import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.getExtra
import coil3.request.Options
@@ -25,24 +23,31 @@ import coil3.size.Scale
import coil3.size.Size
import coil3.size.isOriginal
import coil3.size.pxOrElse
import kotlinx.coroutines.runInterruptible
import kotlin.math.roundToInt
class RegionBitmapDecoder(
private val source: ImageSource,
private val fetchResult: SourceFetchResult,
private val options: Options,
private val imageLoader: ImageLoader,
) : Decoder {
override suspend fun decode(): DecodeResult = runInterruptible {
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(source.source().inputStream())
} else {
@Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(source.source().inputStream(), false)
override suspend fun decode(): DecodeResult? {
val regionDecoder = BitmapDecoderCompat.createRegionDecoder(fetchResult.source.source().inputStream())
if (regionDecoder == null) {
val fallbackDecoder = imageLoader.components.newDecoder(
result = fetchResult,
options = options,
imageLoader = imageLoader,
startIndex = 0,
)?.first
return if (fallbackDecoder == null || fallbackDecoder is RegionBitmapDecoder) {
null
} else {
fallbackDecoder.decode()
}
}
checkNotNull(regionDecoder)
val bitmapOptions = BitmapFactory.Options()
try {
return try {
val rect = bitmapOptions.configureScale(regionDecoder.width, regionDecoder.height)
bitmapOptions.configureConfig()
val bitmap = regionDecoder.decodeRegion(rect, bitmapOptions)
@@ -149,7 +154,7 @@ class RegionBitmapDecoder(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder = RegionBitmapDecoder(result.source, options)
): Decoder = RegionBitmapDecoder(result, options, imageLoader)
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.graphics.Rect
import android.net.Uri
import android.util.Log
import androidx.annotation.AnyThread
import androidx.annotation.CheckResult
import androidx.collection.LongSparseArray
import androidx.collection.set
import androidx.core.net.toFile
@@ -30,7 +32,6 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.use
@@ -184,9 +185,10 @@ class PageLoader @Inject constructor(
return loadPageAsync(page, force).await()
}
@CheckResult
suspend fun convertBimap(uri: Uri): Uri = convertLock.withLock {
if (uri.isZipUri()) {
val bitmap = runInterruptible(Dispatchers.IO) {
runInterruptible(Dispatchers.IO) {
ZipFile(uri.schemeSpecificPart).use { zip ->
val entry = zip.getEntry(uri.fragment)
context.ensureRamAtLeast(entry.size * 2)
@@ -194,8 +196,9 @@ class PageLoader @Inject constructor(
BitmapDecoderCompat.decode(it, MimeTypes.getMimeTypeFromExtension(entry.name))
}
}
}.use { image ->
cache.put(uri.toString(), image).toUri()
}
cache.put(uri.toString(), bitmap).toUri()
} else {
val file = uri.toFile()
runInterruptible(Dispatchers.IO) {

View File

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