mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +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