Automatic background color for PagerViewer (#4996)

* Add J2K implementation of automatic background

Co-authored-by: Jays2Kings <8617760+Jays2Kings@users.noreply.github.com>

* Tweak the monstrosity called automatic background

* Add ability to choose Automatic as a background

* More tweaks

Co-authored-by: Jays2Kings <8617760+Jays2Kings@users.noreply.github.com>
This commit is contained in:
Andreas 2021-05-05 04:46:42 +02:00 committed by GitHub
parent 157d8db68c
commit 122cdae5bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 243 additions and 1 deletions

View file

@ -23,6 +23,9 @@ class PagerConfig(
preferences: PreferencesHelper = Injekt.get() preferences: PreferencesHelper = Injekt.get()
) : ViewerConfig(preferences, scope) { ) : ViewerConfig(preferences, scope) {
var automaticBackground = false
private set
var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null var dualPageSplitChangedListener: ((Boolean) -> Unit)? = null
var imageScaleType = 1 var imageScaleType = 1
@ -35,6 +38,9 @@ class PagerConfig(
private set private set
init { init {
preferences.readerTheme()
.register({ automaticBackground = it == 3 }, { imagePropertyChangedListener?.invoke() })
preferences.imageScaleType() preferences.imageScaleType()
.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() }) .register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })

View file

@ -238,7 +238,12 @@ class PagerPageHolder(
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (!isAnimated) { if (!isAnimated) {
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!)) initSubsamplingImageView().apply {
if (viewer.config.automaticBackground) {
background = ImageUtil.chooseBackground(context, openStream!!)
}
setImage(ImageSource.inputStream(openStream!!))
}
} else { } else {
initImageView().setImage(openStream!!) initImageView().setImage(openStream!!)
} }

View file

@ -1,14 +1,25 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Rect 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.createBitmap
import androidx.core.graphics.green
import androidx.core.graphics.red
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.net.URLConnection import java.net.URLConnection
import kotlin.math.abs
object ImageUtil { object ImageUtil {
@ -153,4 +164,221 @@ object ImageUtil {
enum class Side { enum class Side {
RIGHT, LEFT 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
} }

View file

@ -13,12 +13,14 @@
<item>@string/black_background</item> <item>@string/black_background</item>
<item>@string/gray_background</item> <item>@string/gray_background</item>
<item>@string/white_background</item> <item>@string/white_background</item>
<item>@string/automatic_background</item>
</string-array> </string-array>
<string-array name="reader_themes_values"> <string-array name="reader_themes_values">
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
<item>0</item> <item>0</item>
<item>3</item>
</string-array> </string-array>
<string-array name="image_scale_type"> <string-array name="image_scale_type">

View file

@ -300,6 +300,7 @@
<string name="white_background">White</string> <string name="white_background">White</string>
<string name="gray_background">Gray</string> <string name="gray_background">Gray</string>
<string name="black_background">Black</string> <string name="black_background">Black</string>
<string name="automatic_background">Automatic</string>
<string name="pref_viewer_type">Default reading mode</string> <string name="pref_viewer_type">Default reading mode</string>
<string name="default_viewer">Default</string> <string name="default_viewer">Default</string>
<string name="default_nav">Default</string> <string name="default_nav">Default</string>