Remove the option to split tall images when downloading

The long strip reader decodes every page in strips of about 4MB since b585613, so splitting at
download time no longer helps it. The split also re-encoded the pieces as JPEG, turning PNG and WebP
pages lossy, and sized them for the screen of the device that downloaded the chapter.
This commit is contained in:
AntsyLich
2026-10-20 04:47:55 +06:00
parent 4750748dd1
commit 8b9faa383b
8 changed files with 1 additions and 211 deletions
+1
View File
@@ -38,6 +38,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Removed
- Remove "Custom hardware bitmap threshold" and "Custom display profile" advanced settings ([@wwww-wwww](https://github.com/wwww-wwww)) ([#3786](https://github.com/mihonapp/mihon/pull/3786))
- Remove the "Split tall images" download setting ([@AntsyLich](https://github.com/AntsyLich))
## [v0.21.2] - 2026-10-13
### Fixed
@@ -43,11 +43,6 @@ object SettingsDownloadScreen : SearchableSettings {
preference = downloadPreferences.saveChaptersAsCBZ,
title = stringResource(MR.strings.save_chapter_as_cbz),
),
Preference.PreferenceItem.SwitchPreference(
preference = downloadPreferences.splitTallImages,
title = stringResource(MR.strings.split_tall_images),
subtitle = stringResource(MR.strings.split_tall_images_summary),
),
Preference.PreferenceItem.SliderPreference(
preference = downloadPreferences.parallelSourceLimit,
valueRange = 1..10,
@@ -551,8 +551,6 @@ class Downloader(
}
// When the page is ready, set page path, progress (just in case) and status
splitTallImageIfNeeded(page, tmpDir)
page.uri = file.uri
page.progress = 100
page.status = Page.State.Ready
@@ -645,23 +643,6 @@ class Downloader(
return ImageUtil.getExtensionFromMimeType(mime) { file.openInputStream() }
}
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile) {
if (!downloadPreferences.splitTallImages.get()) return
try {
val filenamePrefix = "%03d".format(Locale.ENGLISH, page.number)
val imageFile = tmpDir.listFiles()?.firstOrNull { it.name.orEmpty().startsWith(filenamePrefix) }
?: error(context.stringResource(MR.strings.download_notifier_split_page_not_found, page.number))
// If the original page was previously split, then skip
if (imageFile.name.orEmpty().startsWith("${filenamePrefix}__")) return
ImageUtil.splitTallImage(tmpDir, imageFile, filenamePrefix)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to split downloaded image" }
}
}
/**
* Checks if the download was successful.
*
@@ -185,120 +185,6 @@ object ImageUtil {
LEFT,
}
/**
* Check whether the image is considered a tall image.
*
* @return true if the height:width ratio is greater than 3.
*/
private fun isTallImage(imageSource: BufferedSource): Boolean {
val options = extractImageOptions(imageSource)
return TallImageSplitCalculator.shouldSplit(
imageWidth = options.outWidth,
imageHeight = options.outHeight,
optimalImageHeight = optimalImageHeight,
)
}
/**
* Splits tall images to improve performance of reader
*/
fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String): Boolean {
val imageSource = imageFile.openInputStream().use { Buffer().readFrom(it) }
if (isAnimatedAndSupported(imageSource) || !isTallImage(imageSource)) {
return true
}
val bitmapRegionDecoder = getBitmapRegionDecoder(imageSource.peek().inputStream())
if (bitmapRegionDecoder == null) {
logcat { "Failed to create new instance of BitmapRegionDecoder" }
return false
}
val options = extractImageOptions(imageSource).apply {
inJustDecodeBounds = false
}
val splitDataList = options.splitData
return try {
splitDataList.forEach { splitData ->
val splitImageName = splitImageName(filenamePrefix, splitData.index)
// Remove pre-existing split if exists (this split shouldn't exist under normal circumstances)
tmpDir.findFile(splitImageName)?.delete()
val splitFile = tmpDir.createFile(splitImageName)!!
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
splitFile.openOutputStream().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.splitHeight} 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 { splitImageName(filenamePrefix, it.index) }
.forEach { tmpDir.findFile(it)?.delete() }
logcat(LogPriority.ERROR, e)
false
} finally {
bitmapRegionDecoder.recycle()
}
}
private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(
Locale.ENGLISH,
index + 1,
)}.jpg"
private val BitmapFactory.Options.splitData
get(): List<SplitData> {
val imageHeight = outHeight
val imageWidth = outWidth
val partCount = TallImageSplitCalculator.calculatePartCount(imageHeight, optimalImageHeight)
val optimalSplitHeight = imageHeight / partCount
logcat {
"Generating SplitData for image (height: $imageHeight): " +
"$partCount parts @ ${optimalSplitHeight}px height per part"
}
return buildList {
val range = 0..<partCount
for (index in range) {
// Only continue if the list is empty or there is image remaining
if (isNotEmpty() && imageHeight <= last().bottomOffset) break
val topOffset = index * optimalSplitHeight
var splitHeight = min(optimalSplitHeight, imageHeight - topOffset)
if (index == range.last) {
val remainingHeight = imageHeight - (topOffset + splitHeight)
splitHeight += remainingHeight
}
add(SplitData(index, topOffset, splitHeight, imageWidth))
}
}
}
data class SplitData(
val index: Int,
val topOffset: Int,
val splitHeight: Int,
val splitWidth: Int,
) {
val bottomOffset = topOffset + splitHeight
}
/**
* Algorithm for determining what background to accompany a comic/manga page
*/
@@ -553,18 +439,4 @@ object ImageUtil {
BitmapFactory.decodeStream(imageSource.peek().inputStream(), null, options)
return options
}
private fun getBitmapRegionDecoder(imageStream: InputStream): BitmapRegionDecoder? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(imageStream)
} else {
@Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(imageStream, false)
}
}
private val optimalImageHeight = getDisplayMaxHeightInPx * 2
}
val getDisplayMaxHeightInPx: Int
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
@@ -1,17 +0,0 @@
package tachiyomi.core.common.util.system
internal object TallImageSplitCalculator {
fun calculatePartCount(imageHeight: Int, optimalImageHeight: Int): Int {
require(imageHeight > 0) { "imageHeight must be positive" }
require(optimalImageHeight > 0) { "optimalImageHeight must be positive" }
// -1 so it doesn't try to split when imageHeight = optimalImageHeight
return (imageHeight - 1) / optimalImageHeight + 1
}
fun shouldSplit(imageWidth: Int, imageHeight: Int, optimalImageHeight: Int): Boolean {
require(imageWidth > 0) { "imageWidth must be positive" }
return imageHeight > imageWidth * 3 &&
calculatePartCount(imageHeight, optimalImageHeight) > 1
}
}
@@ -1,37 +0,0 @@
package tachiyomi.core.common.util.system
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class TallImageSplitCalculatorTest {
@Test
fun `does not split when aspect ratio is tall but computed split count is one`() {
assertFalse(
TallImageSplitCalculator.shouldSplit(
imageWidth = 1024,
imageHeight = 4385,
optimalImageHeight = 4386,
),
)
}
@Test
fun `splits when aspect ratio is tall and computed split count is greater than one`() {
assertTrue(
TallImageSplitCalculator.shouldSplit(
imageWidth = 1024,
imageHeight = 4385,
optimalImageHeight = 4384,
),
)
}
@Test
fun `calculate part count rounds boundary correctly`() {
assertEquals(1, TallImageSplitCalculator.calculatePartCount(imageHeight = 4384, optimalImageHeight = 4384))
assertEquals(2, TallImageSplitCalculator.calculatePartCount(imageHeight = 4385, optimalImageHeight = 4384))
}
}
@@ -19,8 +19,6 @@ class DownloadPreferences(
val saveChaptersAsCBZ: Preference<Boolean> = preferenceStore.getBoolean("save_chapter_as_cbz", true)
val splitTallImages: Preference<Boolean> = preferenceStore.getBoolean("split_tall_images", true)
val autoDownloadWhileReading: Preference<Int> = preferenceStore.getInt("auto_download_while_reading", 0)
val removeAfterReadSlots: Preference<Int> = preferenceStore.getInt("remove_after_read_slots", -1)
@@ -577,8 +577,6 @@
<string name="auto_download_while_reading">Auto download while reading</string>
<string name="download_ahead_info">Only works if the current chapter + the next one are already downloaded.</string>
<string name="save_chapter_as_cbz">Save as CBZ archive</string>
<string name="split_tall_images">Split tall images</string>
<string name="split_tall_images_summary">Improves reader performance</string>
<string name="pref_download_concurrent_sources">Concurrent source downloads</string>
<string name="pref_download_concurrent_pages">Concurrent page downloads</string>
<string name="pref_download_concurrent_pages_summary">Pages downloaded simultaneously per source</string>
@@ -1038,7 +1036,6 @@
<string name="download_notifier_text_only_wifi">No Wi-Fi connection available</string>
<string name="download_notifier_no_network">No network connection available</string>
<string name="download_notifier_download_paused">Downloads paused</string>
<string name="download_notifier_split_page_not_found">Page %d not found while splitting</string>
<string name="download_notifier_split_page_path_not_found">Couldn\'t find file path of page %d</string>
<string name="download_notifier_cache_renewal">Checking downloads</string>