From ff32ab09fbc32eff33cb378d6e1b1aa0d9ef8e1a Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 2 Jul 2022 22:49:50 +0600 Subject: [PATCH] Downloader: Optimize split tall image (#7435) --- .../tachiyomi/data/download/Downloader.kt | 7 +- .../kanade/tachiyomi/util/system/ImageUtil.kt | 68 ++++++++++++++----- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 936d2e10b..6ff894442 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -493,7 +493,12 @@ class Downloader( // check if the original page was previously splitted before then skip. if (imageFile.name!!.contains("__")) return true - return ImageUtil.splitTallImage(imageFile, imageFilePath) + return try { + ImageUtil.splitTallImage(imageFile, imageFilePath) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + false + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index b4b11552b..c1d4edfce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -185,7 +185,7 @@ object ImageUtil { * @return true if the height:width ratio is greater than 3. */ private fun isTallImage(imageStream: InputStream): Boolean { - val options = extractImageOptions(imageStream, false) + val options = extractImageOptions(imageStream, resetAfterExtraction = false) return (options.outHeight / options.outWidth) > 3 } @@ -197,16 +197,34 @@ object ImageUtil { return true } - val options = extractImageOptions(imageFile.openInputStream(), false).apply { inJustDecodeBounds = false } + val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false } // Values are stored as they get modified during split loop val imageHeight = options.outHeight val imageWidth = options.outWidth - val splitHeight = getDisplayMaxHeightInPx + val splitHeight = (getDisplayMaxHeightInPx * 1.5).toInt() // -1 so it doesn't try to split when imageHeight = getDisplayHeightInPx - val partCount = (imageHeight - 1) / getDisplayMaxHeightInPx + 1 + val partCount = (imageHeight - 1) / splitHeight + 1 - logcat { "Splitting ${imageHeight}px height image into $partCount part with estimated ${splitHeight}px per height" } + val optimalSplitHeight = imageHeight / partCount + + val splitDataList = (0 until partCount).fold(mutableListOf()) { list, index -> + list.apply { + // Only continue if the list is empty or there is image remaining + if (isEmpty() || imageHeight > last().bottomOffset) { + val topOffset = index * optimalSplitHeight + var outputImageHeight = min(optimalSplitHeight, imageHeight - topOffset) + + val remainingHeight = imageHeight - (topOffset + outputImageHeight) + // If remaining height is smaller or equal to 1/3th of + // optimal split height then include it in current page + if (remainingHeight <= (optimalSplitHeight / 3)) { + outputImageHeight += remainingHeight + } + add(SplitData(index, topOffset, outputImageHeight)) + } + } + } val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { BitmapRegionDecoder.newInstance(imageFile.openInputStream()) @@ -220,36 +238,52 @@ object ImageUtil { return false } - try { - (0 until partCount).forEach { splitIndex -> - val splitPath = imageFilePath.substringBeforeLast(".") + "__${"%03d".format(splitIndex + 1)}.jpg" + logcat { + "Splitting image with height of $imageHeight into $partCount part " + + "with estimated ${optimalSplitHeight}px height per split" + } - val topOffset = splitIndex * splitHeight - val outputImageHeight = min(splitHeight, imageHeight - topOffset) - val bottomOffset = topOffset + outputImageHeight - logcat { "Split #$splitIndex with topOffset=$topOffset height=$outputImageHeight bottomOffset=$bottomOffset" } + return try { + splitDataList.forEach { splitData -> + val splitPath = splitImagePath(imageFilePath, splitData.index) - val region = Rect(0, topOffset, imageWidth, bottomOffset) + val region = Rect(0, splitData.topOffset, imageWidth, splitData.bottomOffset) FileOutputStream(splitPath).use { outputStream -> val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options) splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + splitBitmap.recycle() + } + logcat { + "Success: Split #${splitData.index + 1} with topOffset=${splitData.topOffset} " + + "height=${splitData.outputImageHeight} bottomOffset=${splitData.bottomOffset}" } } imageFile.delete() - return true + true } catch (e: Exception) { // Image splits were not successfully saved so delete them and keep the original image - (0 until partCount) - .map { imageFilePath.substringBeforeLast(".") + "__${"%03d".format(it + 1)}.jpg" } + splitDataList + .map { splitImagePath(imageFilePath, it.index) } .forEach { File(it).delete() } logcat(LogPriority.ERROR, e) - return false + false } finally { bitmapRegionDecoder.recycle() } } + private fun splitImagePath(imageFilePath: String, index: Int) = + imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg" + + data class SplitData( + val index: Int, + val topOffset: Int, + val outputImageHeight: Int, + ) { + val bottomOffset = topOffset + outputImageHeight + } + /** * Algorithm for determining what background to accompany a comic/manga page */