mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-23 08:59:55 -07:00
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:
@@ -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
|
||||
|
||||
-5
@@ -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) }
|
||||
|
||||
-17
@@ -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
|
||||
}
|
||||
}
|
||||
-37
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user