diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt index 2965b8b90b..d02fcda4ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt @@ -23,6 +23,9 @@ class PagerConfig( preferences: PreferencesHelper = Injekt.get() ) : ViewerConfig(preferences, scope) { + var automaticBackground = false + private set + var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null var imageScaleType = 1 @@ -35,6 +38,9 @@ class PagerConfig( private set init { + preferences.readerTheme() + .register({ automaticBackground = it == 3 }, { imagePropertyChangedListener?.invoke() }) + preferences.imageScaleType() .register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() }) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 791a8a025d..7b78bafaf2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -238,7 +238,12 @@ class PagerPageHolder( .observeOn(AndroidSchedulers.mainThread()) .doOnNext { isAnimated -> if (!isAnimated) { - initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!)) + initSubsamplingImageView().apply { + if (viewer.config.automaticBackground) { + background = ImageUtil.chooseBackground(context, openStream!!) + } + setImage(ImageSource.inputStream(openStream!!)) + } } else { initImageView().setImage(openStream!!) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index b21efff0bf..6a178a0903 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -1,14 +1,25 @@ package eu.kanade.tachiyomi.util.system +import android.content.Context +import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas +import android.graphics.Color import android.graphics.Rect +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import androidx.core.graphics.alpha +import androidx.core.graphics.blue import androidx.core.graphics.createBitmap +import androidx.core.graphics.green +import androidx.core.graphics.red import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream import java.net.URLConnection +import kotlin.math.abs object ImageUtil { @@ -153,4 +164,221 @@ object ImageUtil { enum class Side { RIGHT, LEFT } + + /** + * Algorithm for determining what background to accompany a comic/manga page + */ + fun chooseBackground(context: Context, imageStream: InputStream): Drawable { + imageStream.mark(imageStream.available() + 1) + + val image = BitmapFactory.decodeStream(imageStream) + + imageStream.reset() + + val whiteColor = Color.WHITE + if (image == null) return ColorDrawable(whiteColor) + if (image.width < 50 || image.height < 50) { + return ColorDrawable(whiteColor) + } + + val top = 5 + val bot = image.height - 5 + val left = (image.width * 0.0275).toInt() + val right = image.width - left + val midX = image.width / 2 + val midY = image.height / 2 + val offsetX = (image.width * 0.01).toInt() + 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 topLeftIsDark = topLeftPixel.isDark() + val topRightIsDark = topRightPixel.isDark() + val midLeftIsDark = midLeftPixel.isDark() + val midRightIsDark = midRightPixel.isDark() + val topMidIsDark = topCenterPixel.isDark() + val botLeftIsDark = botLeftPixel.isDark() + val botRightIsDark = botRightPixel.isDark() + + var darkBG = (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) || + (topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark)) + + val topAndBotPixels = listOf(topLeftPixel, topCenterPixel, topRightPixel, botRightPixel, bottomCenterPixel, botLeftPixel) + val isNotWhiteAndCloseTo = topAndBotPixels.mapIndexed { index, color -> + val other = topAndBotPixels[(index + 1) % topAndBotPixels.size] + !color.isWhite() && color.isCloseTo(other) + } + if (isNotWhiteAndCloseTo.all { it }) { + return ColorDrawable(topLeftPixel) + } + + val cornerPixels = listOf(topLeftPixel, topRightPixel, botLeftPixel, botRightPixel) + val numberOfWhiteCorners = cornerPixels.map { cornerPixel -> cornerPixel.isWhite() } + .filter { it } + .size + if (numberOfWhiteCorners > 2) { + darkBG = false + } + + var blackColor = when { + topLeftIsDark -> topLeftPixel + topRightIsDark -> topRightPixel + botLeftIsDark -> botLeftPixel + botRightIsDark -> botRightPixel + else -> whiteColor + } + + var overallWhitePixels = 0 + var overallBlackPixels = 0 + var topBlackStreak = 0 + var topWhiteStreak = 0 + var botBlackStreak = 0 + var botWhiteStreak = 0 + outer@ for (x in intArrayOf(left, right, leftOffsetX, rightOffsetX)) { + var whitePixelsStreak = 0 + var whitePixels = 0 + var blackPixelsStreak = 0 + var blackPixels = 0 + var blackStreak = false + 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) + if (pixel.isWhite()) { + whitePixelsStreak++ + whitePixels++ + if (notOffset) { + overallWhitePixels++ + } + if (whitePixelsStreak > 14) { + whiteStreak = true + } + if (whitePixelsStreak > 6 && whitePixelsStreak >= index - 1) { + topWhiteStreak = whitePixelsStreak + } + } else { + whitePixelsStreak = 0 + if (pixel.isDark() && pixelOff.isDark()) { + blackPixels++ + if (notOffset) { + overallBlackPixels++ + } + blackPixelsStreak++ + if (blackPixelsStreak >= 14) { + blackStreak = true + } + continue@inner + } + } + if (blackPixelsStreak > 6 && blackPixelsStreak >= index - 1) { + topBlackStreak = blackPixelsStreak + } + blackPixelsStreak = 0 + } + if (blackPixelsStreak > 6) { + botBlackStreak = blackPixelsStreak + } else if (whitePixelsStreak > 6) { + botWhiteStreak = whitePixelsStreak + } + when { + blackPixels > 22 -> { + if (x == right || x == rightOffsetX) { + blackColor = when { + topRightIsDark -> topRightPixel + botRightIsDark -> botRightPixel + else -> blackColor + } + } + darkBG = true + overallWhitePixels = 0 + break@outer + } + blackStreak -> { + darkBG = true + if (x == right || x == rightOffsetX) { + blackColor = when { + topRightIsDark -> topRightPixel + botRightIsDark -> botRightPixel + else -> blackColor + } + } + if (blackPixels > 18) { + overallWhitePixels = 0 + break@outer + } + } + whiteStreak || whitePixels > 22 -> darkBG = false + } + } + + val topIsBlackStreak = topBlackStreak > topWhiteStreak + val bottomIsBlackStreak = botBlackStreak > botWhiteStreak + if (overallWhitePixels > 9 && overallWhitePixels > overallBlackPixels) { + darkBG = false + } + if (topIsBlackStreak && bottomIsBlackStreak) { + darkBG = true + } + + val isLandscape = context.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE + if (isLandscape) { + return when { + darkBG -> ColorDrawable(blackColor) + else -> ColorDrawable(whiteColor) + } + } + + val botCornersIsWhite = botLeftPixel.isWhite() && botRightPixel.isWhite() + val topCornersIsWhite = topLeftPixel.isWhite() && topRightPixel.isWhite() + + 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 gradient = when { + darkBG && botCornersIsWhite -> { + intArrayOf(blackColor, blackColor, whiteColor, whiteColor) + } + darkBG && topCornersIsWhite -> { + intArrayOf(whiteColor, whiteColor, blackColor, blackColor) + } + darkBG -> { + return ColorDrawable(blackColor) + } + topIsBlackStreak || (topCornersIsDark && topOffsetCornersIsDark && (topMidIsDark || overallBlackPixels > 9)) -> { + intArrayOf(blackColor, blackColor, whiteColor, whiteColor) + } + bottomIsBlackStreak || (botCornersIsDark && botOffsetCornersIsDark && (bottomCenterPixel.isDark() || overallBlackPixels > 9)) -> { + intArrayOf(whiteColor, whiteColor, blackColor, blackColor) + } + else -> { + return ColorDrawable(whiteColor) + } + } + + return GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + gradient + ) + } + + private fun Int.isDark(): Boolean = + red < 40 && blue < 40 && green < 40 && alpha > 200 + + private fun 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 = + red + blue + green > 740 } diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index fd178ae5ab..a516a943af 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -13,12 +13,14 @@ @string/black_background @string/gray_background @string/white_background + @string/automatic_background 1 2 0 + 3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e480943b72..56511bd686 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -300,6 +300,7 @@ White Gray Black + Automatic Default reading mode Default Default