Use Coil pipeline instead of SSIV for image decode

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
This commit is contained in:
Secozzi 2024-07-11 21:09:41 +02:00
parent 107c3f6853
commit 1ee6f0fc57
No known key found for this signature in database
GPG key ID: 71E9C97D8DDC2662
4 changed files with 123 additions and 13 deletions

View file

@ -1,12 +1,16 @@
package eu.kanade.tachiyomi.data.coil
import android.graphics.Bitmap
import coil3.ImageLoader
import coil3.asCoilImage
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.system.GLUtil
import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder
@ -18,27 +22,55 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
override suspend fun decode(): DecodeResult {
val decoder = resources.sourceOrNull()?.use {
ImageDecoder.newInstance(it.inputStream())
ImageDecoder.newInstance(it.inputStream(), options.cropBorders, displayProfile)
}
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder" }
val bitmap = decoder.decode()
val srcWidth = decoder.width
val srcHeight = decoder.height
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
val sampleSize = DecodeUtils.calculateInSampleSize(
srcWidth = srcWidth,
srcHeight = srcHeight,
dstWidth = dstWidth,
dstHeight = dstHeight,
scale = options.scale,
)
var bitmap = decoder.decode(sampleSize = sampleSize)
decoder.recycle()
check(bitmap != null) { "Failed to decode image" }
if (
options.bitmapConfig == Bitmap.Config.HARDWARE &&
maxOf(bitmap.width, bitmap.height) <= GLUtil.maxTextureSize
) {
val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false)
if (hwBitmap != null) {
bitmap.recycle()
bitmap = hwBitmap
}
}
return DecodeResult(
image = bitmap.asCoilImage(),
isSampled = false,
isSampled = sampleSize > 1,
)
}
class Factory : Decoder.Factory {
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null
return TachiyomiImageDecoder(result.source, options)
return if (options.customDecoder || isApplicable(result.source.source())) {
TachiyomiImageDecoder(result.source, options)
} else {
null
}
}
private fun isApplicable(source: BufferedSource): Boolean {
@ -55,4 +87,8 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
override fun hashCode() = javaClass.hashCode()
}
companion object {
var displayProfile: ByteArray? = null
}
}

View file

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.data.coil
import coil3.Extras
import coil3.getExtra
import coil3.request.ImageRequest
import coil3.request.Options
import coil3.size.Dimension
import coil3.size.Scale
import coil3.size.Size
import coil3.size.isOriginal
import coil3.size.pxOrElse
internal inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale)
}
internal inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else height.toPx(scale)
}
internal fun Dimension.toPx(scale: Scale): Int = pxOrElse {
when (scale) {
Scale.FILL -> Int.MIN_VALUE
Scale.FIT -> Int.MAX_VALUE
}
}
fun ImageRequest.Builder.cropBorders(enable: Boolean) = apply {
extras[cropBordersKey] = enable
}
val Options.cropBorders: Boolean
get() = getExtra(cropBordersKey)
private val cropBordersKey = Extras.Key(default = false)
fun ImageRequest.Builder.customDecoder(enable: Boolean) = apply {
extras[customDecoderKey] = enable
}
val Options.customDecoder: Boolean
get() = getExtra(customDecoderKey)
private val customDecoderKey = Extras.Key(default = false)

View file

@ -52,6 +52,7 @@ import eu.kanade.presentation.reader.appbars.ReaderAppBars
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.common.Constants
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
@ -879,7 +880,9 @@ class ReaderActivity : BaseActivity() {
input.copyTo(output)
}
}
SubsamplingScaleImageView.setDisplayProfile(outputStream.toByteArray())
val data = outputStream.toByteArray()
SubsamplingScaleImageView.setDisplayProfile(data)
TachiyomiImageDecoder.displayProfile = data
}
}

View file

@ -18,17 +18,22 @@ import androidx.annotation.StyleRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed
import androidx.core.view.isVisible
import coil3.BitmapImage
import coil3.dispose
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.size.Precision
import coil3.size.ViewSizeResolver
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.data.coil.cropBorders
import eu.kanade.tachiyomi.data.coil.customDecoder
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
@ -295,15 +300,37 @@ open class ReaderPageImageView @JvmOverloads constructor(
},
)
if (isWebtoon) {
val request = ImageRequest.Builder(context)
.data(data)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.target(
onSuccess = { result ->
val image = result as BitmapImage
setImage(ImageSource.bitmap(image.bitmap))
isVisible = true
},
onError = {
this@ReaderPageImageView.onImageLoadError()
},
)
.size(ViewSizeResolver(this@ReaderPageImageView))
.precision(Precision.INEXACT)
.cropBorders(config.cropBorders)
.customDecoder(true)
.crossfade(false)
.build()
context.imageLoader.enqueue(request)
} else {
when (data) {
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream()))
else -> throw IllegalArgumentException(
"Not implemented for class ${data::class.simpleName}",
)
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
}
isVisible = true
}
}
private fun prepareAnimatedImageView() {
if (pageView is AppCompatImageView) return