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 package eu.kanade.tachiyomi.data.coil
import android.graphics.Bitmap
import coil3.ImageLoader import coil3.ImageLoader
import coil3.asCoilImage import coil3.asCoilImage
import coil3.decode.DecodeResult import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder import coil3.decode.Decoder
import coil3.decode.ImageSource import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult import coil3.fetch.SourceFetchResult
import coil3.request.Options import coil3.request.Options
import coil3.request.bitmapConfig
import eu.kanade.tachiyomi.util.system.GLUtil
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
@ -18,27 +22,55 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
override suspend fun decode(): DecodeResult { override suspend fun decode(): DecodeResult {
val decoder = resources.sourceOrNull()?.use { 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" } 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() decoder.recycle()
check(bitmap != null) { "Failed to decode image" } 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( return DecodeResult(
image = bitmap.asCoilImage(), image = bitmap.asCoilImage(),
isSampled = false, isSampled = sampleSize > 1,
) )
} }
class Factory : Decoder.Factory { class Factory : Decoder.Factory {
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? { override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null return if (options.customDecoder || isApplicable(result.source.source())) {
return TachiyomiImageDecoder(result.source, options) TachiyomiImageDecoder(result.source, options)
} else {
null
}
} }
private fun isApplicable(source: BufferedSource): Boolean { private fun isApplicable(source: BufferedSource): Boolean {
@ -55,4 +87,8 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
override fun hashCode() = javaClass.hashCode() 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.presentation.reader.settings.ReaderSettingsDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.common.Constants 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.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
@ -879,7 +880,9 @@ class ReaderActivity : BaseActivity() {
input.copyTo(output) 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.appcompat.widget.AppCompatImageView
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil3.BitmapImage
import coil3.dispose import coil3.dispose
import coil3.imageLoader import coil3.imageLoader
import coil3.request.CachePolicy import coil3.request.CachePolicy
import coil3.request.ImageRequest import coil3.request.ImageRequest
import coil3.request.crossfade import coil3.request.crossfade
import coil3.size.Precision
import coil3.size.ViewSizeResolver
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD 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.EASE_OUT_QUAD
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView 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.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale 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) { when (data) {
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap)) is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream())) is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream()))
else -> throw IllegalArgumentException( else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
"Not implemented for class ${data::class.simpleName}",
)
} }
isVisible = true isVisible = true
} }
}
private fun prepareAnimatedImageView() { private fun prepareAnimatedImageView() {
if (pageView is AppCompatImageView) return if (pageView is AppCompatImageView) return