mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	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:
		@@ -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() })
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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!!)
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user