From 6db2becd3023046558c1100a6658a8a76eac1148 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 14:56:08 -0400 Subject: [PATCH] Add auto split tall images setting Also includes some fixes for bad merges in earlier commits Co-authored-by: Saud-97 Co-authored-by: AntsyLich --- .../tachiyomi/data/download/Downloader.kt | 46 +++-- .../data/preference/PreferencesHelper.kt | 2 + .../myanimelist/MyAnimeListInterceptor.kt | 1 - .../eu/kanade/tachiyomi/source/LocalSource.kt | 4 + .../ui/reader/viewer/pager/PagerPageHolder.kt | 5 +- .../viewer/webtoon/WebtoonPageHolder.kt | 5 +- .../ui/setting/SettingsDownloadController.kt | 7 + .../util/system/ContextExtensions.kt | 12 +- .../kanade/tachiyomi/util/system/ImageUtil.kt | 178 +++++++++++++++--- app/src/main/res/values/strings.xml | 5 + 10 files changed, 217 insertions(+), 48 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 3221168e76..6ae972c82b 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 @@ -273,7 +273,7 @@ class Downloader( // Start downloader if needed if (autoStart && wasEmpty) { - val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count() + val queuedDownloads = queue.count { it.source !is UnmeteredSource } val maxDownloadsFromSource = queue .groupBy { it.source } .filterKeys { it !is UnmeteredSource } @@ -352,6 +352,7 @@ class Downloader( .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } // If the page list threw, it will resume here .onErrorReturn { error -> + logcat(LogPriority.ERROR, error) download.status = Download.State.ERROR notifier.onError(error.message, download.chapter.name, download.manga.title) download @@ -379,7 +380,7 @@ class Downloader( tmpFile?.delete() // Try to find the image file. - val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } + val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") || it.name!!.contains("${filename}__001") } // If the image is already downloaded, do nothing. Otherwise download from network val pageObservable = when { @@ -389,8 +390,12 @@ class Downloader( } return pageObservable - // When the image is ready, set image path, progress (just in case) and status + // When the page is ready, set page path, progress (just in case) and status .doOnNext { file -> + val success = splitTallImageIfNeeded(page, tmpDir) + if (success.not()) { + notifier.onError(context.getString(R.string.download_notifier_split_failed), download.chapter.name, download.manga.title) + } page.uri = file.uri page.progress = 100 download.downloadedImages++ @@ -401,6 +406,7 @@ class Downloader( .onErrorReturn { page.progress = 0 page.status = Page.ERROR + notifier.onError(it.message, download.chapter.name, download.manga.title) page } } @@ -474,6 +480,26 @@ class Downloader( return ImageUtil.getExtensionFromMimeType(mime) } + private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean { + if (!preferences.splitTallImages().get()) return true + + val filename = String.format("%03d", page.number) + val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) } + ?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number)) + val imageFilePath = imageFile.filePath + ?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number)) + + // check if the original page was previously splitted before then skip. + if (imageFile.name!!.contains("__")) return true + + return try { + ImageUtil.splitTallImage(imageFile, imageFilePath) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + false + } + } + /** * Checks if the download was successful. * @@ -489,16 +515,10 @@ class Downloader( dirname: String, ) { // Ensure that the chapter folder has all the images. - val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } + val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) } download.status = if (downloadedImages.size == download.pages!!.size) { - Download.State.DOWNLOADED - } else { - Download.State.ERROR - } - - // Only rename the directory if it's downloaded. - if (download.status == Download.State.DOWNLOADED) { + // Only rename the directory if it's downloaded. if (preferences.saveChaptersAsCBZ().get()) { archiveChapter(mangaDir, dirname, tmpDir) } else { @@ -507,6 +527,10 @@ class Downloader( cache.addChapter(dirname, mangaDir, download.manga) DiskUtil.createNoMediaFile(tmpDir, context) + + Download.State.DOWNLOADED + } else { + Download.State.ERROR } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 96b0989b27..45c568b204 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -206,6 +206,8 @@ class PreferencesHelper(val context: Context) { fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true) + fun splitTallImages() = flowPrefs.getBoolean("split_tall_images", false) + fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false) fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 22da4c1212..cf26d57e8e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.track.myanimelist import eu.kanade.tachiyomi.network.parseAs -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index e8709e4686..65cd080241 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -4,6 +4,7 @@ import android.content.Context import com.github.junrar.Archive import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -31,6 +32,8 @@ import logcat.LogPriority import rx.Observable import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.MangaInfo +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.io.FileInputStream @@ -40,6 +43,7 @@ import java.util.zip.ZipFile class LocalSource( private val context: Context, + private val coverCache: CoverCache = Injekt.get(), ) : CatalogueSource, UnmeteredSource { private val json: Json by injectLazy() 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 7cfea4cc92..694c47da6b 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,6 +19,7 @@ import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers +import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.InputStream import java.util.concurrent.TimeUnit @@ -238,7 +239,7 @@ class PagerPageHolder( .subscribe({}, {}) } - private fun process(page: ReaderPage, imageStream: InputStream): InputStream { + private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream { if (!viewer.config.dualPageSplit) { return imageStream } @@ -247,7 +248,7 @@ class PagerPageHolder( return splitInHalf(imageStream) } - val isDoublePage = ImageUtil.isDoublePage(imageStream) + val isDoublePage = ImageUtil.isWideImage(imageStream) if (!isDoublePage) { return imageStream } 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 c8b23b673c..3463eafd62 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 @@ -23,6 +23,7 @@ import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers +import java.io.BufferedInputStream import java.io.InputStream import java.util.concurrent.TimeUnit @@ -272,12 +273,12 @@ class WebtoonPageHolder( addSubscription(readImageHeaderSubscription) } - private fun process(imageStream: InputStream): InputStream { + private fun process(imageStream: BufferedInputStream): InputStream { if (!viewer.config.dualPageSplit) { return imageStream } - val isDoublePage = ImageUtil.isDoublePage(imageStream) + val isDoublePage = ImageUtil.isWideImage(imageStream) if (!isDoublePage) { return imageStream } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 74fce340b6..eaf43573e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.preference.multiSelectListPreference import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.preferenceCategory +import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.system.toast @@ -72,6 +73,12 @@ class SettingsDownloadController : SettingsController() { bindTo(preferences.saveChaptersAsCBZ()) titleRes = R.string.save_chapter_as_cbz } + switchPreference { + bindTo(preferences.splitTallImages()) + titleRes = R.string.split_tall_images + summaryRes = R.string.split_tall_images_summary + } + preferenceCategory { titleRes = R.string.pref_category_delete_chapters diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index d9a76d31e4..b041210037 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -47,6 +47,7 @@ import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File +import kotlin.math.max import kotlin.math.roundToInt private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720 @@ -166,6 +167,9 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio } } +val getDisplayMaxHeightInPx: Int + get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) } + /** * Converts to dp. */ @@ -258,7 +262,7 @@ fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) { } fun Context.defaultBrowserPackageName(): String? { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://")) + val browserIntent = Intent(Intent.ACTION_VIEW, "http://".toUri()) return packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) ?.activityInfo?.packageName ?.takeUnless { it in DeviceUtil.invalidDefaultBrowsers } @@ -315,8 +319,8 @@ fun Context.isNightMode(): Boolean { * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898 */ fun Context.createReaderThemeContext(): Context { - val prefs = Injekt.get() - val isDarkBackground = when (prefs.readerTheme().get()) { + val preferences = Injekt.get() + val isDarkBackground = when (preferences.readerTheme().get()) { 1, 2 -> true // Black, Gray 3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default else -> false // White @@ -329,7 +333,7 @@ fun Context.createReaderThemeContext(): Context { val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi) wrappedContext.applyOverrideConfiguration(overrideConf) - ThemingDelegate.getThemeResIds(prefs.appTheme().get(), prefs.themeDarkAmoled().get()) + ThemingDelegate.getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get()) .forEach { wrappedContext.theme.applyStyle(it, true) } return wrappedContext } 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 f50c9151d0..2d9bec7183 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 @@ -4,6 +4,7 @@ import android.content.Context import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.ColorDrawable @@ -11,19 +12,27 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.os.Build import android.webkit.MimeTypeMap +import androidx.annotation.ColorInt import androidx.core.graphics.alpha import androidx.core.graphics.applyCanvas import androidx.core.graphics.blue import androidx.core.graphics.createBitmap +import androidx.core.graphics.get import androidx.core.graphics.green import androidx.core.graphics.red +import com.hippo.unifile.UniFile +import logcat.LogPriority 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.FileOutputStream import java.io.InputStream import java.net.URLConnection import kotlin.math.abs +import kotlin.math.min object ImageUtil { @@ -73,8 +82,7 @@ object ImageUtil { Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P else -> false } - } catch (e: Exception) { - } + } catch (e: Exception) { /* Do Nothing */ } return false } @@ -106,19 +114,12 @@ object ImageUtil { } /** - * Check whether the image is a double-page spread + * Check whether the image is wide (which we consider a double-page spread). + * * @return true if the width is greater than the height */ - fun isDoublePage(imageStream: InputStream): Boolean { - imageStream.mark(imageStream.available() + 1) - - val imageBytes = imageStream.readBytes() - - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) - - imageStream.reset() - + fun isWideImage(imageStream: BufferedInputStream): Boolean { + val options = extractImageOptions(imageStream) return options.outWidth > options.outHeight } @@ -185,6 +186,111 @@ object ImageUtil { RIGHT, LEFT } + /** + * Check whether the image is considered a tall image. + * + * @return true if the height:width ratio is greater than 3. + */ + private fun isTallImage(imageStream: InputStream): Boolean { + val options = extractImageOptions(imageStream, resetAfterExtraction = false) + return (options.outHeight / options.outWidth) > 3 + } + + /** + * Splits tall images to improve performance of reader + */ + fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean { + if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) { + return true + } + + 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 * 1.5).toInt() + // -1 so it doesn't try to split when imageHeight = getDisplayHeightInPx + val partCount = (imageHeight - 1) / splitHeight + 1 + + 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()) + } else { + @Suppress("DEPRECATION") + BitmapRegionDecoder.newInstance(imageFile.openInputStream(), false) + } + + if (bitmapRegionDecoder == null) { + logcat { "Failed to create new instance of BitmapRegionDecoder" } + return false + } + + logcat { + "Splitting image with height of $imageHeight into $partCount part " + + "with estimated ${optimalSplitHeight}px height per split" + } + + return try { + splitDataList.forEach { splitData -> + val splitPath = splitImagePath(imageFilePath, splitData.index) + + 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() + true + } catch (e: Exception) { + // Image splits were not successfully saved so delete them and keep the original image + splitDataList + .map { splitImagePath(imageFilePath, it.index) } + .forEach { File(it).delete() } + logcat(LogPriority.ERROR, e) + 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 */ @@ -209,14 +315,14 @@ object ImageUtil { val leftOffsetX = left - offsetX val rightOffsetX = right + offsetX - val topLeftPixel = image.getPixel(left, top) - val topRightPixel = image.getPixel(right, top) - val midLeftPixel = image.getPixel(left, midY) - val midRightPixel = image.getPixel(right, midY) - val topCenterPixel = image.getPixel(midX, top) - val botLeftPixel = image.getPixel(left, bot) - val bottomCenterPixel = image.getPixel(midX, bot) - val botRightPixel = image.getPixel(right, bot) + val topLeftPixel = image[left, top] + val topRightPixel = image[right, top] + val midLeftPixel = image[left, midY] + val midRightPixel = image[right, midY] + val topCenterPixel = image[midX, top] + val botLeftPixel = image[left, bot] + val bottomCenterPixel = image[midX, bot] + val botRightPixel = image[right, bot] val topLeftIsDark = topLeftPixel.isDark() val topRightIsDark = topRightPixel.isDark() @@ -269,8 +375,8 @@ object ImageUtil { var whiteStreak = false val notOffset = x == left || x == right inner@ for ((index, y) in (0 until image.height step image.height / 25).withIndex()) { - val pixel = image.getPixel(x, y) - val pixelOff = image.getPixel(x + (if (x < image.width / 2) -offsetX else offsetX), y) + val pixel = image[x, y] + val pixelOff = image[x + (if (x < image.width / 2) -offsetX else offsetX), y] if (pixel.isWhite()) { whitePixelsStreak++ whitePixels++ @@ -361,8 +467,8 @@ object ImageUtil { val topCornersIsDark = topLeftIsDark && topRightIsDark val botCornersIsDark = botLeftIsDark && botRightIsDark - val topOffsetCornersIsDark = image.getPixel(leftOffsetX, top).isDark() && image.getPixel(rightOffsetX, top).isDark() - val botOffsetCornersIsDark = image.getPixel(leftOffsetX, bot).isDark() && image.getPixel(rightOffsetX, bot).isDark() + val topOffsetCornersIsDark = image[leftOffsetX, top].isDark() && image[rightOffsetX, top].isDark() + val botOffsetCornersIsDark = image[leftOffsetX, bot].isDark() && image[rightOffsetX, bot].isDark() val gradient = when { darkBG && botCornersIsWhite -> { @@ -391,15 +497,31 @@ object ImageUtil { ) } - private fun Int.isDark(): Boolean = + private fun @receiver:ColorInt Int.isDark(): Boolean = red < 40 && blue < 40 && green < 40 && alpha > 200 - private fun Int.isCloseTo(other: Int): Boolean = + private fun @receiver:ColorInt Int.isCloseTo(other: Int): Boolean = abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30 - private fun Int.isWhite(): Boolean = + private fun @receiver:ColorInt Int.isWhite(): Boolean = red + blue + green > 740 + /** + * 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(imageStream.available() + 1) + + val imageBytes = imageStream.readBytes() + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) + if (resetAfterExtraction) imageStream.reset() + return options + } + // Android doesn't include some mappings private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf( // https://issuetracker.google.com/issues/182703810 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a85736a223..37dede11e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -410,6 +410,8 @@ Download new chapters Manga in excluded categories will not be downloaded even if they are also in included categories. Save as CBZ archive + Auto split tall images + Improves reader performance by splitting tall downloaded images. Tracking guide @@ -809,6 +811,9 @@ No network connection available Download paused Download completed + Page %d not found while splitting + Couldn\'t find file path of page %d + Couldn\'t split downloaded image Common