Fix avif image decoding
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user