mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	Add auto split tall images setting
Also includes some fixes for bad merges in earlier commits Co-authored-by: Saud-97 <Saud-97@users.noreply.github.com> Co-authored-by: AntsyLich <AntsyLich@users.noreply.github.com>
This commit is contained in:
		@@ -273,7 +273,7 @@ class Downloader(
 | 
			
		||||
 | 
			
		||||
            // Start downloader if needed
 | 
			
		||||
            if (autoStart && wasEmpty) {
 | 
			
		||||
                val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count()
 | 
			
		||||
                val queuedDownloads = queue.count { it.source !is UnmeteredSource }
 | 
			
		||||
                val maxDownloadsFromSource = queue
 | 
			
		||||
                    .groupBy { it.source }
 | 
			
		||||
                    .filterKeys { it !is UnmeteredSource }
 | 
			
		||||
@@ -352,6 +352,7 @@ class Downloader(
 | 
			
		||||
            .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
 | 
			
		||||
            // If the page list threw, it will resume here
 | 
			
		||||
            .onErrorReturn { error ->
 | 
			
		||||
                logcat(LogPriority.ERROR, error)
 | 
			
		||||
                download.status = Download.State.ERROR
 | 
			
		||||
                notifier.onError(error.message, download.chapter.name, download.manga.title)
 | 
			
		||||
                download
 | 
			
		||||
@@ -379,7 +380,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 {
 | 
			
		||||
@@ -389,8 +390,12 @@ class Downloader(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return pageObservable
 | 
			
		||||
            // When the image is ready, set image path, progress (just in case) and status
 | 
			
		||||
            // When the page is ready, set page path, progress (just in case) and status
 | 
			
		||||
            .doOnNext { file ->
 | 
			
		||||
                val success = splitTallImageIfNeeded(page, tmpDir)
 | 
			
		||||
                if (success.not()) {
 | 
			
		||||
                    notifier.onError(context.getString(R.string.download_notifier_split_failed), download.chapter.name, download.manga.title)
 | 
			
		||||
                }
 | 
			
		||||
                page.uri = file.uri
 | 
			
		||||
                page.progress = 100
 | 
			
		||||
                download.downloadedImages++
 | 
			
		||||
@@ -401,6 +406,7 @@ class Downloader(
 | 
			
		||||
            .onErrorReturn {
 | 
			
		||||
                page.progress = 0
 | 
			
		||||
                page.status = Page.ERROR
 | 
			
		||||
                notifier.onError(it.message, download.chapter.name, download.manga.title)
 | 
			
		||||
                page
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
@@ -474,6 +480,26 @@ class Downloader(
 | 
			
		||||
        return ImageUtil.getExtensionFromMimeType(mime)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
 | 
			
		||||
        if (!preferences.splitTallImages().get()) return true
 | 
			
		||||
 | 
			
		||||
        val filename = String.format("%03d", page.number)
 | 
			
		||||
        val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
 | 
			
		||||
            ?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
 | 
			
		||||
        val imageFilePath = imageFile.filePath
 | 
			
		||||
            ?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number))
 | 
			
		||||
 | 
			
		||||
        // check if the original page was previously splitted before then skip.
 | 
			
		||||
        if (imageFile.name!!.contains("__")) return true
 | 
			
		||||
 | 
			
		||||
        return try {
 | 
			
		||||
            ImageUtil.splitTallImage(imageFile, imageFilePath)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            logcat(LogPriority.ERROR, e)
 | 
			
		||||
            false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if the download was successful.
 | 
			
		||||
     *
 | 
			
		||||
@@ -489,16 +515,10 @@ 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
 | 
			
		||||
        } else {
 | 
			
		||||
            Download.State.ERROR
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Only rename the directory if it's downloaded.
 | 
			
		||||
        if (download.status == Download.State.DOWNLOADED) {
 | 
			
		||||
            // Only rename the directory if it's downloaded.
 | 
			
		||||
            if (preferences.saveChaptersAsCBZ().get()) {
 | 
			
		||||
                archiveChapter(mangaDir, dirname, tmpDir)
 | 
			
		||||
            } else {
 | 
			
		||||
@@ -507,6 +527,10 @@ class Downloader(
 | 
			
		||||
            cache.addChapter(dirname, mangaDir, download.manga)
 | 
			
		||||
 | 
			
		||||
            DiskUtil.createNoMediaFile(tmpDir, context)
 | 
			
		||||
 | 
			
		||||
            Download.State.DOWNLOADED
 | 
			
		||||
        } else {
 | 
			
		||||
            Download.State.ERROR
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -206,6 +206,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)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
package eu.kanade.tachiyomi.data.track.myanimelist
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.network.parseAs
 | 
			
		||||
import kotlinx.serialization.decodeFromString
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import android.content.Context
 | 
			
		||||
import com.github.junrar.Archive
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.cache.CoverCache
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
@@ -31,6 +32,8 @@ import logcat.LogPriority
 | 
			
		||||
import rx.Observable
 | 
			
		||||
import tachiyomi.source.model.ChapterInfo
 | 
			
		||||
import tachiyomi.source.model.MangaInfo
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.FileInputStream
 | 
			
		||||
@@ -40,6 +43,7 @@ import java.util.zip.ZipFile
 | 
			
		||||
 | 
			
		||||
class LocalSource(
 | 
			
		||||
    private val context: Context,
 | 
			
		||||
    private val coverCache: CoverCache = Injekt.get(),
 | 
			
		||||
) : CatalogueSource, UnmeteredSource {
 | 
			
		||||
 | 
			
		||||
    private val json: Json by injectLazy()
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import java.io.BufferedInputStream
 | 
			
		||||
import java.io.ByteArrayInputStream
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
@@ -238,7 +239,7 @@ class PagerPageHolder(
 | 
			
		||||
            .subscribe({}, {})
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun process(page: ReaderPage, imageStream: InputStream): InputStream {
 | 
			
		||||
    private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
 | 
			
		||||
        if (!viewer.config.dualPageSplit) {
 | 
			
		||||
            return imageStream
 | 
			
		||||
        }
 | 
			
		||||
@@ -247,7 +248,7 @@ class PagerPageHolder(
 | 
			
		||||
            return splitInHalf(imageStream)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val isDoublePage = ImageUtil.isDoublePage(imageStream)
 | 
			
		||||
        val isDoublePage = ImageUtil.isWideImage(imageStream)
 | 
			
		||||
        if (!isDoublePage) {
 | 
			
		||||
            return imageStream
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ import rx.Observable
 | 
			
		||||
import rx.Subscription
 | 
			
		||||
import rx.android.schedulers.AndroidSchedulers
 | 
			
		||||
import rx.schedulers.Schedulers
 | 
			
		||||
import java.io.BufferedInputStream
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
@@ -272,12 +273,12 @@ class WebtoonPageHolder(
 | 
			
		||||
        addSubscription(readImageHeaderSubscription)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun process(imageStream: InputStream): InputStream {
 | 
			
		||||
    private fun process(imageStream: BufferedInputStream): InputStream {
 | 
			
		||||
        if (!viewer.config.dualPageSplit) {
 | 
			
		||||
            return imageStream
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val isDoublePage = ImageUtil.isDoublePage(imageStream)
 | 
			
		||||
        val isDoublePage = ImageUtil.isWideImage(imageStream)
 | 
			
		||||
        if (!isDoublePage) {
 | 
			
		||||
            return imageStream
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,7 @@ import logcat.LogPriority
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.io.File
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
 | 
			
		||||
@@ -166,6 +167,9 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val getDisplayMaxHeightInPx: Int
 | 
			
		||||
    get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Converts to dp.
 | 
			
		||||
 */
 | 
			
		||||
@@ -258,7 +262,7 @@ fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Context.defaultBrowserPackageName(): String? {
 | 
			
		||||
    val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
 | 
			
		||||
    val browserIntent = Intent(Intent.ACTION_VIEW, "http://".toUri())
 | 
			
		||||
    return packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
 | 
			
		||||
        ?.activityInfo?.packageName
 | 
			
		||||
        ?.takeUnless { it in DeviceUtil.invalidDefaultBrowsers }
 | 
			
		||||
@@ -315,8 +319,8 @@ fun Context.isNightMode(): Boolean {
 | 
			
		||||
 * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898
 | 
			
		||||
 */
 | 
			
		||||
fun Context.createReaderThemeContext(): Context {
 | 
			
		||||
    val prefs = Injekt.get<PreferencesHelper>()
 | 
			
		||||
    val isDarkBackground = when (prefs.readerTheme().get()) {
 | 
			
		||||
    val preferences = Injekt.get<PreferencesHelper>()
 | 
			
		||||
    val isDarkBackground = when (preferences.readerTheme().get()) {
 | 
			
		||||
        1, 2 -> true // Black, Gray
 | 
			
		||||
        3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default
 | 
			
		||||
        else -> false // White
 | 
			
		||||
@@ -329,7 +333,7 @@ fun Context.createReaderThemeContext(): Context {
 | 
			
		||||
 | 
			
		||||
        val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi)
 | 
			
		||||
        wrappedContext.applyOverrideConfiguration(overrideConf)
 | 
			
		||||
        ThemingDelegate.getThemeResIds(prefs.appTheme().get(), prefs.themeDarkAmoled().get())
 | 
			
		||||
        ThemingDelegate.getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get())
 | 
			
		||||
            .forEach { wrappedContext.theme.applyStyle(it, true) }
 | 
			
		||||
        return wrappedContext
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import android.content.Context
 | 
			
		||||
import android.content.res.Configuration
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.graphics.BitmapFactory
 | 
			
		||||
import android.graphics.BitmapRegionDecoder
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.ColorDrawable
 | 
			
		||||
@@ -11,19 +12,27 @@ import android.graphics.drawable.Drawable
 | 
			
		||||
import android.graphics.drawable.GradientDrawable
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.webkit.MimeTypeMap
 | 
			
		||||
import androidx.annotation.ColorInt
 | 
			
		||||
import androidx.core.graphics.alpha
 | 
			
		||||
import androidx.core.graphics.applyCanvas
 | 
			
		||||
import androidx.core.graphics.blue
 | 
			
		||||
import androidx.core.graphics.createBitmap
 | 
			
		||||
import androidx.core.graphics.get
 | 
			
		||||
import androidx.core.graphics.green
 | 
			
		||||
import androidx.core.graphics.red
 | 
			
		||||
import com.hippo.unifile.UniFile
 | 
			
		||||
import logcat.LogPriority
 | 
			
		||||
import tachiyomi.decoder.Format
 | 
			
		||||
import tachiyomi.decoder.ImageDecoder
 | 
			
		||||
import java.io.BufferedInputStream
 | 
			
		||||
import java.io.ByteArrayInputStream
 | 
			
		||||
import java.io.ByteArrayOutputStream
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.FileOutputStream
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
import java.net.URLConnection
 | 
			
		||||
import kotlin.math.abs
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
 | 
			
		||||
object ImageUtil {
 | 
			
		||||
 | 
			
		||||
@@ -73,8 +82,7 @@ object ImageUtil {
 | 
			
		||||
                Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
 | 
			
		||||
                else -> false
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
        }
 | 
			
		||||
        } catch (e: Exception) { /* Do Nothing */ }
 | 
			
		||||
        return false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -106,19 +114,12 @@ object ImageUtil {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the image is a double-page spread
 | 
			
		||||
     * Check whether the image is wide (which we consider a double-page spread).
 | 
			
		||||
     *
 | 
			
		||||
     * @return true if the width is greater than the height
 | 
			
		||||
     */
 | 
			
		||||
    fun isDoublePage(imageStream: InputStream): Boolean {
 | 
			
		||||
        imageStream.mark(imageStream.available() + 1)
 | 
			
		||||
 | 
			
		||||
        val imageBytes = imageStream.readBytes()
 | 
			
		||||
 | 
			
		||||
        val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
 | 
			
		||||
        BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
 | 
			
		||||
 | 
			
		||||
        imageStream.reset()
 | 
			
		||||
 | 
			
		||||
    fun isWideImage(imageStream: BufferedInputStream): Boolean {
 | 
			
		||||
        val options = extractImageOptions(imageStream)
 | 
			
		||||
        return options.outWidth > options.outHeight
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -185,6 +186,111 @@ object ImageUtil {
 | 
			
		||||
        RIGHT, LEFT
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check whether the image is considered a tall image.
 | 
			
		||||
     *
 | 
			
		||||
     * @return true if the height:width ratio is greater than 3.
 | 
			
		||||
     */
 | 
			
		||||
    private fun isTallImage(imageStream: InputStream): Boolean {
 | 
			
		||||
        val options = extractImageOptions(imageStream, resetAfterExtraction = false)
 | 
			
		||||
        return (options.outHeight / options.outWidth) > 3
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Splits tall images to improve performance of reader
 | 
			
		||||
     */
 | 
			
		||||
    fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean {
 | 
			
		||||
        if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) {
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false }
 | 
			
		||||
        // Values are stored as they get modified during split loop
 | 
			
		||||
        val imageHeight = options.outHeight
 | 
			
		||||
        val imageWidth = options.outWidth
 | 
			
		||||
 | 
			
		||||
        val splitHeight = (getDisplayMaxHeightInPx * 1.5).toInt()
 | 
			
		||||
        // -1 so it doesn't try to split when imageHeight = getDisplayHeightInPx
 | 
			
		||||
        val partCount = (imageHeight - 1) / splitHeight + 1
 | 
			
		||||
 | 
			
		||||
        val optimalSplitHeight = imageHeight / partCount
 | 
			
		||||
 | 
			
		||||
        val splitDataList = (0 until partCount).fold(mutableListOf<SplitData>()) { list, index ->
 | 
			
		||||
            list.apply {
 | 
			
		||||
                // Only continue if the list is empty or there is image remaining
 | 
			
		||||
                if (isEmpty() || imageHeight > last().bottomOffset) {
 | 
			
		||||
                    val topOffset = index * optimalSplitHeight
 | 
			
		||||
                    var outputImageHeight = min(optimalSplitHeight, imageHeight - topOffset)
 | 
			
		||||
 | 
			
		||||
                    val remainingHeight = imageHeight - (topOffset + outputImageHeight)
 | 
			
		||||
                    // If remaining height is smaller or equal to 1/3th of
 | 
			
		||||
                    // optimal split height then include it in current page
 | 
			
		||||
                    if (remainingHeight <= (optimalSplitHeight / 3)) {
 | 
			
		||||
                        outputImageHeight += remainingHeight
 | 
			
		||||
                    }
 | 
			
		||||
                    add(SplitData(index, topOffset, outputImageHeight))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 | 
			
		||||
            BitmapRegionDecoder.newInstance(imageFile.openInputStream())
 | 
			
		||||
        } else {
 | 
			
		||||
            @Suppress("DEPRECATION")
 | 
			
		||||
            BitmapRegionDecoder.newInstance(imageFile.openInputStream(), false)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (bitmapRegionDecoder == null) {
 | 
			
		||||
            logcat { "Failed to create new instance of BitmapRegionDecoder" }
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        logcat {
 | 
			
		||||
            "Splitting image with height of $imageHeight into $partCount part " +
 | 
			
		||||
                "with estimated ${optimalSplitHeight}px height per split"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return try {
 | 
			
		||||
            splitDataList.forEach { splitData ->
 | 
			
		||||
                val splitPath = splitImagePath(imageFilePath, splitData.index)
 | 
			
		||||
 | 
			
		||||
                val region = Rect(0, splitData.topOffset, imageWidth, splitData.bottomOffset)
 | 
			
		||||
 | 
			
		||||
                FileOutputStream(splitPath).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.outputImageHeight} 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 { splitImagePath(imageFilePath, it.index) }
 | 
			
		||||
                .forEach { File(it).delete() }
 | 
			
		||||
            logcat(LogPriority.ERROR, e)
 | 
			
		||||
            false
 | 
			
		||||
        } finally {
 | 
			
		||||
            bitmapRegionDecoder.recycle()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun splitImagePath(imageFilePath: String, index: Int) =
 | 
			
		||||
        imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg"
 | 
			
		||||
 | 
			
		||||
    data class SplitData(
 | 
			
		||||
        val index: Int,
 | 
			
		||||
        val topOffset: Int,
 | 
			
		||||
        val outputImageHeight: Int,
 | 
			
		||||
    ) {
 | 
			
		||||
        val bottomOffset = topOffset + outputImageHeight
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Algorithm for determining what background to accompany a comic/manga page
 | 
			
		||||
     */
 | 
			
		||||
@@ -209,14 +315,14 @@ object ImageUtil {
 | 
			
		||||
        val leftOffsetX = left - offsetX
 | 
			
		||||
        val rightOffsetX = right + offsetX
 | 
			
		||||
 | 
			
		||||
        val topLeftPixel = image.getPixel(left, top)
 | 
			
		||||
        val topRightPixel = image.getPixel(right, top)
 | 
			
		||||
        val midLeftPixel = image.getPixel(left, midY)
 | 
			
		||||
        val midRightPixel = image.getPixel(right, midY)
 | 
			
		||||
        val topCenterPixel = image.getPixel(midX, top)
 | 
			
		||||
        val botLeftPixel = image.getPixel(left, bot)
 | 
			
		||||
        val bottomCenterPixel = image.getPixel(midX, bot)
 | 
			
		||||
        val botRightPixel = image.getPixel(right, bot)
 | 
			
		||||
        val topLeftPixel = image[left, top]
 | 
			
		||||
        val topRightPixel = image[right, top]
 | 
			
		||||
        val midLeftPixel = image[left, midY]
 | 
			
		||||
        val midRightPixel = image[right, midY]
 | 
			
		||||
        val topCenterPixel = image[midX, top]
 | 
			
		||||
        val botLeftPixel = image[left, bot]
 | 
			
		||||
        val bottomCenterPixel = image[midX, bot]
 | 
			
		||||
        val botRightPixel = image[right, bot]
 | 
			
		||||
 | 
			
		||||
        val topLeftIsDark = topLeftPixel.isDark()
 | 
			
		||||
        val topRightIsDark = topRightPixel.isDark()
 | 
			
		||||
@@ -269,8 +375,8 @@ object ImageUtil {
 | 
			
		||||
            var whiteStreak = false
 | 
			
		||||
            val notOffset = x == left || x == right
 | 
			
		||||
            inner@ for ((index, y) in (0 until image.height step image.height / 25).withIndex()) {
 | 
			
		||||
                val pixel = image.getPixel(x, y)
 | 
			
		||||
                val pixelOff = image.getPixel(x + (if (x < image.width / 2) -offsetX else offsetX), y)
 | 
			
		||||
                val pixel = image[x, y]
 | 
			
		||||
                val pixelOff = image[x + (if (x < image.width / 2) -offsetX else offsetX), y]
 | 
			
		||||
                if (pixel.isWhite()) {
 | 
			
		||||
                    whitePixelsStreak++
 | 
			
		||||
                    whitePixels++
 | 
			
		||||
@@ -361,8 +467,8 @@ object ImageUtil {
 | 
			
		||||
        val topCornersIsDark = topLeftIsDark && topRightIsDark
 | 
			
		||||
        val botCornersIsDark = botLeftIsDark && botRightIsDark
 | 
			
		||||
 | 
			
		||||
        val topOffsetCornersIsDark = image.getPixel(leftOffsetX, top).isDark() && image.getPixel(rightOffsetX, top).isDark()
 | 
			
		||||
        val botOffsetCornersIsDark = image.getPixel(leftOffsetX, bot).isDark() && image.getPixel(rightOffsetX, bot).isDark()
 | 
			
		||||
        val topOffsetCornersIsDark = image[leftOffsetX, top].isDark() && image[rightOffsetX, top].isDark()
 | 
			
		||||
        val botOffsetCornersIsDark = image[leftOffsetX, bot].isDark() && image[rightOffsetX, bot].isDark()
 | 
			
		||||
 | 
			
		||||
        val gradient = when {
 | 
			
		||||
            darkBG && botCornersIsWhite -> {
 | 
			
		||||
@@ -391,15 +497,31 @@ object ImageUtil {
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun Int.isDark(): Boolean =
 | 
			
		||||
    private fun @receiver:ColorInt Int.isDark(): Boolean =
 | 
			
		||||
        red < 40 && blue < 40 && green < 40 && alpha > 200
 | 
			
		||||
 | 
			
		||||
    private fun Int.isCloseTo(other: Int): Boolean =
 | 
			
		||||
    private fun @receiver:ColorInt Int.isCloseTo(other: Int): Boolean =
 | 
			
		||||
        abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30
 | 
			
		||||
 | 
			
		||||
    private fun Int.isWhite(): Boolean =
 | 
			
		||||
    private fun @receiver:ColorInt Int.isWhite(): Boolean =
 | 
			
		||||
        red + blue + green > 740
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Used to check an image's dimensions without loading it in the memory.
 | 
			
		||||
     */
 | 
			
		||||
    private fun extractImageOptions(
 | 
			
		||||
        imageStream: InputStream,
 | 
			
		||||
        resetAfterExtraction: Boolean = true,
 | 
			
		||||
    ): BitmapFactory.Options {
 | 
			
		||||
        imageStream.mark(imageStream.available() + 1)
 | 
			
		||||
 | 
			
		||||
        val imageBytes = imageStream.readBytes()
 | 
			
		||||
        val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
 | 
			
		||||
        BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
 | 
			
		||||
        if (resetAfterExtraction) imageStream.reset()
 | 
			
		||||
        return options
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Android doesn't include some mappings
 | 
			
		||||
    private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
 | 
			
		||||
        // https://issuetracker.google.com/issues/182703810
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user