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:
parent
157d8db68c
commit
122cdae5bc
5 changed files with 243 additions and 1 deletions
|
@ -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() })
|
||||||
|
|
||||||
|
|
|
@ -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!!)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Reference in a new issue