Image wrap a CoilImage (#588)

* Image wrap a CoilImage

* Make ImageSource sealed

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2024-01-02 07:43:31 +00:00
1 parent 6e73fcc8c9
commit 5d116d9477
8 files changed
+79 -77

No files matched your search

@@ -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)
}
/**
@@ -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() {
@@ -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<ByteBufferSource, UniFileSource>.decodeCoil(): Drawable {
private suspend fun Either<ByteBufferSource, UniFileSource>.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
@@ -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
@@ -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 {
@@ -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
}
@@ -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
}
}
@@ -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()