From c3e7bb12f4cccf42dd3ea169111c771876e640fe Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Wed, 1 May 2024 15:07:30 +0800 Subject: [PATCH] Use Coil pipeline instead of SSIV for image decode (#692) --- .../data/coil/TachiyomiImageDecoder.kt | 46 +++++++++++++++++-- .../eu/kanade/tachiyomi/data/coil/Utils.kt | 44 ++++++++++++++++++ .../tachiyomi/ui/reader/ReaderActivity.kt | 5 +- .../ui/reader/viewer/ReaderPageImageView.kt | 39 ++++++++++++++-- 4 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index 2f0c3df49..2929f4da3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -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 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt new file mode 100644 index 000000000..7a920bf39 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 62bf23646..ae471a025 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -51,6 +51,7 @@ import eu.kanade.presentation.reader.ReadingModeSelectDialog import eu.kanade.presentation.reader.appbars.ReaderAppBars import eu.kanade.presentation.reader.settings.ReaderSettingsDialog import eu.kanade.tachiyomi.R +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 @@ -872,7 +873,9 @@ class ReaderActivity : BaseActivity() { input.copyTo(output) } } - SubsamplingScaleImageView.setDisplayProfile(outputStream.toByteArray()) + val data = outputStream.toByteArray() + SubsamplingScaleImageView.setDisplayProfile(data) + TachiyomiImageDecoder.displayProfile = data } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 0c83d97ea..17b973a7e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -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 @@ -282,12 +287,36 @@ open class ReaderPageImageView @JvmOverloads constructor( }, ) - 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}") + 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}") + } + isVisible = true } - isVisible = true } private fun prepareAnimatedImageView() {