parent
dfb50fbddc
commit
81aac0d431
@ -0,0 +1,150 @@
|
|||||||
|
package org.koitharu.kotatsu.reader.domain
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Point
|
||||||
|
import android.graphics.Rect
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.graphics.alpha
|
||||||
|
import androidx.core.graphics.blue
|
||||||
|
import androidx.core.graphics.get
|
||||||
|
import androidx.core.graphics.green
|
||||||
|
import androidx.core.graphics.red
|
||||||
|
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||||
|
import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder
|
||||||
|
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.use
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class EdgeDetector(private val context: Context) {
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
suspend fun getBounds(imageSource: ImageSource): Rect? = mutex.withLock {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val decoder = SkiaPooledImageRegionDecoder(Bitmap.Config.RGB_565)
|
||||||
|
try {
|
||||||
|
val size = runInterruptible {
|
||||||
|
decoder.init(context, imageSource)
|
||||||
|
}
|
||||||
|
val edges = coroutineScope {
|
||||||
|
listOf(
|
||||||
|
async { detectLeftRightEdge(decoder, size, isLeft = true) },
|
||||||
|
async { detectTopBottomEdge(decoder, size, isTop = true) },
|
||||||
|
async { detectLeftRightEdge(decoder, size, isLeft = false) },
|
||||||
|
async { detectTopBottomEdge(decoder, size, isTop = false) },
|
||||||
|
).awaitAll()
|
||||||
|
}
|
||||||
|
var hasEdges = false
|
||||||
|
for (edge in edges) {
|
||||||
|
if (edge > 0) {
|
||||||
|
hasEdges = true
|
||||||
|
} else if (edge < 0) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasEdges) {
|
||||||
|
Rect(edges[0], edges[1], size.x - edges[2], size.y - edges[3])
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
decoder.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detectLeftRightEdge(decoder: ImageRegionDecoder, size: Point, isLeft: Boolean): Int {
|
||||||
|
var width = size.x
|
||||||
|
val rectCount = size.x / BLOCK_SIZE
|
||||||
|
val maxRect = rectCount / 3
|
||||||
|
for (i in 0 until rectCount) {
|
||||||
|
if (i > maxRect) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
var dd = BLOCK_SIZE
|
||||||
|
for (j in 0 until size.y / BLOCK_SIZE) {
|
||||||
|
val regionX = if (isLeft) i * BLOCK_SIZE else size.x - (i + 1) * BLOCK_SIZE
|
||||||
|
decoder.decodeRegion(region(regionX, j * BLOCK_SIZE), 1).use { bitmap ->
|
||||||
|
for (ii in 0 until minOf(BLOCK_SIZE, dd)) {
|
||||||
|
for (jj in 0 until BLOCK_SIZE) {
|
||||||
|
val bi = if (isLeft) ii else BLOCK_SIZE - ii - 1
|
||||||
|
if (bitmap[bi, jj].isNotWhite()) {
|
||||||
|
width = minOf(width, BLOCK_SIZE * i + ii)
|
||||||
|
dd--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dd == 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dd < BLOCK_SIZE) {
|
||||||
|
break // We have already found vertical field or it is not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun detectTopBottomEdge(decoder: ImageRegionDecoder, size: Point, isTop: Boolean): Int {
|
||||||
|
var height = size.y
|
||||||
|
val rectCount = size.y / BLOCK_SIZE
|
||||||
|
val maxRect = rectCount / 3
|
||||||
|
for (j in 0 until rectCount) {
|
||||||
|
if (j > maxRect) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
var dd = BLOCK_SIZE
|
||||||
|
for (i in 0 until size.x / BLOCK_SIZE) {
|
||||||
|
val regionY = if (isTop) j * BLOCK_SIZE else size.y - (j + 1) * BLOCK_SIZE
|
||||||
|
decoder.decodeRegion(region(i * BLOCK_SIZE, regionY), 1).use { bitmap ->
|
||||||
|
for (jj in 0 until minOf(BLOCK_SIZE, dd)) {
|
||||||
|
for (ii in 0 until BLOCK_SIZE) {
|
||||||
|
val bj = if (isTop) jj else BLOCK_SIZE - jj - 1
|
||||||
|
if (bitmap[ii, bj].isNotWhite()) {
|
||||||
|
height = minOf(height, BLOCK_SIZE * j + jj)
|
||||||
|
dd--
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dd == 0) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dd < BLOCK_SIZE) {
|
||||||
|
break // We have already found vertical field or it is not exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val BLOCK_SIZE = 100
|
||||||
|
private const val COLOR_TOLERANCE = 16
|
||||||
|
|
||||||
|
fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int, tolerance: Int): Boolean {
|
||||||
|
return abs(a.red - b.red) <= tolerance &&
|
||||||
|
abs(a.green - b.green) <= tolerance &&
|
||||||
|
abs(a.blue - b.blue) <= tolerance &&
|
||||||
|
abs(a.alpha - b.alpha) <= tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.isNotWhite() = !isColorTheSame(this, Color.WHITE, COLOR_TOLERANCE)
|
||||||
|
|
||||||
|
private fun region(x: Int, y: Int) = Rect(x, y, x + BLOCK_SIZE, y + BLOCK_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,79 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.reader.domain
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Point
|
|
||||||
import android.graphics.Rect
|
|
||||||
import androidx.core.graphics.get
|
|
||||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
|
||||||
import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder
|
|
||||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.runInterruptible
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlin.math.abs
|
|
||||||
|
|
||||||
class WhitespaceDetector(
|
|
||||||
private val context: Context
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val mutex = Mutex()
|
|
||||||
|
|
||||||
suspend fun getBounds(imageSource: ImageSource): Rect? = mutex.withLock {
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
val decoder = SkiaImageRegionDecoder(Bitmap.Config.RGB_565)
|
|
||||||
try {
|
|
||||||
val size = decoder.init(context, imageSource)
|
|
||||||
detectWhitespaces(decoder, size)
|
|
||||||
} finally {
|
|
||||||
decoder.recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
private fun detectWhitespaces(decoder: ImageRegionDecoder, size: Point): Rect? {
|
|
||||||
val result = Rect(0, 0, size.x, size.y)
|
|
||||||
val window = Rect()
|
|
||||||
val windowSize = 200
|
|
||||||
|
|
||||||
var baseColor = -1
|
|
||||||
window.set(0, 0, windowSize, windowSize)
|
|
||||||
decoder.decodeRegion(window, 1).use { bitmap ->
|
|
||||||
baseColor = bitmap[0, 0]
|
|
||||||
outerTop@ for (x in 1 until bitmap.width / 2) {
|
|
||||||
for (y in 1 until bitmap.height / 2) {
|
|
||||||
if (isSameColor(baseColor, bitmap[x, y])) {
|
|
||||||
result.left = x
|
|
||||||
result.top = y
|
|
||||||
} else {
|
|
||||||
break@outerTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.set(size.x - windowSize - 1, size.y - windowSize - 1, size.x - 1, size.y - 1)
|
|
||||||
decoder.decodeRegion(window, 1).use { bitmap ->
|
|
||||||
outerBottom@ for (x in (bitmap.width / 2 until bitmap.width).reversed()) {
|
|
||||||
for (y in (bitmap.height / 2 until bitmap.height).reversed()) {
|
|
||||||
if (isSameColor(baseColor, bitmap[x, y])) {
|
|
||||||
result.right = size.x - x
|
|
||||||
result.bottom = size.y - y
|
|
||||||
} else {
|
|
||||||
break@outerBottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.takeUnless { it.isEmpty || (it.width() == size.x && it.height() == size.y) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isSameColor(a: Int, b: Int) = abs(a - b) <= 4 // TODO
|
|
||||||
|
|
||||||
private inline fun <R> Bitmap.use(block: (Bitmap) -> R) = try {
|
|
||||||
block(this)
|
|
||||||
} finally {
|
|
||||||
recycle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue