Use Okio instead of java.io
for image processing (#691)
(cherry picked from commit b152e3881bffd9050a8a0ed4030823886e3fe04f) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/App.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt # core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt
This commit is contained in:
parent
5895e78b39
commit
aeeff72bed
@ -38,6 +38,7 @@ import eu.kanade.domain.ui.UiPreferences
|
|||||||
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
|
||||||
import eu.kanade.tachiyomi.crash.CrashActivity
|
import eu.kanade.tachiyomi.crash.CrashActivity
|
||||||
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
|
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.MangaCoverFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
import eu.kanade.tachiyomi.data.coil.MangaKeyer
|
||||||
@ -208,6 +209,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
|||||||
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy))
|
||||||
add(MangaKeyer())
|
add(MangaKeyer())
|
||||||
add(MangaCoverKeyer())
|
add(MangaCoverKeyer())
|
||||||
|
add(BufferedSourceFetcher.Factory())
|
||||||
// SY -->
|
// SY -->
|
||||||
add(PagePreviewKeyer())
|
add(PagePreviewKeyer())
|
||||||
add(PagePreviewFetcher.Factory(callFactoryLazy))
|
add(PagePreviewFetcher.Factory(callFactoryLazy))
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1141,7 +1141,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||||||
|
|
||||||
return imageSaver.save(
|
return imageSaver.save(
|
||||||
image = Image.Page(
|
image = Image.Page(
|
||||||
inputStream = { ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, 0, bg) },
|
inputStream = { ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, 0, bg).inputStream() },
|
||||||
name = filename,
|
name = filename,
|
||||||
location = location,
|
location = location,
|
||||||
),
|
),
|
||||||
|
@ -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.GLUtil
|
||||||
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
|
import eu.kanade.tachiyomi.util.view.isVisibleOnScreen
|
||||||
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.
|
||||||
@ -140,14 +139,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
|
this.config = config
|
||||||
if (isAnimated) {
|
if (isAnimated) {
|
||||||
prepareAnimatedImageView()
|
prepareAnimatedImageView()
|
||||||
setAnimatedImage(inputStream, config)
|
setAnimatedImage(source, config)
|
||||||
} else {
|
} else {
|
||||||
prepareNonAnimatedImageView()
|
prepareNonAnimatedImageView()
|
||||||
setNonAnimatedImage(inputStream, config)
|
setNonAnimatedImage(source, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,7 +255,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setNonAnimatedImage(
|
private fun setNonAnimatedImage(
|
||||||
image: Any,
|
data: Any,
|
||||||
config: Config,
|
config: Config,
|
||||||
) = (pageView as? SubsamplingScaleImageView)?.apply {
|
) = (pageView as? SubsamplingScaleImageView)?.apply {
|
||||||
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
|
setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration())
|
||||||
@ -277,10 +276,10 @@ open class ReaderPageImageView @JvmOverloads constructor(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
when (image) {
|
when (data) {
|
||||||
is BitmapDrawable -> setImage(ImageSource.bitmap(image.bitmap))
|
is BitmapDrawable -> setImage(ImageSource.bitmap(data.bitmap))
|
||||||
is InputStream -> setImage(ImageSource.inputStream(image))
|
is BufferedSource -> setImage(ImageSource.inputStream(data.inputStream()))
|
||||||
else -> throw IllegalArgumentException("Not implemented for class ${image::class.simpleName}")
|
else -> throw IllegalArgumentException("Not implemented for class ${data::class.simpleName}")
|
||||||
}
|
}
|
||||||
isVisible = true
|
isVisible = true
|
||||||
}
|
}
|
||||||
@ -325,18 +324,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)
|
||||||
|
@ -19,15 +19,14 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.BufferedSource
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -159,41 +158,34 @@ class PagerPageHolder(
|
|||||||
val streamFn2 = extraPage?.stream
|
val streamFn2 = extraPage?.stream
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (bais, isAnimated, background) = withIOContext {
|
val (source, isAnimated, background) = withIOContext {
|
||||||
streamFn().buffered(16).use { stream ->
|
streamFn().buffered(16).use { source ->
|
||||||
// SY -->
|
// SY -->
|
||||||
(
|
|
||||||
if (extraPage != null) {
|
if (extraPage != null) {
|
||||||
streamFn2?.invoke()
|
streamFn2?.invoke()
|
||||||
?.buffered(16)
|
?.buffered(16)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}.use { source2 ->
|
||||||
).use { stream2 ->
|
val itemSource = if (viewer.config.dualPageSplit) {
|
||||||
if (viewer.config.dualPageSplit) {
|
process(item.first, Buffer().readFrom(source))
|
||||||
process(item.first, stream)
|
|
||||||
} else {
|
} else {
|
||||||
mergePages(stream, stream2)
|
mergePages(Buffer().readFrom(source), source2?.let { Buffer().readFrom(it) })
|
||||||
}.use { itemStream ->
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
val bais = ByteArrayInputStream(itemStream.readBytes())
|
val isAnimated = ImageUtil.isAnimatedAndSupported(itemSource)
|
||||||
val isAnimated = ImageUtil.isAnimatedAndSupported(bais)
|
|
||||||
bais.reset()
|
|
||||||
val background = if (!isAnimated && viewer.config.automaticBackground) {
|
val background = if (!isAnimated && viewer.config.automaticBackground) {
|
||||||
ImageUtil.chooseBackground(context, bais)
|
ImageUtil.chooseBackground(context, itemSource.peek())
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
bais.reset()
|
Triple(itemSource, isAnimated, background)
|
||||||
Triple(bais, isAnimated, background)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
withUIContext {
|
withUIContext {
|
||||||
bais.use {
|
|
||||||
setImage(
|
setImage(
|
||||||
it,
|
source,
|
||||||
isAnimated,
|
isAnimated,
|
||||||
Config(
|
Config(
|
||||||
zoomDuration = viewer.config.doubleTapAnimDuration,
|
zoomDuration = viewer.config.doubleTapAnimDuration,
|
||||||
@ -206,7 +198,6 @@ class PagerPageHolder(
|
|||||||
if (!isAnimated) {
|
if (!isAnimated) {
|
||||||
pageBackground = background
|
pageBackground = background
|
||||||
}
|
}
|
||||||
}
|
|
||||||
removeErrorLayout()
|
removeErrorLayout()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@ -217,124 +208,119 @@ class PagerPageHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
|
private fun process(page: ReaderPage, imageSource: BufferedSource): BufferedSource {
|
||||||
if (viewer.config.dualPageRotateToFit) {
|
if (viewer.config.dualPageRotateToFit) {
|
||||||
return rotateDualPage(imageStream)
|
return rotateDualPage(imageSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!viewer.config.dualPageSplit) {
|
if (!viewer.config.dualPageSplit) {
|
||||||
return imageStream
|
return imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page is InsertPage) {
|
if (page is InsertPage) {
|
||||||
return splitInHalf(imageStream)
|
return splitInHalf(imageSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||||
if (!isDoublePage) {
|
if (!isDoublePage) {
|
||||||
return imageStream
|
return imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageSplit(page)
|
onPageSplit(page)
|
||||||
|
|
||||||
return splitInHalf(imageStream)
|
return splitInHalf(imageSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rotateDualPage(imageStream: BufferedInputStream): InputStream {
|
private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
|
||||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||||
return if (isDoublePage) {
|
return if (isDoublePage) {
|
||||||
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
|
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
|
||||||
ImageUtil.rotateImage(imageStream, rotation)
|
ImageUtil.rotateImage(imageSource, rotation)
|
||||||
} else {
|
} else {
|
||||||
imageStream
|
imageSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream {
|
private fun mergePages(imageSource: BufferedSource, imageSource2: BufferedSource?): BufferedSource {
|
||||||
// Handle adding a center margin to wide images if requested
|
// Handle adding a center margin to wide images if requested
|
||||||
if (imageStream2 == null) {
|
if (imageSource2 == null) {
|
||||||
return if (imageStream is BufferedInputStream &&
|
return if (
|
||||||
!ImageUtil.isAnimatedAndSupported(imageStream) &&
|
!ImageUtil.isAnimatedAndSupported(imageSource) &&
|
||||||
ImageUtil.isWideImage(imageStream) &&
|
ImageUtil.isWideImage(imageSource) &&
|
||||||
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
|
viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 &&
|
||||||
!viewer.config.imageCropBorders
|
!viewer.config.imageCropBorders
|
||||||
) {
|
) {
|
||||||
ImageUtil.addHorizontalCenterMargin(imageStream, height, context)
|
ImageUtil.addHorizontalCenterMargin(imageSource, height, context)
|
||||||
} else {
|
} else {
|
||||||
imageStream
|
imageSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.fullPage) return imageStream
|
if (page.fullPage) return imageSource
|
||||||
if (ImageUtil.isAnimatedAndSupported(imageStream)) {
|
if (ImageUtil.isAnimatedAndSupported(imageSource)) {
|
||||||
page.fullPage = true
|
page.fullPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
return imageStream
|
return imageSource
|
||||||
} else if (ImageUtil.isAnimatedAndSupported(imageStream2)) {
|
} else if (ImageUtil.isAnimatedAndSupported(imageSource2)) {
|
||||||
page.isolatedPage = true
|
page.isolatedPage = true
|
||||||
extraPage?.fullPage = true
|
extraPage?.fullPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
return imageStream
|
return imageSource
|
||||||
}
|
}
|
||||||
val imageBytes = imageStream.readBytes()
|
|
||||||
val imageBitmap = try {
|
val imageBitmap = try {
|
||||||
ImageDecoder.newInstance(imageBytes.inputStream())?.decode()
|
ImageDecoder.newInstance(imageSource.inputStream())?.decode()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
if (imageBitmap == null) {
|
if (imageBitmap == null) {
|
||||||
imageStream2.close()
|
imageSource2.close()
|
||||||
imageStream.close()
|
|
||||||
page.fullPage = true
|
page.fullPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageBytes.inputStream()
|
return imageSource
|
||||||
}
|
}
|
||||||
scope.launch { progressIndicator.setProgress(96) }
|
scope.launch { progressIndicator.setProgress(96) }
|
||||||
val height = imageBitmap.height
|
val height = imageBitmap.height
|
||||||
val width = imageBitmap.width
|
val width = imageBitmap.width
|
||||||
|
|
||||||
if (height < width) {
|
if (height < width) {
|
||||||
imageStream2.close()
|
imageSource2.close()
|
||||||
imageStream.close()
|
|
||||||
page.fullPage = true
|
page.fullPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
return imageBytes.inputStream()
|
return imageSource
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageBytes2 = imageStream2.readBytes()
|
|
||||||
val imageBitmap2 = try {
|
val imageBitmap2 = try {
|
||||||
ImageDecoder.newInstance(imageBytes2.inputStream())?.decode()
|
ImageDecoder.newInstance(imageSource2.inputStream())?.decode()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
if (imageBitmap2 == null) {
|
if (imageBitmap2 == null) {
|
||||||
imageStream2.close()
|
imageSource2.close()
|
||||||
imageStream.close()
|
|
||||||
extraPage?.fullPage = true
|
extraPage?.fullPage = true
|
||||||
page.isolatedPage = true
|
page.isolatedPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
logcat(LogPriority.ERROR) { "Cannot combine pages" }
|
||||||
return imageBytes.inputStream()
|
return imageSource
|
||||||
}
|
}
|
||||||
scope.launch { progressIndicator.setProgress(97) }
|
scope.launch { progressIndicator.setProgress(97) }
|
||||||
val height2 = imageBitmap2.height
|
val height2 = imageBitmap2.height
|
||||||
val width2 = imageBitmap2.width
|
val width2 = imageBitmap2.width
|
||||||
|
|
||||||
if (height2 < width2) {
|
if (height2 < width2) {
|
||||||
imageStream2.close()
|
imageSource2.close()
|
||||||
imageStream.close()
|
|
||||||
extraPage?.fullPage = true
|
extraPage?.fullPage = true
|
||||||
page.isolatedPage = true
|
page.isolatedPage = true
|
||||||
splitDoublePages()
|
splitDoublePages()
|
||||||
return imageBytes.inputStream()
|
return imageSource
|
||||||
}
|
}
|
||||||
val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages
|
val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages
|
||||||
|
|
||||||
imageStream.close()
|
imageSource.close()
|
||||||
imageStream2.close()
|
imageSource2.close()
|
||||||
|
|
||||||
val centerMargin = if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) {
|
val centerMargin = if (viewer.config.centerMarginType and PagerConfig.CenterMarginType.DOUBLE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders) {
|
||||||
96 / (this.height.coerceAtLeast(1) / max(height, height2).coerceAtLeast(1)).coerceAtLeast(1)
|
96 / (this.height.coerceAtLeast(1) / max(height, height2).coerceAtLeast(1)).coerceAtLeast(1)
|
||||||
@ -363,7 +349,7 @@ class PagerPageHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun splitInHalf(imageStream: InputStream): InputStream {
|
private fun splitInHalf(imageSource: BufferedSource): BufferedSource {
|
||||||
var side = when {
|
var side = when {
|
||||||
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
|
viewer is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.RIGHT
|
||||||
viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
|
viewer !is L2RPagerViewer && page is InsertPage -> ImageUtil.Side.LEFT
|
||||||
@ -387,7 +373,7 @@ class PagerPageHolder(
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
return ImageUtil.splitInHalf(imageStream, side, sideMargin)
|
return ImageUtil.splitInHalf(imageSource, side, sideMargin)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPageSplit(page: ReaderPage) {
|
private fun onPageSplit(page: ReaderPage) {
|
||||||
|
@ -22,15 +22,14 @@ 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 logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.BufferedSource
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.lang.withUIContext
|
import tachiyomi.core.common.util.lang.withUIContext
|
||||||
import tachiyomi.core.common.util.system.ImageUtil
|
import tachiyomi.core.common.util.system.ImageUtil
|
||||||
import tachiyomi.core.common.util.system.logcat
|
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.
|
* Holder of the webtoon reader for a single page of a chapter.
|
||||||
@ -188,16 +187,14 @@ class WebtoonPageHolder(
|
|||||||
val streamFn = page?.stream ?: return
|
val streamFn = page?.stream ?: return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (openStream, isAnimated) = withIOContext {
|
val (source, isAnimated) = 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)
|
|
||||||
}
|
}
|
||||||
withUIContext {
|
withUIContext {
|
||||||
frame.setImage(
|
frame.setImage(
|
||||||
openStream,
|
source,
|
||||||
isAnimated,
|
isAnimated,
|
||||||
ReaderPageImageView.Config(
|
ReaderPageImageView.Config(
|
||||||
zoomDuration = viewer.config.doubleTapAnimDuration,
|
zoomDuration = viewer.config.doubleTapAnimDuration,
|
||||||
@ -207,10 +204,6 @@ class WebtoonPageHolder(
|
|||||||
)
|
)
|
||||||
removeErrorLayout()
|
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) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
withUIContext {
|
withUIContext {
|
||||||
@ -219,29 +212,29 @@ class WebtoonPageHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun process(imageStream: BufferedInputStream): InputStream {
|
private fun process(imageSource: BufferedSource): BufferedSource {
|
||||||
if (viewer.config.dualPageRotateToFit) {
|
if (viewer.config.dualPageRotateToFit) {
|
||||||
return rotateDualPage(imageStream)
|
return rotateDualPage(imageSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewer.config.dualPageSplit) {
|
if (viewer.config.dualPageSplit) {
|
||||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||||
if (isDoublePage) {
|
if (isDoublePage) {
|
||||||
val upperSide = if (viewer.config.dualPageInvert) ImageUtil.Side.LEFT else ImageUtil.Side.RIGHT
|
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 {
|
private fun rotateDualPage(imageSource: BufferedSource): BufferedSource {
|
||||||
val isDoublePage = ImageUtil.isWideImage(imageStream)
|
val isDoublePage = ImageUtil.isWideImage(imageSource)
|
||||||
return if (isDoublePage) {
|
return if (isDoublePage) {
|
||||||
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
|
val rotation = if (viewer.config.dualPageRotateToFitInvert) -90f else 90f
|
||||||
ImageUtil.rotateImage(imageStream, rotation)
|
ImageUtil.rotateImage(imageSource, rotation)
|
||||||
} else {
|
} else {
|
||||||
imageStream
|
imageSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,11 +26,10 @@ import androidx.core.graphics.red
|
|||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.BufferedSource
|
||||||
import tachiyomi.decoder.Format
|
import tachiyomi.decoder.Format
|
||||||
import tachiyomi.decoder.ImageDecoder
|
import tachiyomi.decoder.ImageDecoder
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
@ -83,9 +82,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
|
||||||
@ -132,18 +131,16 @@ 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)
|
||||||
return options.outWidth > options.outHeight
|
return options.outWidth > options.outHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the 'side' part from imageStream and return it as InputStream.
|
* Extract the 'side' part from [BufferedSource] and return it as [BufferedSource].
|
||||||
*/
|
*/
|
||||||
fun splitInHalf(imageStream: InputStream, side: Side, sidePadding: Int): InputStream {
|
fun splitInHalf(imageSource: BufferedSource, side: Side, sidePadding: Int): 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
|
||||||
|
|
||||||
@ -157,22 +154,20 @@ object ImageUtil {
|
|||||||
half.applyCanvas {
|
half.applyCanvas {
|
||||||
drawBitmap(imageBitmap, part, singlePage, null)
|
drawBitmap(imageBitmap, part, singlePage, null)
|
||||||
}
|
}
|
||||||
val output = ByteArrayOutputStream()
|
val output = Buffer()
|
||||||
half.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
half.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
|
||||||
|
|
||||||
return ByteArrayInputStream(output.toByteArray())
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rotateImage(imageStream: InputStream, degrees: Float): InputStream {
|
fun rotateImage(imageSource: BufferedSource, degrees: Float): BufferedSource {
|
||||||
val imageBytes = imageStream.readBytes()
|
val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream())
|
||||||
|
|
||||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
||||||
val rotated = rotateBitMap(imageBitmap, degrees)
|
val rotated = rotateBitMap(imageBitmap, degrees)
|
||||||
|
|
||||||
val output = ByteArrayOutputStream()
|
val output = Buffer()
|
||||||
rotated.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
rotated.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
|
||||||
|
|
||||||
return ByteArrayInputStream(output.toByteArray())
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rotateBitMap(bitmap: Bitmap, degrees: Float): Bitmap {
|
private fun rotateBitMap(bitmap: Bitmap, degrees: Float): Bitmap {
|
||||||
@ -184,10 +179,8 @@ object ImageUtil {
|
|||||||
* Split the image into left and right parts, then merge them into a
|
* Split the image into left and right parts, then merge them into a
|
||||||
* new vertically-aligned image.
|
* new vertically-aligned image.
|
||||||
*/
|
*/
|
||||||
fun splitAndMerge(imageStream: InputStream, upperSide: Side): InputStream {
|
fun splitAndMerge(imageSource: BufferedSource, upperSide: Side): 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
|
||||||
|
|
||||||
@ -209,9 +202,9 @@ object ImageUtil {
|
|||||||
drawBitmap(imageBitmap, leftPart, bottomPart, null)
|
drawBitmap(imageBitmap, leftPart, bottomPart, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val output = ByteArrayOutputStream()
|
val output = Buffer()
|
||||||
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
|
||||||
return ByteArrayInputStream(output.toByteArray())
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Side {
|
enum class Side {
|
||||||
@ -225,8 +218,8 @@ object ImageUtil {
|
|||||||
* to compensate for scaling.
|
* to compensate for scaling.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun addHorizontalCenterMargin(imageStream: InputStream, viewHeight: Int, backgroundContext: Context): InputStream {
|
fun addHorizontalCenterMargin(imageSource: BufferedSource, viewHeight: Int, backgroundContext: Context): BufferedSource {
|
||||||
val imageBitmap = ImageDecoder.newInstance(imageStream)?.decode()!!
|
val imageBitmap = ImageDecoder.newInstance(imageSource.inputStream())?.decode()!!
|
||||||
val height = imageBitmap.height
|
val height = imageBitmap.height
|
||||||
val width = imageBitmap.width
|
val width = imageBitmap.width
|
||||||
|
|
||||||
@ -237,7 +230,7 @@ object ImageUtil {
|
|||||||
val leftTargetPart = Rect(0, 0, width / 2, height)
|
val leftTargetPart = Rect(0, 0, width / 2, height)
|
||||||
val rightTargetPart = Rect(width / 2 + centerPadding, 0, width + centerPadding, height)
|
val rightTargetPart = Rect(width / 2 + centerPadding, 0, width + centerPadding, height)
|
||||||
|
|
||||||
val bgColor = chooseBackground(backgroundContext, imageStream)
|
val bgColor = chooseBackground(backgroundContext, imageSource)
|
||||||
bgColor.setBounds(width / 2, 0, width / 2 + centerPadding, height)
|
bgColor.setBounds(width / 2, 0, width / 2 + centerPadding, height)
|
||||||
val result = createBitmap(width + centerPadding, height)
|
val result = createBitmap(width + centerPadding, height)
|
||||||
|
|
||||||
@ -247,9 +240,9 @@ object ImageUtil {
|
|||||||
bgColor.draw(this)
|
bgColor.draw(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val output = ByteArrayOutputStream()
|
val output = Buffer()
|
||||||
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream())
|
||||||
return ByteArrayInputStream(output.toByteArray())
|
return output
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
@ -258,11 +251,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(
|
val options = extractImageOptions(imageSource)
|
||||||
imageStream,
|
|
||||||
resetAfterExtraction = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
return (options.outHeight / options.outWidth) > 3
|
return (options.outHeight / options.outWidth) > 3
|
||||||
}
|
}
|
||||||
@ -275,22 +265,18 @@ object ImageUtil {
|
|||||||
imageFile: UniFile,
|
imageFile: UniFile,
|
||||||
filenamePrefix: String,
|
filenamePrefix: String,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (isAnimatedAndSupported(imageFile.openInputStream()) ||
|
val imageSource = imageFile.openInputStream().use { Buffer().readFrom(it) }
|
||||||
!isTallImage(imageFile.openInputStream())
|
if (isAnimatedAndSupported(imageSource) || !isTallImage(imageSource)) {
|
||||||
) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
val bitmapRegionDecoder = getBitmapRegionDecoder(imageFile.openInputStream())
|
val bitmapRegionDecoder = getBitmapRegionDecoder(imageSource.peek().inputStream())
|
||||||
if (bitmapRegionDecoder == null) {
|
if (bitmapRegionDecoder == null) {
|
||||||
logcat { "Failed to create new instance of BitmapRegionDecoder" }
|
logcat { "Failed to create new instance of BitmapRegionDecoder" }
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val options = extractImageOptions(
|
val options = extractImageOptions(imageSource).apply {
|
||||||
imageFile.openInputStream(),
|
|
||||||
resetAfterExtraction = false,
|
|
||||||
).apply {
|
|
||||||
inJustDecodeBounds = false
|
inJustDecodeBounds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,8 +366,8 @@ object ImageUtil {
|
|||||||
/**
|
/**
|
||||||
* Algorithm for determining what background to accompany a comic/manga page
|
* Algorithm for determining what background to accompany a comic/manga page
|
||||||
*/
|
*/
|
||||||
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
fun chooseBackground(context: Context, imageSource: BufferedSource): Drawable {
|
||||||
val decoder = ImageDecoder.newInstance(imageStream)
|
val decoder = ImageDecoder.newInstance(imageSource.inputStream())
|
||||||
val image = decoder?.decode()
|
val image = decoder?.decode()
|
||||||
decoder?.recycle()
|
decoder?.recycle()
|
||||||
|
|
||||||
@ -603,16 +589,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(Int.MAX_VALUE)
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,7 +636,7 @@ object ImageUtil {
|
|||||||
centerMargin: Int,
|
centerMargin: Int,
|
||||||
@ColorInt background: Int = Color.WHITE,
|
@ColorInt background: Int = Color.WHITE,
|
||||||
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 height2 = imageBitmap2.height
|
val height2 = imageBitmap2.height
|
||||||
@ -687,10 +666,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
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Bitmap.rect: Rect
|
private val Bitmap.rect: Rect
|
||||||
|
Loading…
x
Reference in New Issue
Block a user