refactor: Use Okio instead of java.io pt.1

This is the easy part... the next part is hell :')
This commit is contained in:
Ahmad Ansori Palembani 2024-05-29 19:48:43 +07:00
parent 7ddb15f312
commit 32ab87df61
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
6 changed files with 89 additions and 79 deletions

View file

@ -32,8 +32,8 @@ import coil3.request.allowRgb565
import coil3.request.crossfade import coil3.request.crossfade
import coil3.util.DebugLogger import coil3.util.DebugLogger
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.CoilDiskCache import eu.kanade.tachiyomi.data.coil.CoilDiskCache
import eu.kanade.tachiyomi.data.coil.InputStreamFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
@ -199,7 +199,7 @@ open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.F
add(TachiyomiImageDecoder.Factory()) add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.Factory(callFactoryLazy, diskCacheLazy)) add(MangaCoverFetcher.Factory(callFactoryLazy, diskCacheLazy))
add(MangaCoverKeyer()) add(MangaCoverKeyer())
add(InputStreamFetcher.Factory()) add(BufferedSourceFetcher.Factory())
} }
diskCache(diskCacheLazy::value) diskCache(diskCacheLazy::value)
memoryCache { MemoryCache.Builder().maxSizePercent(this@App, 0.40).build() } memoryCache { MemoryCache.Builder().maxSizePercent(this@App, 0.40).build() }

View file

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.coil
import coil3.ImageLoader
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import okio.BufferedSource
class BufferedSourceFetcher(
private val data: BufferedSource,
private val options: Options,
) : Fetcher {
override suspend fun fetch(): FetchResult {
return SourceFetchResult(
source = ImageSource(
source = data,
fileSystem = options.fileSystem,
),
mimeType = null,
dataSource = DataSource.MEMORY,
)
}
class Factory : Fetcher.Factory<BufferedSource> {
override fun create(data: BufferedSource, options: Options, imageLoader: ImageLoader): Fetcher {
return BufferedSourceFetcher(data, options)
}
}
}

View file

@ -781,7 +781,7 @@ class ReaderViewModel(
val imageBytes2 = stream2().readBytes() val imageBytes2 = stream2().readBytes()
val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size) val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
val stream = ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, bg) val stream = ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, bg).inputStream()
val chapter = page1.chapter.chapter val chapter = page1.chapter.chapter
val context = Injekt.get<Application>() val context = Injekt.get<Application>()

View file

@ -38,8 +38,7 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.GLUtil import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.animatorDurationScale
import java.io.InputStream import okio.BufferedSource
import java.nio.ByteBuffer
/** /**
* A wrapper view for showing page image. * A wrapper view for showing page image.
@ -99,13 +98,13 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
} }
fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) { fun setImage(source: BufferedSource, isAnimated: Boolean, config: Config) {
if (isAnimated) { if (isAnimated) {
prepareAnimatedImageView() prepareAnimatedImageView()
setAnimatedImage(inputStream, config) setAnimatedImage(source, config)
} else { } else {
prepareNonAnimatedImageView() prepareNonAnimatedImageView()
setNonAnimatedImage(inputStream, config) setNonAnimatedImage(source, config)
} }
} }
@ -225,7 +224,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
}, },
) )
val useCoilPipeline = isWebtoon && data is InputStream && !ImageUtil.isMaxTextureSizeExceeded(data) val useCoilPipeline = isWebtoon && data is BufferedSource && !ImageUtil.isMaxTextureSizeExceeded(data)
if (isWebtoon && useCoilPipeline) { if (isWebtoon && useCoilPipeline) {
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
@ -252,7 +251,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
} else { } else {
when (data) { when (data) {
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap)) is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
is InputStream -> setImage(ImageSource.inputStream(data)) is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream()))
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}") else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
} }
isVisible = true isVisible = true
@ -299,18 +298,13 @@ open class ReaderPageImageView @JvmOverloads constructor(
} }
private fun setAnimatedImage( private fun setAnimatedImage(
image: Any, data: Any,
config: Config, config: Config,
) = (pageView as? AppCompatImageView)?.apply { ) = (pageView as? AppCompatImageView)?.apply {
if (this is PhotoView) { if (this is PhotoView) {
setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration()) setZoomTransitionDuration(config.zoomDuration.getSystemScaledDuration())
} }
val data = when (image) {
is Drawable -> image
is InputStream -> ByteBuffer.wrap(image.readBytes())
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
}
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
.data(data) .data(data)
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)

View file

@ -28,10 +28,9 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.suspendCancellableCoroutine import okio.Buffer
import okio.BufferedSource
import timber.log.Timber import timber.log.Timber
import java.io.BufferedInputStream
import java.io.InputStream
/** /**
* Holder of the webtoon reader for a single page of a chapter. * Holder of the webtoon reader for a single page of a chapter.
@ -232,13 +231,11 @@ class WebtoonPageHolder(
val streamFn = page?.stream ?: return val streamFn = page?.stream ?: return
val (openStream, isAnimated) = try { val (source, isAnimated) = try {
withIOContext { withIOContext {
val stream = streamFn().buffered(16) val source = streamFn().use { process(Buffer().readFrom(it)) }
val openStream = process(stream) val isAnimated = ImageUtil.isAnimatedAndSupported(source)
Pair(source, isAnimated)
val isAnimated = ImageUtil.isAnimatedAndSupported(stream)
Pair(openStream, isAnimated)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
@ -247,7 +244,7 @@ class WebtoonPageHolder(
} }
withUIContext { withUIContext {
frame.setImage( frame.setImage(
openStream, source,
isAnimated, isAnimated,
ReaderPageImageView.Config( ReaderPageImageView.Config(
zoomDuration = viewer.config.doubleTapAnimDuration, zoomDuration = viewer.config.doubleTapAnimDuration,
@ -258,23 +255,19 @@ class WebtoonPageHolder(
), ),
) )
} }
// Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled
suspendCancellableCoroutine<Nothing> { continuation ->
continuation.invokeOnCancellation { openStream.close() }
}
} }
private fun process(imageStream: BufferedInputStream): InputStream { private fun process(imageSource: BufferedSource): BufferedSource {
if (!viewer.config.splitPages) { if (!viewer.config.splitPages) {
return imageStream return imageSource
} }
val isDoublePage = ImageUtil.isWideImage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageSource)
if (!isDoublePage) { if (!isDoublePage) {
return imageStream return imageSource
} }
return ImageUtil.splitAndStackBitmap(imageStream, viewer.config.invertDoublePages, viewer.hasMargins) return ImageUtil.splitAndStackBitmap(imageSource, viewer.config.invertDoublePages, viewer.hasMargins)
} }
/** /**

View file

@ -25,12 +25,11 @@ import androidx.core.graphics.red
import androidx.core.graphics.scale import androidx.core.graphics.scale
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import okio.Buffer
import okio.BufferedSource
import tachiyomi.decoder.Format import tachiyomi.decoder.Format
import tachiyomi.decoder.ImageDecoder import tachiyomi.decoder.ImageDecoder
import timber.log.Timber import timber.log.Timber
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
@ -92,9 +91,9 @@ object ImageUtil {
?: "jpg" ?: "jpg"
} }
fun isAnimatedAndSupported(stream: InputStream): Boolean { fun isAnimatedAndSupported(source: BufferedSource): Boolean {
return try { return try {
val type = getImageType(stream) ?: return false val type = getImageType(source.peek().inputStream()) ?: return false
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats // https://coil-kt.github.io/coil/getting_started/#supported-image-formats
when (type.format) { when (type.format) {
Format.Gif -> true Format.Gif -> true
@ -335,7 +334,7 @@ object ImageUtil {
imageBitmap: Bitmap, imageBitmap: Bitmap,
secondHalf: Boolean, secondHalf: Boolean,
progressCallback: ((Int) -> Unit)? = null, progressCallback: ((Int) -> Unit)? = null,
): ByteArrayInputStream { ): BufferedSource {
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
val result = Bitmap.createBitmap(width / 2, height, Bitmap.Config.ARGB_8888) val result = Bitmap.createBitmap(width / 2, height, Bitmap.Config.ARGB_8888)
@ -343,10 +342,10 @@ object ImageUtil {
progressCallback?.invoke(98) progressCallback?.invoke(98)
canvas.drawBitmap(imageBitmap, Rect(if (!secondHalf) 0 else width / 2, 0, if (secondHalf) width else width / 2, height), result.rect, null) canvas.drawBitmap(imageBitmap, Rect(if (!secondHalf) 0 else width / 2, 0, if (secondHalf) width else width / 2, height), result.rect, null)
progressCallback?.invoke(99) progressCallback?.invoke(99)
val output = ByteArrayOutputStream() val output = Buffer()
result.compress(Bitmap.CompressFormat.JPEG, 100, output) result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
progressCallback?.invoke(100) progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray()) return output
} }
/** /**
@ -354,20 +353,18 @@ object ImageUtil {
* *
* @return true if the width is greater than the height * @return true if the width is greater than the height
*/ */
fun isWideImage(imageStream: BufferedInputStream): Boolean { fun isWideImage(imageSource: BufferedSource): Boolean {
val options = extractImageOptions(imageStream) val options = extractImageOptions(imageSource)
imageStream.reset()
return options.outWidth > options.outHeight return options.outWidth > options.outHeight
} }
fun splitAndStackBitmap( fun splitAndStackBitmap(
imageStream: InputStream, imageSource: BufferedSource,
rightSideOnTop: Boolean, rightSideOnTop: Boolean,
hasMargins: Boolean, hasMargins: Boolean,
progressCallback: ((Int) -> Unit)? = null, progressCallback: ((Int) -> Unit)? = null,
): ByteArrayInputStream { ): BufferedSource {
val imageBytes = imageStream.readBytes() val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream())
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
@ -411,10 +408,10 @@ object ImageUtil {
null, null,
) )
progressCallback?.invoke(99) progressCallback?.invoke(99)
val output = ByteArrayOutputStream() val output = Buffer()
result.compress(Bitmap.CompressFormat.JPEG, 100, output) result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
progressCallback?.invoke(100) progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray()) return output
} }
fun mergeBitmaps( fun mergeBitmaps(
@ -425,7 +422,7 @@ object ImageUtil {
hingeGap: Int = 0, hingeGap: Int = 0,
context: Context? = null, context: Context? = null,
progressCallback: ((Int) -> Unit)? = null, progressCallback: ((Int) -> Unit)? = null,
): ByteArrayInputStream { ): BufferedSource {
var imageBitmap = iBitmap var imageBitmap = iBitmap
var imageBitmap2 = iBitmap2 var imageBitmap2 = iBitmap2
var height = imageBitmap.height var height = imageBitmap.height
@ -473,10 +470,10 @@ object ImageUtil {
canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null) canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null)
progressCallback?.invoke(99) progressCallback?.invoke(99)
val output = ByteArrayOutputStream() val output = Buffer()
result.compress(Bitmap.CompressFormat.JPEG, 100, output) result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
progressCallback?.invoke(100) progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray()) return output
} }
fun padSingleImage( fun padSingleImage(
@ -487,7 +484,7 @@ object ImageUtil {
hingeGap: Int, hingeGap: Int,
context: Context, context: Context,
progressCallback: ((Int) -> Unit)? = null, progressCallback: ((Int) -> Unit)? = null,
): ByteArrayInputStream { ): BufferedSource {
val height = imageBitmap.height val height = imageBitmap.height
val width = imageBitmap.width val width = imageBitmap.width
val isFullPageSpread = height < width val isFullPageSpread = height < width
@ -523,10 +520,10 @@ object ImageUtil {
canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null) canvas.drawBitmap(imageBitmap, imageBitmap.rect, upperPart, null)
} }
progressCallback?.invoke(99) progressCallback?.invoke(99)
val output = ByteArrayOutputStream() val output = Buffer()
result.compress(Bitmap.CompressFormat.JPEG, 100, output) result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
progressCallback?.invoke(100) progressCallback?.invoke(100)
return ByteArrayInputStream(output.toByteArray()) return output
} }
/** /**
@ -534,8 +531,8 @@ object ImageUtil {
* *
* @return true if the height:width ratio is greater than 3. * @return true if the height:width ratio is greater than 3.
*/ */
private fun isTallImage(imageStream: InputStream): Boolean { private fun isTallImage(imageSource: BufferedSource): Boolean {
val options = extractImageOptions(imageStream, false) val options = extractImageOptions(imageSource)
return (options.outHeight / options.outWidth) > 3 return (options.outHeight / options.outWidth) > 3
} }
@ -543,15 +540,16 @@ object ImageUtil {
* Splits tall images to improve performance of reader * Splits tall images to improve performance of reader
*/ */
fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean { fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean {
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) { val imageSource = imageFile.openInputStream().use { Buffer().readFrom(it) }
if (isAnimatedAndSupported(imageSource) || !isTallImage(imageSource)) {
return true return true
} }
val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(imageFile.openInputStream()) BitmapRegionDecoder.newInstance(imageSource.peek().inputStream())
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(imageFile.openInputStream(), false) BitmapRegionDecoder.newInstance(imageSource.peek().inputStream(), false)
} }
if (bitmapRegionDecoder == null) { if (bitmapRegionDecoder == null) {
@ -559,7 +557,7 @@ object ImageUtil {
return false return false
} }
val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { val options = extractImageOptions(imageSource).apply {
inJustDecodeBounds = false inJustDecodeBounds = false
} }
val splitDataList = options.splitData val splitDataList = options.splitData
@ -773,16 +771,9 @@ object ImageUtil {
/** /**
* Used to check an image's dimensions without loading it in the memory. * Used to check an image's dimensions without loading it in the memory.
*/ */
private fun extractImageOptions( private fun extractImageOptions(imageSource: BufferedSource): BitmapFactory.Options {
imageStream: InputStream,
resetAfterExtraction: Boolean = true,
): BitmapFactory.Options {
imageStream.mark(imageStream.available() + 1)
val imageBytes = imageStream.readBytes()
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) BitmapFactory.decodeStream(imageSource.peek().inputStream(), null, options)
if (resetAfterExtraction) imageStream.reset()
return options return options
} }
@ -792,8 +783,8 @@ object ImageUtil {
"image/jxl" to "jxl", "image/jxl" to "jxl",
) )
fun isMaxTextureSizeExceeded(imageStream: InputStream): Boolean { fun isMaxTextureSizeExceeded(imageSource: BufferedSource): Boolean {
val opts = extractImageOptions(imageStream) val opts = extractImageOptions(imageSource)
return maxOf(opts.outWidth, opts.outHeight) > GLUtil.maxTextureSize return maxOf(opts.outWidth, opts.outHeight) > GLUtil.maxTextureSize
} }
} }