From 5d116d9477ce5ef316e59cd7c90f9ba4563395a6 Mon Sep 17 00:00:00 2001 From: Vladimir Dostoyevskiy <99822064+VDostoyevskiy@users.noreply.github.com> Date: Tue, 2 Jan 2024 15:43:31 +0800 Subject: [PATCH] Image wrap a CoilImage (#588) * Image wrap a CoilImage * Make ImageSource sealed --------- Co-authored-by: GG Bond --- .../subscaleview/SubsamplingScaleImageView.kt | 7 +- .../ehviewer/gallery/ArchivePageLoader.kt | 3 +- .../kotlin/com/hippo/ehviewer/image/Image.kt | 101 +++++++++--------- .../com/hippo/ehviewer/spider/SpiderDen.kt | 2 +- .../tachiyomi/ui/reader/loader/PageLoader.kt | 2 +- .../ui/reader/viewer/ReaderPageImageView.kt | 23 ++-- .../ui/reader/viewer/pager/PagerPageHolder.kt | 12 +-- .../viewer/webtoon/WebtoonPageHolder.kt | 6 +- 8 files changed, 79 insertions(+), 77 deletions(-) diff --git a/app/src/main/kotlin/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt b/app/src/main/kotlin/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt index b44492b58..ec0873df4 100644 --- a/app/src/main/kotlin/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt +++ b/app/src/main/kotlin/com/davemorrissey/labs/subscaleview/SubsamplingScaleImageView.kt @@ -9,7 +9,6 @@ import android.graphics.Paint import android.graphics.PointF import android.graphics.Rect import android.graphics.RectF -import android.graphics.drawable.BitmapDrawable import android.util.AttributeSet import android.util.TypedValue import android.view.GestureDetector @@ -231,10 +230,10 @@ open class SubsamplingScaleImageView @JvmOverloads constructor( * * @param bitmap Image source. */ - fun setImage(bitmap: BitmapDrawable) { + fun setImage(bitmap: Bitmap, rect: Rect) { reset(true) - srcRect = Rect(bitmap.bounds) - onImageLoaded(bitmap.bitmap) + srcRect = Rect(rect) + onImageLoaded(bitmap) } /** diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt index 778eec434..b52305bd8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt @@ -24,6 +24,7 @@ import android.os.ParcelFileDescriptor import android.util.Log import androidx.annotation.RequiresApi import com.hippo.ehviewer.Settings.archivePasswds +import com.hippo.ehviewer.image.ByteBufferSource import com.hippo.ehviewer.image.Image import com.hippo.ehviewer.jni.closeArchive import com.hippo.ehviewer.jni.extractToByteBuffer @@ -110,7 +111,7 @@ class ArchivePageLoader(context: Context, private val uri: Uri, passwdProvider: val buffer = extractToByteBuffer(index) buffer ?: return check(buffer.isDirect) - val src = object : Image.ByteBufferSource { + val src = object : ByteBufferSource { override val source: ByteBuffer = buffer override fun close() { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt b/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt index 166805290..9eed8f6b6 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/image/Image.kt @@ -22,16 +22,16 @@ import android.graphics.Bitmap import android.graphics.ColorSpace import android.graphics.Rect import android.graphics.drawable.Animatable -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import androidx.annotation.RequiresApi -import androidx.core.graphics.drawable.toDrawable import arrow.core.Either import arrow.core.left import arrow.core.right import coil3.BitmapImage +import coil3.DrawableImage +import coil3.Image as CoilImage +import coil3.asCoilImage import coil3.executeBlocking import coil3.imageLoader import coil3.request.CachePolicy @@ -58,45 +58,41 @@ import splitties.init.appCtx private val CropAspect = 0.1..100.0 -class Image private constructor(drawable: Drawable, private val src: AutoCloseable) { - val size = drawable.run { intrinsicHeight * intrinsicWidth * 4 * if (this is Animatable) 4 else 1 } - private val intrinsicRect = drawable.run { Rect(0, 0, intrinsicWidth, intrinsicHeight) } - - var mObtainedDrawable: Drawable? = if (drawable is Animatable) { - // Cannot crop animated image's border - drawable +class Image private constructor(image: CoilImage, private val src: AutoCloseable) { + val size = image.size + private val intrinsicRect = image.run { Rect(0, 0, width, height) } + val rect: Rect = if (Settings.cropBorder.value && image is BitmapImage) { + val bitmap = image.bitmap + val array = detectBorder(bitmap) + val r = Rect(array[0], array[1], array[2], array[3]) + val aspectAfterCrop = r.height().toFloat() / r.width() + if (aspectAfterCrop in CropAspect) r else intrinsicRect } else { - if (Settings.cropBorder.value) { - val bitmap = (drawable as BitmapDrawable).bitmap - val array = detectBorder(bitmap) - val rect = with(bitmap) { - val r = Rect(array[0], array[1], array[2], array[3]) - val aspectAfterCrop = r.height().toFloat() / r.width() - if (aspectAfterCrop in CropAspect) r else intrinsicRect - } + intrinsicRect + } - val final = if (isAtLeastQ) { - // Upload to Graphical Buffer to accelerate render - bitmap.copy(Bitmap.Config.HARDWARE, false).apply { - bitmap.recycle() - } - } else { - bitmap - } + var innerImage: CoilImage? = if (image is BitmapImage && isAtLeastQ) { + val bitmap = image.bitmap - final.toDrawable(appCtx.resources).apply { bounds = rect } - } else { - drawable.apply { bounds = intrinsicRect } - } + // Upload to Graphical Buffer to accelerate render + bitmap.copy(Bitmap.Config.HARDWARE, false).apply { + bitmap.recycle() + }.asCoilImage() + } else { + image } @Synchronized fun recycle() { - (mObtainedDrawable as? Animatable)?.stop() - (mObtainedDrawable as? BitmapDrawable)?.bitmap?.recycle() - mObtainedDrawable?.callback = null - if (mObtainedDrawable is Animatable) src.close() - mObtainedDrawable = null + when (val image = innerImage ?: return) { + is DrawableImage -> { + (image.drawable as Animatable).stop() + image.drawable.callback = null + src.close() + } + is BitmapImage -> image.bitmap.recycle() + } + innerImage = null } companion object { @@ -110,7 +106,7 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab @RequiresApi(Build.VERSION_CODES.O) lateinit var colorSpace: ColorSpace - private suspend fun Either.decodeCoil(): Drawable { + private suspend fun Either.decodeCoil(): CoilImage { val req = appCtx.imageRequest { onLeft { data(it.source) } onRight { data(it.source.uri) } @@ -125,14 +121,13 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab } memoryCachePolicy(CachePolicy.DISABLED) } - val image = when (val result = appCtx.imageLoader.execute(req)) { + return when (val result = appCtx.imageLoader.execute(req)) { is SuccessResult -> result.image is ErrorResult -> throw result.throwable } - return image.asDrawable(appCtx.resources) } - suspend fun decode(src: AutoCloseable): Image? { + suspend fun decode(src: ImageSource): Image? { return runCatching { when (src) { is UniFileSource -> { @@ -152,21 +147,19 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab } } } - val drawable = src.right().decodeCoil() - if (drawable !is Animatable) src.close() - Image(drawable, src) + val image = src.right().decodeCoil() + if (image is BitmapImage) src.close() + Image(image, src) } is ByteBufferSource -> { if (isAtLeastP && !isAtLeastU) { rewriteGifSource(src.source) } - val drawable = src.left().decodeCoil() - if (drawable !is Animatable) src.close() - Image(drawable, src) + val image = src.left().decodeCoil() + if (image is BitmapImage) src.close() + Image(image, src) } - - else -> TODO("Unsupported") } }.onFailure { src.close() @@ -188,14 +181,16 @@ class Image private constructor(drawable: Drawable, private val src: AutoCloseab return (image as BitmapImage).bitmap } } +} - interface UniFileSource : AutoCloseable { - val source: UniFile - } +sealed interface ImageSource : AutoCloseable - interface ByteBufferSource : AutoCloseable { - val source: ByteBuffer - } +interface UniFileSource : ImageSource { + val source: UniFile +} + +interface ByteBufferSource : ImageSource { + val source: ByteBuffer } private external fun detectBorder(bitmap: Bitmap): IntArray diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt index 662b60ca0..44f0c0310 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt @@ -24,7 +24,7 @@ import com.hippo.ehviewer.client.getImageKey import com.hippo.ehviewer.coil.read import com.hippo.ehviewer.coil.suspendEdit import com.hippo.ehviewer.download.downloadLocation -import com.hippo.ehviewer.image.Image.UniFileSource +import com.hippo.ehviewer.image.UniFileSource import com.hippo.ehviewer.util.FileUtils import com.hippo.ehviewer.util.sendTo import com.hippo.unifile.UniFile diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt index c3863aec4..351c068f8 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/loader/PageLoader.kt @@ -19,7 +19,7 @@ abstract class PageLoader { } else { (OSUtils.appMaxMemory / 3 * 2).toInt() }, - sizeOf = { _, v -> v.size }, + sizeOf = { _, v -> v.size.toInt() }, onEntryRemoved = { _, _, o, _ -> o.recycle() }, ) val mPages by lazy { diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 2acec950e..c8262a8cb 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -1,10 +1,11 @@ package eu.kanade.tachiyomi.ui.reader.viewer import android.content.Context +import android.graphics.Bitmap import android.graphics.PointF +import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Animatable -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.GestureDetector @@ -18,10 +19,13 @@ import androidx.annotation.StyleRes import androidx.appcompat.widget.AppCompatImageView import androidx.core.os.postDelayed import androidx.core.view.isVisible +import coil3.BitmapImage +import coil3.DrawableImage import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.EASE_OUT_QUAD import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.Companion.SCALE_TYPE_CENTER_INSIDE import com.github.chrisbanes.photoview.PhotoView +import com.hippo.ehviewer.image.Image import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView import eu.kanade.tachiyomi.util.system.animatorDurationScale @@ -120,14 +124,16 @@ open class ReaderPageImageView @JvmOverloads constructor( } } - fun setImage(drawable: Drawable, config: Config) { + fun setImage(drawable: Image, config: Config) { this.config = config - if (drawable is Animatable) { + val image = drawable.innerImage ?: return + if (image is DrawableImage) { prepareAnimatedImageView() - setAnimatedImage(drawable, config) - } else { + setAnimatedImage(image.drawable, config) + } + if (image is BitmapImage) { prepareNonAnimatedImageView() - setNonAnimatedImage(drawable, config) + setNonAnimatedImage(image.bitmap, drawable.rect, config) } } @@ -233,7 +239,8 @@ open class ReaderPageImageView @JvmOverloads constructor( } private fun setNonAnimatedImage( - image: Drawable, + image: Bitmap, + bounds: Rect, config: Config, ) = (pageView as? SubsamplingScaleImageView)?.apply { setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) @@ -254,7 +261,7 @@ open class ReaderPageImageView @JvmOverloads constructor( }, ) - setImage(image as BitmapDrawable) + setImage(image, bounds) isVisible = true } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 8ef7befd3..5ffdbebaa 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager import android.annotation.SuppressLint import android.content.Context -import android.graphics.drawable.Animatable -import android.graphics.drawable.Drawable import android.view.LayoutInflater import androidx.core.view.isVisible +import coil3.BitmapImage import com.hippo.ehviewer.databinding.ReaderErrorBinding +import com.hippo.ehviewer.image.Image import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView @@ -97,7 +97,7 @@ class PagerPageHolder( setDownloading() } Page.State.READY -> { - page.image?.mObtainedDrawable?.let { setImage(it) } + page.image?.let { setImage(it) } cancelProgressJob() } Page.State.ERROR -> { @@ -148,12 +148,12 @@ class PagerPageHolder( /** * Called when the page is ready. */ - private fun setImage(drawable: Drawable) { + private fun setImage(image: Image) { progressIndicator.setProgress(0) progressIndicator.hide() errorLayout?.root?.isVisible = false setImage( - drawable, + image, Config( zoomDuration = viewer.config.doubleTapAnimDuration, minimumScaleType = viewer.config.imageScaleType, @@ -162,7 +162,7 @@ class PagerPageHolder( landscapeZoom = viewer.config.landscapeZoom, ), ) - if (drawable !is Animatable) { + if (image.innerImage is BitmapImage) { pageBackground = background } } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index 724aee0b1..6110e817b 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon import android.content.res.Resources -import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -10,6 +9,7 @@ import android.widget.FrameLayout import androidx.core.view.isVisible import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.hippo.ehviewer.databinding.ReaderErrorBinding +import com.hippo.ehviewer.image.Image import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView @@ -145,7 +145,7 @@ class WebtoonPageHolder( setDownloading() } Page.State.READY -> { - page?.image?.mObtainedDrawable?.let { setImage(it) } + page?.image?.let { setImage(it) } cancelProgressJob() } Page.State.ERROR -> { @@ -194,7 +194,7 @@ class WebtoonPageHolder( /** * Called when the page is ready. */ - private fun setImage(drawable: Drawable) { + private fun setImage(drawable: Image) { progressIndicator.setProgress(0) progressIndicator.hide() removeErrorLayout()