Move Local Source to separate module (#9152)
* Move Local Source to separate module * Review changes
This commit is contained in:
@@ -46,7 +46,6 @@ import tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
@@ -113,9 +112,6 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio
|
||||
}
|
||||
}
|
||||
|
||||
val getDisplayMaxHeightInPx: Int
|
||||
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
|
||||
|
||||
/**
|
||||
* Converts to px and takes into account LTR/RTL layout.
|
||||
*/
|
||||
|
||||
@@ -1,589 +0,0 @@
|
||||
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.BitmapRegionDecoder
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.alpha
|
||||
import androidx.core.graphics.applyCanvas
|
||||
import androidx.core.graphics.blue
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.get
|
||||
import androidx.core.graphics.green
|
||||
import androidx.core.graphics.red
|
||||
import com.hippo.unifile.UniFile
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.decoder.Format
|
||||
import tachiyomi.decoder.ImageDecoder
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.net.URLConnection
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
object ImageUtil {
|
||||
|
||||
fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean {
|
||||
val contentType = try {
|
||||
URLConnection.guessContentTypeFromName(name)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} ?: openStream?.let { findImageType(it)?.mime }
|
||||
return contentType?.startsWith("image/") ?: false
|
||||
}
|
||||
|
||||
fun findImageType(openStream: () -> InputStream): ImageType? {
|
||||
return openStream().use { findImageType(it) }
|
||||
}
|
||||
|
||||
fun findImageType(stream: InputStream): ImageType? {
|
||||
return try {
|
||||
when (getImageType(stream)?.format) {
|
||||
Format.Avif -> ImageType.AVIF
|
||||
Format.Gif -> ImageType.GIF
|
||||
Format.Heif -> ImageType.HEIF
|
||||
Format.Jpeg -> ImageType.JPEG
|
||||
Format.Jxl -> ImageType.JXL
|
||||
Format.Png -> ImageType.PNG
|
||||
Format.Webp -> ImageType.WEBP
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getExtensionFromMimeType(mime: String?): String {
|
||||
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
|
||||
?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime]
|
||||
?: "jpg"
|
||||
}
|
||||
|
||||
fun isAnimatedAndSupported(stream: InputStream): Boolean {
|
||||
try {
|
||||
val type = getImageType(stream) ?: return false
|
||||
return when (type.format) {
|
||||
Format.Gif -> true
|
||||
// Coil supports animated WebP on Android 9.0+
|
||||
// https://coil-kt.github.io/coil/getting_started/#supported-image-formats
|
||||
Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
else -> false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
/* Do Nothing */
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getImageType(stream: InputStream): tachiyomi.decoder.ImageType? {
|
||||
val bytes = ByteArray(32)
|
||||
|
||||
val length = if (stream.markSupported()) {
|
||||
stream.mark(bytes.size)
|
||||
stream.read(bytes, 0, bytes.size).also { stream.reset() }
|
||||
} else {
|
||||
stream.read(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
if (length == -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ImageDecoder.findType(bytes)
|
||||
}
|
||||
|
||||
enum class ImageType(val mime: String, val extension: String) {
|
||||
AVIF("image/avif", "avif"),
|
||||
GIF("image/gif", "gif"),
|
||||
HEIF("image/heif", "heif"),
|
||||
JPEG("image/jpeg", "jpg"),
|
||||
JXL("image/jxl", "jxl"),
|
||||
PNG("image/png", "png"),
|
||||
WEBP("image/webp", "webp"),
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the image is wide (which we consider a double-page spread).
|
||||
*
|
||||
* @return true if the width is greater than the height
|
||||
*/
|
||||
fun isWideImage(imageStream: BufferedInputStream): Boolean {
|
||||
val options = extractImageOptions(imageStream)
|
||||
return options.outWidth > options.outHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the 'side' part from imageStream and return it as InputStream.
|
||||
*/
|
||||
fun splitInHalf(imageStream: InputStream, side: Side): InputStream {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val height = imageBitmap.height
|
||||
val width = imageBitmap.width
|
||||
|
||||
val singlePage = Rect(0, 0, width / 2, height)
|
||||
|
||||
val half = createBitmap(width / 2, height)
|
||||
val part = when (side) {
|
||||
Side.RIGHT -> Rect(width - width / 2, 0, width, height)
|
||||
Side.LEFT -> Rect(0, 0, width / 2, height)
|
||||
}
|
||||
half.applyCanvas {
|
||||
drawBitmap(imageBitmap, part, singlePage, null)
|
||||
}
|
||||
val output = ByteArrayOutputStream()
|
||||
half.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the image into left and right parts, then merge them into a new image.
|
||||
*/
|
||||
fun splitAndMerge(imageStream: InputStream, upperSide: Side): InputStream {
|
||||
val imageBytes = imageStream.readBytes()
|
||||
|
||||
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
val height = imageBitmap.height
|
||||
val width = imageBitmap.width
|
||||
|
||||
val result = createBitmap(width / 2, height * 2)
|
||||
result.applyCanvas {
|
||||
// right -> upper
|
||||
val rightPart = when (upperSide) {
|
||||
Side.RIGHT -> Rect(width - width / 2, 0, width, height)
|
||||
Side.LEFT -> Rect(0, 0, width / 2, height)
|
||||
}
|
||||
val upperPart = Rect(0, 0, width / 2, height)
|
||||
drawBitmap(imageBitmap, rightPart, upperPart, null)
|
||||
// left -> bottom
|
||||
val leftPart = when (upperSide) {
|
||||
Side.LEFT -> Rect(width - width / 2, 0, width, height)
|
||||
Side.RIGHT -> Rect(0, 0, width / 2, height)
|
||||
}
|
||||
val bottomPart = Rect(0, height, width / 2, height * 2)
|
||||
drawBitmap(imageBitmap, leftPart, bottomPart, null)
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 100, output)
|
||||
return ByteArrayInputStream(output.toByteArray())
|
||||
}
|
||||
|
||||
enum class Side {
|
||||
RIGHT,
|
||||
LEFT,
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the image is considered a tall image.
|
||||
*
|
||||
* @return true if the height:width ratio is greater than 3.
|
||||
*/
|
||||
private fun isTallImage(imageStream: InputStream): Boolean {
|
||||
val options = extractImageOptions(imageStream, resetAfterExtraction = false)
|
||||
return (options.outHeight / options.outWidth) > 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits tall images to improve performance of reader
|
||||
*/
|
||||
fun splitTallImage(tmpDir: UniFile, imageFile: UniFile, filenamePrefix: String): Boolean {
|
||||
if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) {
|
||||
return true
|
||||
}
|
||||
|
||||
val bitmapRegionDecoder = getBitmapRegionDecoder(imageFile.openInputStream())
|
||||
if (bitmapRegionDecoder == null) {
|
||||
logcat { "Failed to create new instance of BitmapRegionDecoder" }
|
||||
return false
|
||||
}
|
||||
|
||||
val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply {
|
||||
inJustDecodeBounds = false
|
||||
}
|
||||
|
||||
val splitDataList = options.splitData
|
||||
|
||||
return try {
|
||||
splitDataList.forEach { splitData ->
|
||||
val splitImageName = splitImageName(filenamePrefix, splitData.index)
|
||||
// Remove pre-existing split if exists (this split shouldn't exist under normal circumstances)
|
||||
tmpDir.findFile(splitImageName)?.delete()
|
||||
|
||||
val splitFile = tmpDir.createFile(splitImageName)
|
||||
|
||||
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
|
||||
|
||||
splitFile.openOutputStream().use { outputStream ->
|
||||
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options)
|
||||
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
splitBitmap.recycle()
|
||||
}
|
||||
logcat {
|
||||
"Success: Split #${splitData.index + 1} with topOffset=${splitData.topOffset} " +
|
||||
"height=${splitData.splitHeight} bottomOffset=${splitData.bottomOffset}"
|
||||
}
|
||||
}
|
||||
imageFile.delete()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
// Image splits were not successfully saved so delete them and keep the original image
|
||||
splitDataList
|
||||
.map { splitImageName(filenamePrefix, it.index) }
|
||||
.forEach { tmpDir.findFile(it)?.delete() }
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
} finally {
|
||||
bitmapRegionDecoder.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg"
|
||||
|
||||
/**
|
||||
* Check whether the image is a long Strip that needs splitting
|
||||
* @return true if the image is not animated and it's height is greater than image width and screen height
|
||||
*/
|
||||
fun isStripSplitNeeded(imageStream: BufferedInputStream): Boolean {
|
||||
if (isAnimatedAndSupported(imageStream)) return false
|
||||
|
||||
val options = extractImageOptions(imageStream)
|
||||
val imageHeightIsBiggerThanWidth = options.outHeight > options.outWidth
|
||||
val imageHeightBiggerThanScreenHeight = options.outHeight > optimalImageHeight
|
||||
return imageHeightIsBiggerThanWidth && imageHeightBiggerThanScreenHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the imageStream according to the provided splitData
|
||||
*/
|
||||
fun splitStrip(splitData: SplitData, streamFn: () -> InputStream): InputStream {
|
||||
val bitmapRegionDecoder = getBitmapRegionDecoder(streamFn())
|
||||
?: throw Exception("Failed to create new instance of BitmapRegionDecoder")
|
||||
|
||||
logcat {
|
||||
"WebtoonSplit #${splitData.index} with topOffset=${splitData.topOffset} " +
|
||||
"splitHeight=${splitData.splitHeight} bottomOffset=${splitData.bottomOffset}"
|
||||
}
|
||||
|
||||
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
|
||||
|
||||
try {
|
||||
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, null)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
return ByteArrayInputStream(outputStream.toByteArray())
|
||||
} catch (e: Throwable) {
|
||||
throw e
|
||||
} finally {
|
||||
bitmapRegionDecoder.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSplitDataForStream(imageStream: InputStream): List<SplitData> {
|
||||
return extractImageOptions(imageStream).splitData
|
||||
}
|
||||
|
||||
private val BitmapFactory.Options.splitData
|
||||
get(): List<SplitData> {
|
||||
val imageHeight = outHeight
|
||||
val imageWidth = outWidth
|
||||
|
||||
// -1 so it doesn't try to split when imageHeight = optimalImageHeight
|
||||
val partCount = (imageHeight - 1) / optimalImageHeight + 1
|
||||
val optimalSplitHeight = imageHeight / partCount
|
||||
|
||||
logcat {
|
||||
"Generating SplitData for image (height: $imageHeight): " +
|
||||
"$partCount parts @ ${optimalSplitHeight}px height per part"
|
||||
}
|
||||
|
||||
return mutableListOf<SplitData>().apply {
|
||||
val range = 0 until partCount
|
||||
for (index in range) {
|
||||
// Only continue if the list is empty or there is image remaining
|
||||
if (isNotEmpty() && imageHeight <= last().bottomOffset) break
|
||||
|
||||
val topOffset = index * optimalSplitHeight
|
||||
var splitHeight = min(optimalSplitHeight, imageHeight - topOffset)
|
||||
|
||||
if (index == range.last) {
|
||||
val remainingHeight = imageHeight - (topOffset + splitHeight)
|
||||
splitHeight += remainingHeight
|
||||
}
|
||||
|
||||
add(SplitData(index, topOffset, splitHeight, imageWidth))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SplitData(
|
||||
val index: Int,
|
||||
val topOffset: Int,
|
||||
val splitHeight: Int,
|
||||
val splitWidth: Int,
|
||||
) {
|
||||
val bottomOffset = topOffset + splitHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Algorithm for determining what background to accompany a comic/manga page
|
||||
*/
|
||||
fun chooseBackground(context: Context, imageStream: InputStream): Drawable {
|
||||
val decoder = ImageDecoder.newInstance(imageStream)
|
||||
val image = decoder?.decode()
|
||||
decoder?.recycle()
|
||||
|
||||
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[left, top]
|
||||
val topRightPixel = image[right, top]
|
||||
val midLeftPixel = image[left, midY]
|
||||
val midRightPixel = image[right, midY]
|
||||
val topCenterPixel = image[midX, top]
|
||||
val botLeftPixel = image[left, bot]
|
||||
val bottomCenterPixel = image[midX, bot]
|
||||
val botRightPixel = image[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[x, y]
|
||||
val pixelOff = image[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[leftOffsetX, top].isDark() && image[rightOffsetX, top].isDark()
|
||||
val botOffsetCornersIsDark = image[leftOffsetX, bot].isDark() && image[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 @receiver:ColorInt Int.isDark(): Boolean =
|
||||
red < 40 && blue < 40 && green < 40 && alpha > 200
|
||||
|
||||
private fun @receiver:ColorInt Int.isCloseTo(other: Int): Boolean =
|
||||
abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30
|
||||
|
||||
private fun @receiver:ColorInt Int.isWhite(): Boolean =
|
||||
red + blue + green > 740
|
||||
|
||||
/**
|
||||
* Used to check an image's dimensions without loading it in the memory.
|
||||
*/
|
||||
private fun extractImageOptions(
|
||||
imageStream: InputStream,
|
||||
resetAfterExtraction: Boolean = true,
|
||||
): BitmapFactory.Options {
|
||||
imageStream.mark(imageStream.available() + 1)
|
||||
|
||||
val imageBytes = imageStream.readBytes()
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options)
|
||||
if (resetAfterExtraction) imageStream.reset()
|
||||
return options
|
||||
}
|
||||
|
||||
private fun getBitmapRegionDecoder(imageStream: InputStream): BitmapRegionDecoder? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
BitmapRegionDecoder.newInstance(imageStream)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
BitmapRegionDecoder.newInstance(imageStream, false)
|
||||
}
|
||||
}
|
||||
|
||||
private val optimalImageHeight = getDisplayMaxHeightInPx * 2
|
||||
|
||||
// Android doesn't include some mappings
|
||||
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
|
||||
// https://issuetracker.google.com/issues/182703810
|
||||
"image/jxl" to "jxl",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user