mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-23 13:23:28 +03:00
Use Coil pipeline instead of SSIV for image decode
Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
This commit is contained in:
parent
107c3f6853
commit
1ee6f0fc57
4 changed files with 123 additions and 13 deletions
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
44
app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt
Normal file
44
app/src/main/java/eu/kanade/tachiyomi/data/coil/Utils.kt
Normal 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)
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue