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 <arkon@users.noreply.github.com> * Update app/src/main/res/values/strings.xml Co-authored-by: arkon <arkon@users.noreply.github.com> Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
parent
c4088bad12
commit
aa11902aa1
@ -1,7 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.download
|
package eu.kanade.tachiyomi.data.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.graphics.BitmapCompat
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
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.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
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 eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
@ -38,6 +43,8 @@ import rx.subscriptions.CompositeSubscription
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.OutputStream
|
||||||
import java.util.zip.CRC32
|
import java.util.zip.CRC32
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
@ -345,7 +352,12 @@ class Downloader(
|
|||||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
|
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
.doOnNext { notifier.onProgressChange(download) }
|
.doOnNext { page ->
|
||||||
|
if (preferences.splitTallImages().get()) {
|
||||||
|
splitTallImage(page, download, tmpDir)
|
||||||
|
}
|
||||||
|
notifier.onProgressChange(download)
|
||||||
|
}
|
||||||
.toList()
|
.toList()
|
||||||
.map { download }
|
.map { download }
|
||||||
// Do after download completes
|
// Do after download completes
|
||||||
@ -379,7 +391,7 @@ class Downloader(
|
|||||||
tmpFile?.delete()
|
tmpFile?.delete()
|
||||||
|
|
||||||
// Try to find the image file.
|
// 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
|
// If the image is already downloaded, do nothing. Otherwise download from network
|
||||||
val pageObservable = when {
|
val pageObservable = when {
|
||||||
@ -490,7 +502,7 @@ class Downloader(
|
|||||||
dirname: String,
|
dirname: String,
|
||||||
) {
|
) {
|
||||||
// Ensure that the chapter folder has all the images.
|
// 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.status = if (downloadedImages.size == download.pages!!.size) {
|
||||||
Download.State.DOWNLOADED
|
Download.State.DOWNLOADED
|
||||||
@ -545,6 +557,57 @@ class Downloader(
|
|||||||
tmpDir.delete()
|
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.
|
* Completes a download. This method is called in the main thread.
|
||||||
*/
|
*/
|
||||||
|
@ -200,6 +200,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
|
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 folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
|
||||||
|
@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
|
|||||||
import eu.kanade.tachiyomi.util.preference.onClick
|
import eu.kanade.tachiyomi.util.preference.onClick
|
||||||
import eu.kanade.tachiyomi.util.preference.preference
|
import eu.kanade.tachiyomi.util.preference.preference
|
||||||
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
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.switchPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
import eu.kanade.tachiyomi.util.preference.titleRes
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
@ -72,6 +73,12 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
bindTo(preferences.saveChaptersAsCBZ())
|
bindTo(preferences.saveChaptersAsCBZ())
|
||||||
titleRes = R.string.save_chapter_as_cbz
|
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 {
|
preferenceCategory {
|
||||||
titleRes = R.string.pref_category_delete_chapters
|
titleRes = R.string.pref_category_delete_chapters
|
||||||
|
|
||||||
|
@ -115,6 +115,24 @@ object ImageUtil {
|
|||||||
return options.outWidth > options.outHeight
|
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.
|
* Extract the 'side' part from imageStream and return it as InputStream.
|
||||||
*/
|
*/
|
||||||
|
@ -407,6 +407,8 @@
|
|||||||
<string name="pref_download_new">Download new chapters</string>
|
<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="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="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 -->
|
<!-- Tracking section -->
|
||||||
<string name="tracking_guide">Tracking guide</string>
|
<string name="tracking_guide">Tracking guide</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user