mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-19 09:39:43 +02:00
Use Okio instead of java.io
for image processing (#691)
This commit is contained in:
@@ -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
|
||||
@@ -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))
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
@@ -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.
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -231,7 +222,7 @@ class PagerPageHolder(
|
||||
}
|
||||
}
|
||||
|
||||
return ImageUtil.splitInHalf(imageStream, side)
|
||||
return ImageUtil.splitInHalf(imageSource, side)
|
||||
}
|
||||
|
||||
private fun onPageSplit(page: ReaderPage) {
|
||||
|
@@ -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.
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user