diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 161e5bdc9..907858af1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -38,6 +38,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 @@ -208,6 +209,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy)) add(MangaKeyer()) add(MangaCoverKeyer()) + add(BufferedSourceFetcher.Factory()) // SY --> add(PagePreviewKeyer()) add(PagePreviewFetcher.Factory(callFactoryLazy)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt new file mode 100644 index 000000000..4bee925ed --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/BufferedSourceFetcher.kt @@ -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 { + + override fun create( + data: BufferedSource, + options: Options, + imageLoader: ImageLoader, + ): Fetcher { + return BufferedSourceFetcher(data, options) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 38ffb1ace..e799afe58 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -1141,7 +1141,7 @@ class ReaderViewModel @JvmOverloads constructor( return imageSaver.save( image = Image.Page( - inputStream = { ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, 0, bg) }, + inputStream = { ImageUtil.mergeBitmaps(imageBitmap, imageBitmap2, isLTR, 0, bg).inputStream() }, name = filename, location = location, ), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index 91045613d..619978077 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -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. @@ -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 if (isAnimated) { prepareAnimatedImageView() - setAnimatedImage(inputStream, config) + setAnimatedImage(source, config) } else { prepareNonAnimatedImageView() - setNonAnimatedImage(inputStream, config) + setNonAnimatedImage(source, config) } } @@ -256,7 +255,7 @@ open class ReaderPageImageView @JvmOverloads constructor( } private fun setNonAnimatedImage( - image: Any, + data: Any, config: Config, ) = (pageView as? SubsamplingScaleImageView)?.apply { setDoubleTapZoomDuration(config.zoomDuration.getSystemScaledDuration()) @@ -277,10 +276,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 } @@ -325,18 +324,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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 4f5636779..0400fb16e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -19,15 +19,14 @@ 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 tachiyomi.decoder.ImageDecoder -import java.io.BufferedInputStream -import java.io.ByteArrayInputStream -import java.io.InputStream import kotlin.math.max /** @@ -159,53 +158,45 @@ class PagerPageHolder( val streamFn2 = extraPage?.stream try { - val (bais, isAnimated, background) = withIOContext { - streamFn().buffered(16).use { stream -> + val (source, isAnimated, background) = withIOContext { + streamFn().buffered(16).use { source -> // SY --> - ( - if (extraPage != null) { - streamFn2?.invoke() - ?.buffered(16) + if (extraPage != null) { + streamFn2?.invoke() + ?.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 { null } - ).use { stream2 -> - 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) - } - } + Triple(itemSource, 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() } @@ -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) { - 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 mergePages(imageStream: InputStream, imageStream2: InputStream?): InputStream { + private fun mergePages(imageSource: BufferedSource, imageSource2: BufferedSource?): BufferedSource { // Handle adding a center margin to wide images if requested - if (imageStream2 == null) { - return if (imageStream is BufferedInputStream && - !ImageUtil.isAnimatedAndSupported(imageStream) && - ImageUtil.isWideImage(imageStream) && + if (imageSource2 == null) { + return if ( + !ImageUtil.isAnimatedAndSupported(imageSource) && + ImageUtil.isWideImage(imageSource) && viewer.config.centerMarginType and PagerConfig.CenterMarginType.WIDE_PAGE_CENTER_MARGIN > 0 && !viewer.config.imageCropBorders ) { - ImageUtil.addHorizontalCenterMargin(imageStream, height, context) + ImageUtil.addHorizontalCenterMargin(imageSource, height, context) } else { - imageStream + imageSource } } - if (page.fullPage) return imageStream - if (ImageUtil.isAnimatedAndSupported(imageStream)) { + if (page.fullPage) return imageSource + if (ImageUtil.isAnimatedAndSupported(imageSource)) { page.fullPage = true splitDoublePages() - return imageStream - } else if (ImageUtil.isAnimatedAndSupported(imageStream2)) { + return imageSource + } else if (ImageUtil.isAnimatedAndSupported(imageSource2)) { page.isolatedPage = true extraPage?.fullPage = true splitDoublePages() - return imageStream + return imageSource } - val imageBytes = imageStream.readBytes() + val imageBitmap = try { - ImageDecoder.newInstance(imageBytes.inputStream())?.decode() + ImageDecoder.newInstance(imageSource.inputStream())?.decode() } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Cannot combine pages" } null } if (imageBitmap == null) { - imageStream2.close() - imageStream.close() + imageSource2.close() page.fullPage = true splitDoublePages() logcat(LogPriority.ERROR) { "Cannot combine pages" } - return imageBytes.inputStream() + return imageSource } scope.launch { progressIndicator.setProgress(96) } val height = imageBitmap.height val width = imageBitmap.width if (height < width) { - imageStream2.close() - imageStream.close() + imageSource2.close() page.fullPage = true splitDoublePages() - return imageBytes.inputStream() + return imageSource } - val imageBytes2 = imageStream2.readBytes() val imageBitmap2 = try { - ImageDecoder.newInstance(imageBytes2.inputStream())?.decode() + ImageDecoder.newInstance(imageSource2.inputStream())?.decode() } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Cannot combine pages" } null } if (imageBitmap2 == null) { - imageStream2.close() - imageStream.close() + imageSource2.close() extraPage?.fullPage = true page.isolatedPage = true splitDoublePages() logcat(LogPriority.ERROR) { "Cannot combine pages" } - return imageBytes.inputStream() + return imageSource } scope.launch { progressIndicator.setProgress(97) } val height2 = imageBitmap2.height val width2 = imageBitmap2.width if (height2 < width2) { - imageStream2.close() - imageStream.close() + imageSource2.close() extraPage?.fullPage = true page.isolatedPage = true splitDoublePages() - return imageBytes.inputStream() + return imageSource } val isLTR = (viewer !is R2LPagerViewer) xor viewer.config.invertDoublePages - imageStream.close() - imageStream2.close() + imageSource.close() + imageSource2.close() 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) @@ -363,7 +349,7 @@ class PagerPageHolder( } } - 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 @@ -387,7 +373,7 @@ class PagerPageHolder( 0 } - return ImageUtil.splitInHalf(imageStream, side, sideMargin) + return ImageUtil.splitInHalf(imageSource, side, sideMargin) } private fun onPageSplit(page: ReaderPage) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index d57ade4d9..914b732a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -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 { 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 } } diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt index 39bb27cfb..8602fba94 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt @@ -26,11 +26,10 @@ import androidx.core.graphics.red import androidx.exifinterface.media.ExifInterface import com.hippo.unifile.UniFile import logcat.LogPriority +import okio.Buffer +import okio.BufferedSource import tachiyomi.decoder.Format import tachiyomi.decoder.ImageDecoder -import java.io.BufferedInputStream -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream import java.net.URLConnection @@ -83,9 +82,9 @@ object ImageUtil { ?: "jpg" } - fun isAnimatedAndSupported(stream: InputStream): Boolean { + fun isAnimatedAndSupported(source: BufferedSource): Boolean { 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 when (type.format) { Format.Gif -> true @@ -132,18 +131,16 @@ object ImageUtil { * * @return true if the width is greater than the height */ - fun isWideImage(imageStream: BufferedInputStream): Boolean { - val options = extractImageOptions(imageStream) + fun isWideImage(imageSource: BufferedSource): Boolean { + val options = extractImageOptions(imageSource) 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 { - val imageBytes = imageStream.readBytes() - - val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + fun splitInHalf(imageSource: BufferedSource, side: Side, sidePadding: Int): BufferedSource { + val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val height = imageBitmap.height val width = imageBitmap.width @@ -157,22 +154,20 @@ object ImageUtil { half.applyCanvas { drawBitmap(imageBitmap, part, singlePage, null) } - val output = ByteArrayOutputStream() - half.compress(Bitmap.CompressFormat.JPEG, 100, output) + val output = Buffer() + half.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) - return ByteArrayInputStream(output.toByteArray()) + return output } - fun rotateImage(imageStream: InputStream, degrees: Float): InputStream { - val imageBytes = imageStream.readBytes() - - val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + fun rotateImage(imageSource: BufferedSource, degrees: Float): BufferedSource { + val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val rotated = rotateBitMap(imageBitmap, degrees) - val output = ByteArrayOutputStream() - rotated.compress(Bitmap.CompressFormat.JPEG, 100, output) + val output = Buffer() + rotated.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) - return ByteArrayInputStream(output.toByteArray()) + return output } 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 * new vertically-aligned image. */ - fun splitAndMerge(imageStream: InputStream, upperSide: Side): InputStream { - val imageBytes = imageStream.readBytes() - - val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) + fun splitAndMerge(imageSource: BufferedSource, upperSide: Side): BufferedSource { + val imageBitmap = BitmapFactory.decodeStream(imageSource.inputStream()) val height = imageBitmap.height val width = imageBitmap.width @@ -209,9 +202,9 @@ object ImageUtil { drawBitmap(imageBitmap, leftPart, bottomPart, null) } - val output = ByteArrayOutputStream() - result.compress(Bitmap.CompressFormat.JPEG, 100, output) - return ByteArrayInputStream(output.toByteArray()) + val output = Buffer() + result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) + return output } enum class Side { @@ -225,8 +218,8 @@ object ImageUtil { * to compensate for scaling. */ - fun addHorizontalCenterMargin(imageStream: InputStream, viewHeight: Int, backgroundContext: Context): InputStream { - val imageBitmap = ImageDecoder.newInstance(imageStream)?.decode()!! + fun addHorizontalCenterMargin(imageSource: BufferedSource, viewHeight: Int, backgroundContext: Context): BufferedSource { + val imageBitmap = ImageDecoder.newInstance(imageSource.inputStream())?.decode()!! val height = imageBitmap.height val width = imageBitmap.width @@ -237,7 +230,7 @@ object ImageUtil { val leftTargetPart = Rect(0, 0, width / 2, 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) val result = createBitmap(width + centerPadding, height) @@ -247,9 +240,9 @@ object ImageUtil { bgColor.draw(this) } - val output = ByteArrayOutputStream() - result.compress(Bitmap.CompressFormat.JPEG, 100, output) - return ByteArrayInputStream(output.toByteArray()) + val output = Buffer() + result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) + return output } // SY <-- @@ -258,11 +251,8 @@ object ImageUtil { * * @return true if the height:width ratio is greater than 3. */ - private fun isTallImage(imageStream: InputStream): Boolean { - val options = extractImageOptions( - imageStream, - resetAfterExtraction = false, - ) + private fun isTallImage(imageSource: BufferedSource): Boolean { + val options = extractImageOptions(imageSource) return (options.outHeight / options.outWidth) > 3 } @@ -275,22 +265,18 @@ object ImageUtil { imageFile: UniFile, filenamePrefix: String, ): Boolean { - if (isAnimatedAndSupported(imageFile.openInputStream()) || - !isTallImage(imageFile.openInputStream()) - ) { + val imageSource = imageFile.openInputStream().use { Buffer().readFrom(it) } + if (isAnimatedAndSupported(imageSource) || !isTallImage(imageSource)) { return true } - val bitmapRegionDecoder = getBitmapRegionDecoder(imageFile.openInputStream()) + val bitmapRegionDecoder = getBitmapRegionDecoder(imageSource.peek().inputStream()) if (bitmapRegionDecoder == null) { logcat { "Failed to create new instance of BitmapRegionDecoder" } return false } - val options = extractImageOptions( - imageFile.openInputStream(), - resetAfterExtraction = false, - ).apply { + val options = extractImageOptions(imageSource).apply { inJustDecodeBounds = false } @@ -380,8 +366,8 @@ object ImageUtil { /** * Algorithm for determining what background to accompany a comic/manga page */ - fun chooseBackground(context: Context, imageStream: InputStream): Drawable { - val decoder = ImageDecoder.newInstance(imageStream) + fun chooseBackground(context: Context, imageSource: BufferedSource): Drawable { + val decoder = ImageDecoder.newInstance(imageSource.inputStream()) val image = decoder?.decode() decoder?.recycle() @@ -603,16 +589,9 @@ object ImageUtil { /** * Used to check an image's dimensions without loading it in the memory. */ - private fun extractImageOptions( - imageStream: InputStream, - resetAfterExtraction: Boolean = true, - ): BitmapFactory.Options { - imageStream.mark(Int.MAX_VALUE) - - val imageBytes = imageStream.readBytes() + private fun extractImageOptions(imageSource: BufferedSource): BitmapFactory.Options { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) - if (resetAfterExtraction) imageStream.reset() + BitmapFactory.decodeStream(imageSource.peek().inputStream(), null, options) return options } @@ -657,7 +636,7 @@ object ImageUtil { centerMargin: Int, @ColorInt background: Int = Color.WHITE, progressCallback: ((Int) -> Unit)? = null, - ): ByteArrayInputStream { + ): BufferedSource { val height = imageBitmap.height val width = imageBitmap.width val height2 = imageBitmap2.height @@ -687,10 +666,10 @@ object ImageUtil { canvas.drawBitmap(imageBitmap2, imageBitmap2.rect, bottomPart, null) progressCallback?.invoke(99) - val output = ByteArrayOutputStream() - result.compress(Bitmap.CompressFormat.JPEG, 100, output) + val output = Buffer() + result.compress(Bitmap.CompressFormat.JPEG, 100, output.outputStream()) progressCallback?.invoke(100) - return ByteArrayInputStream(output.toByteArray()) + return output } private val Bitmap.rect: Rect