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