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:
FooIbar 2024-04-20 12:52:40 +08:00 committed by Jobobby04
parent 5895e78b39
commit aeeff72bed
7 changed files with 181 additions and 189 deletions

View File

@ -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))

View File

@ -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)
}
}
}

View File

@ -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,
), ),

View File

@ -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)

View File

@ -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,53 +158,45 @@ 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 {
null
}.use { source2 ->
val itemSource = if (viewer.config.dualPageSplit) {
process(item.first, Buffer().readFrom(source))
} else {
mergePages(Buffer().readFrom(source), source2?.let { Buffer().readFrom(it) })
}
// SY <--
val isAnimated = ImageUtil.isAnimatedAndSupported(itemSource)
val background = if (!isAnimated && viewer.config.automaticBackground) {
ImageUtil.chooseBackground(context, itemSource.peek())
} else { } else {
null null
} }
).use { stream2 -> Triple(itemSource, isAnimated, background)
if (viewer.config.dualPageSplit) { }
process(item.first, stream)
} else {
mergePages(stream, stream2)
}.use { itemStream ->
// SY <--
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)
}
}
} }
} }
withUIContext { withUIContext {
bais.use { setImage(
setImage( source,
it, isAnimated,
isAnimated, Config(
Config( zoomDuration = viewer.config.doubleTapAnimDuration,
zoomDuration = viewer.config.doubleTapAnimDuration, minimumScaleType = viewer.config.imageScaleType,
minimumScaleType = viewer.config.imageScaleType, cropBorders = viewer.config.imageCropBorders,
cropBorders = viewer.config.imageCropBorders, zoomStartPosition = viewer.config.imageZoomType,
zoomStartPosition = viewer.config.imageZoomType, landscapeZoom = viewer.config.landscapeZoom,
landscapeZoom = viewer.config.landscapeZoom, ),
), )
) if (!isAnimated) {
if (!isAnimated) { pageBackground = background
pageBackground = background
}
} }
removeErrorLayout() removeErrorLayout()
} }
@ -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) {

View File

@ -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
} }
} }

View File

@ -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