Skip to content

Commit

Permalink
Use Okio instead of java.io for image processing (#691)
Browse files Browse the repository at this point in the history
  • Loading branch information
FooIbar authored Apr 20, 2024
1 parent f27ca3b commit b152e38
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 127 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.MangaKeyer
Expand Down Expand Up @@ -162,6 +163,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
add(MangaKeyer())
add(MangaCoverKeyer())
add(BufferedSourceFetcher.Factory())
}
crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonSubsamplingImageView
import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
import java.io.InputStream
import java.nio.ByteBuffer
import okio.BufferedSource

/**
* A wrapper view for showing page image.
Expand Down Expand Up @@ -146,14 +145,14 @@ open class ReaderPageImageView @JvmOverloads constructor(
}
}

fun setImage(inputStream: InputStream, isAnimated: Boolean, config: Config) {
fun setImage(source: BufferedSource, isAnimated: Boolean, config: Config) {
this.config = config
if (isAnimated) {
prepareAnimatedImageView()
setAnimatedImage(inputStream, config)
setAnimatedImage(source, config)
} else {
prepareNonAnimatedImageView()
setNonAnimatedImage(inputStream, config)
setNonAnimatedImage(source, config)
}
}

Expand Down Expand Up @@ -262,7 +261,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
}

private fun setNonAnimatedImage(
image: Any,
data: Any,
config: Config,
) = (pageView as? SubsamplingScaleImageView)?.apply {
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
Expand All @@ -283,10 +282,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
},
)

when (image) {
is BitmapDrawable -> setImage(ImageSource.bitmap(image.bitmap))
is InputStream -> setImage(ImageSource.inputStream(image))
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
when (data) {
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream()))
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
}
isVisible = true
}
Expand Down Expand Up @@ -331,18 +330,13 @@ open class ReaderPageImageView @JvmOverloads constructor(
}

private fun setAnimatedImage(
image: Any,
data: Any,
config: Config,
) = (pageView as? AppCompatImageView)?.apply {
if (this is PhotoView) {
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)
.data(data)
.memoryCachePolicy(CachePolicy.DISABLED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import logcat.LogPriority
import okio.Buffer
import okio.BufferedSource
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.InputStream

/**
* View of the ViewPager that contains a page of a chapter.
Expand Down Expand Up @@ -139,38 +138,30 @@ class PagerPageHolder(
val streamFn = page.stream ?: return

try {
val (bais, isAnimated, background) = withIOContext {
streamFn().buffered(16).use { stream ->
process(item, stream).use { itemStream ->
val bais = ByteArrayInputStream(itemStream.readBytes())
val isAnimated = ImageUtil.isAnimatedAndSupported(bais)
bais.reset()
val background = if (!isAnimated && viewer.config.automaticBackground) {
ImageUtil.chooseBackground(context, bais)
} else {
null
}
bais.reset()
Triple(bais, isAnimated, background)
}
val (source, isAnimated, background) = withIOContext {
val source = streamFn().use { process(item, Buffer().readFrom(it)) }
val isAnimated = ImageUtil.isAnimatedAndSupported(source)
val background = if (!isAnimated && viewer.config.automaticBackground) {
ImageUtil.chooseBackground(context, source.peek().inputStream())
} else {
null
}
Triple(source, isAnimated, background)
}
withUIContext {
bais.use {
setImage(
it,
isAnimated,
Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = viewer.config.imageScaleType,
cropBorders = viewer.config.imageCropBorders,
zoomStartPosition = viewer.config.imageZoomType,
landscapeZoom = viewer.config.landscapeZoom,
),
)
if (!isAnimated) {
pageBackground = background
}
setImage(
source,
isAnimated,
Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
minimumScaleType = viewer.config.imageScaleType,
cropBorders = viewer.config.imageCropBorders,
zoomStartPosition = viewer.config.imageZoomType,
landscapeZoom = viewer.config.landscapeZoom,
),
)
if (!isAnimated) {
pageBackground = background
}
removeErrorLayout()
}
Expand All @@ -182,40 +173,40 @@ class PagerPageHolder(
}
}

private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
private fun process(page: ReaderPage, imageSource: BufferedSource): BufferedSource {
if (viewer.config.dualPageRotateToFit) {
return rotateDualPage(imageStream)
return rotateDualPage(imageSource)
}

if (!viewer.config.dualPageSplit) {
return imageStream
return imageSource
}

if (page is InsertPage) {
return splitInHalf(imageStream)
return splitInHalf(imageSource)
}

val isDoublePage = ImageUtil.isWideImage(imageStream)
val isDoublePage = ImageUtil.isWideImage(imageSource)
if (!isDoublePage) {
return imageStream
return imageSource
}

onPageSplit(page)

return splitInHalf(imageStream)
return splitInHalf(imageSource)
}

private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
val isDoublePage = ImageUtil.isWideImage(imageStream)
private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
val isDoublePage = ImageUtil.isWideImage(imageSource)
return if (isDoublePage) {
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
ImageUtil.rotateImage(imageStream, rotation)
ImageUtil.rotateImage(imageSource, rotation)
} else {
imageStream
imageSource
}
}

private fun splitInHalf(imageStream: InputStream): InputStream {
private fun splitInHalf(imageSource: BufferedSource): BufferedSource {
var side = when {
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
Expand All @@ -231,7 +222,7 @@ class PagerPageHolder(
}
}

return ImageUtil.splitInHalf(imageStream, side)
return ImageUtil.splitInHalf(imageSource, side)
}

private fun onPageSplit(page: ReaderPage) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.suspendCancellableCoroutine
import logcat.LogPriority
import okio.Buffer
import okio.BufferedSource
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import java.io.BufferedInputStream
import java.io.InputStream

/**
* Holder of the webtoon reader for a single page of a chapter.
Expand Down Expand Up @@ -188,16 +187,14 @@ class WebtoonPageHolder(
val streamFn = page?.stream ?: return

try {
val (openStream, isAnimated) = withIOContext {
val stream = streamFn().buffered(16)
val openStream = process(stream)

val isAnimated = ImageUtil.isAnimatedAndSupported(stream)
Pair(openStream, isAnimated)
val (source, isAnimated) = withIOContext {
val source = streamFn().use { process(Buffer().readFrom(it)) }
val isAnimated = ImageUtil.isAnimatedAndSupported(source)
Pair(source, isAnimated)
}
withUIContext {
frame.setImage(
openStream,
source,
isAnimated,
ReaderPageImageView.Config(
zoomDuration = viewer.config.doubleTapAnimDuration,
Expand All @@ -207,10 +204,6 @@ class WebtoonPageHolder(
)
removeErrorLayout()
}
// Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled
suspendCancellableCoroutine<Nothing> { continuation ->
continuation.invokeOnCancellation { openStream.close() }
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext {
Expand All @@ -219,29 +212,29 @@ class WebtoonPageHolder(
}
}

private fun process(imageStream: BufferedInputStream): InputStream {
private fun process(imageSource: BufferedSource): BufferedSource {
if (viewer.config.dualPageRotateToFit) {
return rotateDualPage(imageStream)
return rotateDualPage(imageSource)
}

if (viewer.config.dualPageSplit) {
val isDoublePage = ImageUtil.isWideImage(imageStream)
val isDoublePage = ImageUtil.isWideImage(imageSource)
if (isDoublePage) {
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
return ImageUtil.splitAndMerge(imageStream, upperSide)
return ImageUtil.splitAndMerge(imageSource, upperSide)
}
}

return imageStream
return imageSource
}

private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
val isDoublePage = ImageUtil.isWideImage(imageStream)
private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
val isDoublePage = ImageUtil.isWideImage(imageSource)
return if (isDoublePage) {
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
ImageUtil.rotateImage(imageStream, rotation)
ImageUtil.rotateImage(imageSource, rotation)
} else {
imageStream
imageSource
}
}

Expand Down
Loading

0 comments on commit b152e38

Please sign in to comment.