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