Fix pages thumbnails for webtoons

This commit is contained in:
Koitharu
2022-08-27 17:55:34 +03:00
parent 92aa96a644
commit 2aaaf2f4a2
5 changed files with 179 additions and 5 deletions

View File

@@ -27,10 +27,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.*
@AndroidEntryPoint
class ColorFilterConfigActivity :
@@ -117,6 +114,7 @@ class ColorFilterConfigActivity :
.data(preview.url)
.referer(preview.referer)
.scale(Scale.FILL)
.decodeRegion()
.error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(binding.imageViewBefore))
.allowRgb565(false)

View File

@@ -14,6 +14,7 @@ import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
@@ -30,7 +31,7 @@ fun pageThumbnailAD(
val gridWidth = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
val thumbSize = Size(
width = gridWidth,
height = (gridWidth * 13f / 18f).toInt(),
height = (gridWidth / 13f * 18f).toInt(),
)
suspend fun loadPageThumbnail(item: PageThumbnail): Drawable? = withContext(Dispatchers.Default) {
@@ -52,6 +53,7 @@ fun pageThumbnailAD(
ImageRequest.Builder(context)
.data(file)
.size(thumbSize)
.decodeRegion()
.allowRgb565(isLowRamDevice(context))
.build(),
).drawable

View File

@@ -13,6 +13,7 @@ import com.google.android.material.progressindicator.BaseProgressIndicator
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.utils.image.RegionBitmapDecoder
import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener
fun ImageView.newImageRequest(url: Any?): ImageRequest.Builder? {
@@ -66,6 +67,10 @@ fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRe
return listener(ImageRequestIndicatorListener(indicator))
}
fun ImageRequest.Builder.decodeRegion(): ImageRequest.Builder {
return decoderFactory(RegionBitmapDecoder.Factory())
}
@Suppress("SpellCheckingInspection")
fun ImageRequest.Builder.crossfade(context: Context?): ImageRequest.Builder {
if (context == null) {

View File

@@ -0,0 +1,168 @@
package org.koitharu.kotatsu.utils.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil.ImageLoader
import coil.decode.DecodeResult
import coil.decode.DecodeUtils
import coil.decode.Decoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import coil.size.*
import kotlin.math.roundToInt
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
class RegionBitmapDecoder(
private val source: ImageSource,
private val options: Options,
private val parallelismLock: Semaphore,
) : Decoder {
override suspend fun decode() = parallelismLock.withPermit {
runInterruptible { BitmapFactory.Options().decode() }
}
private fun BitmapFactory.Options.decode(): DecodeResult {
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(source.source().inputStream())
} else {
@Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(source.source().inputStream(), false)
}
checkNotNull(regionDecoder)
try {
val rect = configureScale(regionDecoder.width, regionDecoder.height)
configureConfig()
val bitmap = regionDecoder.decodeRegion(rect, this)
bitmap.density = options.context.resources.displayMetrics.densityDpi
return DecodeResult(
drawable = bitmap.toDrawable(options.context.resources),
isSampled = true,
)
} finally {
regionDecoder.recycle()
}
}
private fun BitmapFactory.Options.configureConfig() {
var config = options.config
inMutable = false
if (Build.VERSION.SDK_INT >= 26 && options.colorSpace != null) {
inPreferredColorSpace = options.colorSpace
}
inPremultiplied = options.premultipliedAlpha
// Decode the image as RGB_565 as an optimization if allowed.
if (options.allowRgb565 && config == Bitmap.Config.ARGB_8888 && outMimeType == "image/jpeg") {
config = Bitmap.Config.RGB_565
}
// High color depth images must be decoded as either RGBA_F16 or HARDWARE.
if (Build.VERSION.SDK_INT >= 26 && outConfig == Bitmap.Config.RGBA_F16 && config != Bitmap.Config.HARDWARE) {
config = Bitmap.Config.RGBA_F16
}
inPreferredConfig = config
}
/** Compute and set the scaling properties for [BitmapFactory.Options]. */
private fun BitmapFactory.Options.configureScale(srcWidth: Int, srcHeight: Int): Rect {
val dstWidth = options.size.widthPx(options.scale) { srcWidth }
val dstHeight = options.size.heightPx(options.scale) { srcHeight }
val srcRatio = srcWidth / srcHeight.toDouble()
val dstRatio = dstWidth / dstHeight.toDouble()
val rect = if (srcRatio < dstRatio) {
// probably manga
Rect(0, 0, srcWidth, (srcWidth / dstRatio).toInt())
} else {
Rect(0, 0, (srcHeight / dstRatio).toInt(), srcHeight)
}
rect.offsetTo(
(srcWidth - rect.width()) / 2,
(srcHeight - rect.height()) / 2,
)
// Calculate the image's sample size.
inSampleSize = DecodeUtils.calculateInSampleSize(
srcWidth = rect.width(),
srcHeight = rect.height(),
dstWidth = dstWidth,
dstHeight = dstHeight,
scale = options.scale,
)
// Calculate the image's density scaling multiple.
var scale = DecodeUtils.computeSizeMultiplier(
srcWidth = rect.width() / inSampleSize.toDouble(),
srcHeight = rect.height() / inSampleSize.toDouble(),
dstWidth = dstWidth.toDouble(),
dstHeight = dstHeight.toDouble(),
scale = options.scale,
)
// Only upscale the image if the options require an exact size.
if (options.allowInexactSize) {
scale = scale.coerceAtMost(1.0)
}
inScaled = scale != 1.0
if (inScaled) {
if (scale > 1) {
// Upscale
inDensity = (Int.MAX_VALUE / scale).roundToInt()
inTargetDensity = Int.MAX_VALUE
} else {
// Downscale
inDensity = Int.MAX_VALUE
inTargetDensity = (Int.MAX_VALUE * scale).roundToInt()
}
}
return rect
}
class Factory(
maxParallelism: Int = DEFAULT_MAX_PARALLELISM,
) : Decoder.Factory {
@Suppress("NEWER_VERSION_IN_SINCE_KOTLIN")
@SinceKotlin("999.9") // Only public in Java.
constructor() : this()
private val parallelismLock = Semaphore(maxParallelism)
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder {
return RegionBitmapDecoder(result.source, options, parallelismLock)
}
override fun equals(other: Any?) = other is Factory
override fun hashCode() = javaClass.hashCode()
}
}
private const val DEFAULT_MAX_PARALLELISM = 4
private inline fun Size.widthPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else width.toPx(scale)
}
private inline fun Size.heightPx(scale: Scale, original: () -> Int): Int {
return if (isOriginal) original() else height.toPx(scale)
}
private fun Dimension.toPx(scale: Scale) = pxOrElse {
when (scale) {
Scale.FILL -> Int.MIN_VALUE
Scale.FIT -> Int.MAX_VALUE
}
}

View File

@@ -123,6 +123,7 @@
android:singleLine="true"
android:text="@string/automatic_scroll"
android:textAppearance="?attr/textAppearanceButton"
android:textColor="@color/list_item_text_color"
app:drawableStartCompat="@drawable/ic_timer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/slider_timer"