Selaa lähdekoodia

Add option to automatically split tall downloaded images (#7029)

* Auto split long images to improve performance of reader

* Auto split long images to improve performance of reader - fixed the sorting

* Improved performance of splitting by getting rid of 1 extra loop

* Cleaned up code and moved the functionality to work during the downloading process (unsure how this affects download speed)

* Replaced the import .* with the actual used imports

* Fixes for Bugs discovered during my testing

* Fixed last split missing bug.

* Reordered the download progress to be updated before splitting instead of after to reflect more meaningful progress of download

* Reverted last commit since it had no effect

* Improved progress tracking when a download is paused then resumed.

* Implemented the recommended changes to enhance the feature.

* Apply suggestions from code review

Co-authored-by: arkon <[email protected]>

* Update app/src/main/res/values/strings.xml

Co-authored-by: arkon <[email protected]>

Co-authored-by: arkon <[email protected]>
S97 2 vuotta sitten
vanhempi
commit
aa11902aa1

+ 66 - 3
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.
      */

+ 2 - 0
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)

+ 7 - 0
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
 

+ 18 - 0
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.
      */

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -407,6 +407,8 @@
     <string name="pref_download_new">Download new chapters</string>
     <string name="pref_download_new_categories_details">Manga in excluded categories will not be downloaded even if they are also in included categories.</string>
     <string name="save_chapter_as_cbz">Save as CBZ archive</string>
+    <string name="split_tall_images">Auto split tall images</string>
+    <string name="split_tall_images_summary">Improves reader performance by splitting tall downloaded images.</string>
 
       <!-- Tracking section -->
     <string name="tracking_guide">Tracking guide</string>