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 2822e3c937..422aa02df6 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 @@ -1,7 +1,10 @@ package eu.kanade.tachiyomi.data.download import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.webkit.MimeTypeMap +import androidx.core.graphics.BitmapCompat import com.hippo.unifile.UniFile import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.PublishRelay @@ -27,6 +30,8 @@ import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.system.ImageUtil +import eu.kanade.tachiyomi.util.system.ImageUtil.isAnimatedAndSupported +import eu.kanade.tachiyomi.util.system.ImageUtil.isTallImage import eu.kanade.tachiyomi.util.system.logcat import kotlinx.coroutines.async import logcat.LogPriority @@ -38,6 +43,8 @@ import rx.subscriptions.CompositeSubscription import uy.kohesive.injekt.injectLazy import java.io.BufferedOutputStream import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream import java.util.zip.CRC32 import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -345,7 +352,12 @@ class Downloader( .flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5) .onBackpressureLatest() // Do when page is downloaded. - .doOnNext { notifier.onProgressChange(download) } + .doOnNext { page -> + if (preferences.splitTallImages().get()) { + splitTallImage(page, download, tmpDir) + } + notifier.onProgressChange(download) + } .toList() .map { download } // Do after download completes @@ -379,7 +391,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 { @@ -490,7 +502,7 @@ 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 @@ -545,6 +557,57 @@ class Downloader( tmpDir.delete() } + /** + * Splits tall images to improve performance of reader + */ + private fun splitTallImage(page: Page, download: Download, tmpDir: UniFile) { + val filename = String.format("%03d", page.number) + val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } + if (imageFile == null) { + notifier.onError("Error: imageFile was not found", download.chapter.name, download.manga.title) + return + } + + if (!isAnimatedAndSupported(imageFile.openInputStream()) && isTallImage(imageFile.openInputStream())) { + // Getting the scaled bitmap of the source image + val bitmap = BitmapFactory.decodeFile(imageFile.filePath) + val scaledBitmap: Bitmap = + BitmapCompat.createScaledBitmap(bitmap, bitmap.width, bitmap.height, null, true) + + val splitsCount: Int = bitmap.height / context.resources.displayMetrics.heightPixels + 1 + val splitHeight = bitmap.height / splitsCount + + // xCoord and yCoord are the pixel positions of the image splits + val xCoord = 0 + var yCoord = 0 + try { + for (i in 0 until splitsCount) { + val splitPath = imageFile.filePath!!.substringBeforeLast(".") + "__${"%03d".format(i + 1)}.jpg" + // Compress the bitmap and save in jpg format + val stream: OutputStream = FileOutputStream(splitPath) + stream.use { + Bitmap.createBitmap( + scaledBitmap, + xCoord, + yCoord, + bitmap.width, + splitHeight, + ).compress(Bitmap.CompressFormat.JPEG, 100, stream) + } + yCoord += splitHeight + } + imageFile.delete() + } catch (e: Exception) { + // Image splits were not successfully saved so delete them and keep the original image + for (i in 0 until splitsCount) { + val splitPath = imageFile.filePath!!.substringBeforeLast(".") + "__${"%03d".format(i + 1)}.jpg" + File(splitPath).delete() + } + throw e + } + } + } + /** * Completes a download. This method is called in the main thread. */ 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 082c7128ad..3aa1b028a3 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 @@ -200,6 +200,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/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/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index 22c9fc440c..8e9eeda550 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 @@ -115,6 +115,24 @@ object ImageUtil { return options.outWidth > options.outHeight } + /** + * Check whether the image is considered a tall image + * @return true if the height:width ratio is greater than the 3:! + */ + fun isTallImage(imageStream: InputStream): Boolean { + imageStream.mark(imageStream.available() + 1) + + val imageBytes = imageStream.readBytes() + // Checking the image dimensions without loading it in the memory. + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) + val width = options.outWidth + val height = options.outHeight + val ratio = height / width + + return ratio > 3 + } + /** * Extract the 'side' part from imageStream and return it as InputStream. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index caf74d51d3..c5e2813097 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -407,6 +407,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