From fa61916176fcb72750703fe50a1e6c1866e1a508 Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Sun, 14 Jul 2024 18:04:33 +0200 Subject: [PATCH 1/3] Replace Kotlin Multiplatform BlurHash library with faster local implementation --- app/build.gradle.kts | 1 - .../com/wolt/blurhashkt/BlurHashDecoder.kt | 131 ++++++++++++++++++ .../jellyfin/androidtv/JellyfinApplication.kt | 7 - .../jellyfin/androidtv/ui/AsyncImageView.kt | 4 +- .../androidtv/ui/composable/AsyncImage.kt | 4 +- gradle/libs.versions.toml | 2 - 6 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0e59afa533..137446211e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,7 +142,6 @@ dependencies { implementation(libs.bundles.markwon) // Image utility - implementation(libs.blurhash) implementation(libs.bundles.coil) // Crash Reporting diff --git a/app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt new file mode 100644 index 0000000000..cdd8c54422 --- /dev/null +++ b/app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt @@ -0,0 +1,131 @@ +package com.wolt.blurhashkt + +import android.graphics.Bitmap +import android.graphics.Color +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.withSign + +object BlurHashDecoder { + + /** + * Decode a blur hash into a new bitmap. + */ + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { + if (blurHash == null || blurHash.length < 6) { + return null + } + val numCompEnc = decode83(blurHash, 0, 1) + val numCompX = (numCompEnc % 9) + 1 + val numCompY = (numCompEnc / 9) + 1 + val totalComp = numCompX * numCompY + if (blurHash.length != 4 + 2 * totalComp) { + return null + } + val maxAcEnc = decode83(blurHash, 1, 2) + val maxAc = (maxAcEnc + 1) / 166f + val colors = FloatArray(totalComp * 3) + var colorEnc = decode83(blurHash, 2, 6) + decodeDc(colorEnc, colors) + for (i in 1 until totalComp) { + val from = 4 + i * 2 + colorEnc = decode83(blurHash, from, from + 2) + decodeAc(colorEnc, maxAc * punch, colors, i * 3) + } + return composeBitmap(width, height, numCompX, numCompY, colors) + } + + private fun decode83(str: String, from: Int, to: Int): Int { + var result = 0 + for (i in from until to) { + val index = CHARS.indexOf(str[i]) + if (index != -1) { + result = result * 83 + index + } + } + return result + } + + private fun decodeDc(colorEnc: Int, outArray: FloatArray) { + val r = (colorEnc shr 16) and 0xFF + val g = (colorEnc shr 8) and 0xFF + val b = colorEnc and 0xFF + outArray[0] = srgbToLinear(r) + outArray[1] = srgbToLinear(g) + outArray[2] = srgbToLinear(b) + } + + private fun srgbToLinear(colorEnc: Int): Float { + val v = colorEnc / 255f + return if (v <= 0.04045f) { + (v / 12.92f) + } else { + ((v + 0.055f) / 1.055f).pow(2.4f) + } + } + + private fun decodeAc(value: Int, maxAc: Float, outArray: FloatArray, outIndex: Int) { + val r = value / (19 * 19) + val g = (value / 19) % 19 + val b = value % 19 + outArray[outIndex] = signedPow2((r - 9) / 9.0f) * maxAc + outArray[outIndex + 1] = signedPow2((g - 9) / 9.0f) * maxAc + outArray[outIndex + 2] = signedPow2((b - 9) / 9.0f) * maxAc + } + + private fun signedPow2(value: Float) = (value * value).withSign(value) + + private fun composeBitmap( + width: Int, height: Int, + numCompX: Int, numCompY: Int, + colors: FloatArray + ): Bitmap { + // use an array for better performance when writing pixel colors + val imageArray = IntArray(width * height) + val cosinesX = createCosines(width, numCompX) + val cosinesY = if (width == height && numCompX == numCompY) { + cosinesX + } else { + createCosines(height, numCompY) + } + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + val cosY = cosinesY[y * numCompY + j] + for (i in 0 until numCompX) { + val cosX = cosinesX[x * numCompX + i] + val basis = cosX * cosY + val colorIndex = (j * numCompX + i) * 3 + r += colors[colorIndex] * basis + g += colors[colorIndex + 1] * basis + b += colors[colorIndex + 2] * basis + } + } + imageArray[x + width * y] = + Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun createCosines(size: Int, numComp: Int) = FloatArray(size * numComp) { index -> + val x = index / numComp + val i = index % numComp + cos(PI * x * i / size).toFloat() + } + + private fun linearToSrgb(value: Float): Int { + val v = value.coerceIn(0f, 1f) + return if (v <= 0.0031308f) { + (v * 12.92f * 255f + 0.5f).toInt() + } else { + ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() + } + } + + private const val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" +} diff --git a/app/src/main/java/org/jellyfin/androidtv/JellyfinApplication.kt b/app/src/main/java/org/jellyfin/androidtv/JellyfinApplication.kt index c82a7b3f97..598589697d 100644 --- a/app/src/main/java/org/jellyfin/androidtv/JellyfinApplication.kt +++ b/app/src/main/java/org/jellyfin/androidtv/JellyfinApplication.kt @@ -7,7 +7,6 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.await -import com.vanniktech.blurhash.BlurHash import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -57,12 +56,6 @@ class JellyfinApplication : Application() { launch { socketListener.updateSession() } } - override fun onLowMemory() { - super.onLowMemory() - - BlurHash.clearCache() - } - override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt b/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt index d6082d9b58..9ba97b6a13 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt @@ -12,7 +12,7 @@ import androidx.lifecycle.lifecycleScope import coil.ImageLoader import coil.request.ImageRequest import coil.transform.CircleCropTransformation -import com.vanniktech.blurhash.BlurHash +import com.wolt.blurhashkt.BlurHashDecoder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -65,7 +65,7 @@ class AsyncImageView @JvmOverloads constructor( // Only show blurhash if an image is going to be loaded from the network if (url != null && blurHash != null) withContext(Dispatchers.IO) { - val blurHashBitmap = BlurHash.decode( + val blurHashBitmap = BlurHashDecoder.decode( blurHash, if (aspectRatio > 1) round(blurHashResolution * aspectRatio).toInt() else blurHashResolution, if (aspectRatio >= 1) blurHashResolution else round(blurHashResolution / aspectRatio).toInt(), diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt b/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt index f176cd3781..d3c5f088de 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.IntSize import androidx.compose.ui.viewinterop.AndroidView -import com.vanniktech.blurhash.BlurHash +import com.wolt.blurhashkt.BlurHashDecoder import org.jellyfin.androidtv.ui.AsyncImageView private data class AsyncImageState( @@ -64,7 +64,7 @@ fun blurHashPainter( size: IntSize, punch: Float = 1f, ): Painter = remember(blurHash, size, punch) { - val bitmap = BlurHash.decode( + val bitmap = BlurHashDecoder.decode( blurHash = blurHash, width = size.width, height = size.height, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 515b045cf3..6a9c96961a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,6 @@ androidx-tv = "1.0.0-rc01" androidx-tvprovider = "1.1.0-alpha01" androidx-window = "1.3.0" androidx-work = "2.9.0" -blurhash = "0.3.0" coil = "2.6.0" detekt = "1.23.6" jellyfin-androidx-media = "1.3.1+2" @@ -98,7 +97,6 @@ markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" } markwon-html = { module = "io.noties.markwon:html", version.ref = "markwon" } # Image utility -blurhash = { module = "com.vanniktech:blurhash", version.ref = "blurhash" } coil-base = { module = "io.coil-kt:coil-base", version.ref = "coil" } coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" } From e53b6beadad4e1846940544dc7ef9bb913a7499b Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Mon, 15 Jul 2024 23:52:09 +0200 Subject: [PATCH 2/3] fix: apply suggested changes after review --- .../com/wolt/blurhashkt/BlurHashDecoder.kt | 131 ----------------- .../jellyfin/androidtv/ui/AsyncImageView.kt | 2 +- .../androidtv/ui/composable/AsyncImage.kt | 2 +- .../androidtv/util/BlurHashDecoder.kt | 132 ++++++++++++++++++ 4 files changed, 134 insertions(+), 133 deletions(-) delete mode 100644 app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt create mode 100644 app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt diff --git a/app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt deleted file mode 100644 index cdd8c54422..0000000000 --- a/app/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.wolt.blurhashkt - -import android.graphics.Bitmap -import android.graphics.Color -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.pow -import kotlin.math.withSign - -object BlurHashDecoder { - - /** - * Decode a blur hash into a new bitmap. - */ - fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { - if (blurHash == null || blurHash.length < 6) { - return null - } - val numCompEnc = decode83(blurHash, 0, 1) - val numCompX = (numCompEnc % 9) + 1 - val numCompY = (numCompEnc / 9) + 1 - val totalComp = numCompX * numCompY - if (blurHash.length != 4 + 2 * totalComp) { - return null - } - val maxAcEnc = decode83(blurHash, 1, 2) - val maxAc = (maxAcEnc + 1) / 166f - val colors = FloatArray(totalComp * 3) - var colorEnc = decode83(blurHash, 2, 6) - decodeDc(colorEnc, colors) - for (i in 1 until totalComp) { - val from = 4 + i * 2 - colorEnc = decode83(blurHash, from, from + 2) - decodeAc(colorEnc, maxAc * punch, colors, i * 3) - } - return composeBitmap(width, height, numCompX, numCompY, colors) - } - - private fun decode83(str: String, from: Int, to: Int): Int { - var result = 0 - for (i in from until to) { - val index = CHARS.indexOf(str[i]) - if (index != -1) { - result = result * 83 + index - } - } - return result - } - - private fun decodeDc(colorEnc: Int, outArray: FloatArray) { - val r = (colorEnc shr 16) and 0xFF - val g = (colorEnc shr 8) and 0xFF - val b = colorEnc and 0xFF - outArray[0] = srgbToLinear(r) - outArray[1] = srgbToLinear(g) - outArray[2] = srgbToLinear(b) - } - - private fun srgbToLinear(colorEnc: Int): Float { - val v = colorEnc / 255f - return if (v <= 0.04045f) { - (v / 12.92f) - } else { - ((v + 0.055f) / 1.055f).pow(2.4f) - } - } - - private fun decodeAc(value: Int, maxAc: Float, outArray: FloatArray, outIndex: Int) { - val r = value / (19 * 19) - val g = (value / 19) % 19 - val b = value % 19 - outArray[outIndex] = signedPow2((r - 9) / 9.0f) * maxAc - outArray[outIndex + 1] = signedPow2((g - 9) / 9.0f) * maxAc - outArray[outIndex + 2] = signedPow2((b - 9) / 9.0f) * maxAc - } - - private fun signedPow2(value: Float) = (value * value).withSign(value) - - private fun composeBitmap( - width: Int, height: Int, - numCompX: Int, numCompY: Int, - colors: FloatArray - ): Bitmap { - // use an array for better performance when writing pixel colors - val imageArray = IntArray(width * height) - val cosinesX = createCosines(width, numCompX) - val cosinesY = if (width == height && numCompX == numCompY) { - cosinesX - } else { - createCosines(height, numCompY) - } - for (y in 0 until height) { - for (x in 0 until width) { - var r = 0f - var g = 0f - var b = 0f - for (j in 0 until numCompY) { - val cosY = cosinesY[y * numCompY + j] - for (i in 0 until numCompX) { - val cosX = cosinesX[x * numCompX + i] - val basis = cosX * cosY - val colorIndex = (j * numCompX + i) * 3 - r += colors[colorIndex] * basis - g += colors[colorIndex + 1] * basis - b += colors[colorIndex + 2] * basis - } - } - imageArray[x + width * y] = - Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) - } - } - return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) - } - - private fun createCosines(size: Int, numComp: Int) = FloatArray(size * numComp) { index -> - val x = index / numComp - val i = index % numComp - cos(PI * x * i / size).toFloat() - } - - private fun linearToSrgb(value: Float): Int { - val v = value.coerceIn(0f, 1f) - return if (v <= 0.0031308f) { - (v * 12.92f * 255f + 0.5f).toInt() - } else { - ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() - } - } - - private const val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" -} diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt b/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt index 9ba97b6a13..326c365a36 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/AsyncImageView.kt @@ -12,11 +12,11 @@ import androidx.lifecycle.lifecycleScope import coil.ImageLoader import coil.request.ImageRequest import coil.transform.CircleCropTransformation -import com.wolt.blurhashkt.BlurHashDecoder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.util.BlurHashDecoder import org.koin.core.component.KoinComponent import org.koin.core.component.inject import kotlin.math.round diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt b/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt index d3c5f088de..2291743853 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/composable/AsyncImage.kt @@ -13,8 +13,8 @@ import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.IntSize import androidx.compose.ui.viewinterop.AndroidView -import com.wolt.blurhashkt.BlurHashDecoder import org.jellyfin.androidtv.ui.AsyncImageView +import org.jellyfin.androidtv.util.BlurHashDecoder private data class AsyncImageState( val url: String?, diff --git a/app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt b/app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt new file mode 100644 index 0000000000..1935d0cbc1 --- /dev/null +++ b/app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt @@ -0,0 +1,132 @@ +package org.jellyfin.androidtv.util + +import android.graphics.Bitmap +import android.graphics.Color +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.withSign + +@Suppress("MagicNumber", "NestedBlockDepth") +object BlurHashDecoder { + + /** + * Decode a blur hash into a new bitmap. + */ + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { + if (blurHash == null || blurHash.length < 6) { + return null + } + val numCompEnc = decode83(blurHash, 0, 1) + val numCompX = (numCompEnc % 9) + 1 + val numCompY = (numCompEnc / 9) + 1 + val totalComp = numCompX * numCompY + if (blurHash.length != 4 + 2 * totalComp) { + return null + } + val maxAcEnc = decode83(blurHash, 1, 2) + val maxAc = (maxAcEnc + 1) / 166f + val colors = FloatArray(totalComp * 3) + var colorEnc = decode83(blurHash, 2, 6) + decodeDc(colorEnc, colors) + for (i in 1 until totalComp) { + val from = 4 + i * 2 + colorEnc = decode83(blurHash, from, from + 2) + decodeAc(colorEnc, maxAc * punch, colors, i * 3) + } + return composeBitmap(width, height, numCompX, numCompY, colors) + } + + private fun decode83(str: String, from: Int, to: Int): Int { + var result = 0 + for (i in from until to) { + val index = CHARS.indexOf(str[i]) + if (index != -1) { + result = result * 83 + index + } + } + return result + } + + private fun decodeDc(colorEnc: Int, outArray: FloatArray) { + val r = (colorEnc shr 16) and 0xFF + val g = (colorEnc shr 8) and 0xFF + val b = colorEnc and 0xFF + outArray[0] = srgbToLinear(r) + outArray[1] = srgbToLinear(g) + outArray[2] = srgbToLinear(b) + } + + private fun srgbToLinear(colorEnc: Int): Float { + val v = colorEnc / 255f + return if (v <= 0.04045f) { + (v / 12.92f) + } else { + ((v + 0.055f) / 1.055f).pow(2.4f) + } + } + + private fun decodeAc(value: Int, maxAc: Float, outArray: FloatArray, outIndex: Int) { + val r = value / (19 * 19) + val g = (value / 19) % 19 + val b = value % 19 + outArray[outIndex] = signedPow2((r - 9) / 9.0f) * maxAc + outArray[outIndex + 1] = signedPow2((g - 9) / 9.0f) * maxAc + outArray[outIndex + 2] = signedPow2((b - 9) / 9.0f) * maxAc + } + + private fun signedPow2(value: Float) = (value * value).withSign(value) + + private fun composeBitmap( + width: Int, height: Int, + numCompX: Int, numCompY: Int, + colors: FloatArray + ): Bitmap { + // use an array for better performance when writing pixel colors + val imageArray = IntArray(width * height) + val cosinesX = createCosines(width, numCompX) + val cosinesY = if (width == height && numCompX == numCompY) { + cosinesX + } else { + createCosines(height, numCompY) + } + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + val cosY = cosinesY[y * numCompY + j] + for (i in 0 until numCompX) { + val cosX = cosinesX[x * numCompX + i] + val basis = cosX * cosY + val colorIndex = (j * numCompX + i) * 3 + r += colors[colorIndex] * basis + g += colors[colorIndex + 1] * basis + b += colors[colorIndex + 2] * basis + } + } + imageArray[x + width * y] = + Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun createCosines(size: Int, numComp: Int) = FloatArray(size * numComp) { index -> + val x = index / numComp + val i = index % numComp + cos(PI * x * i / size).toFloat() + } + + private fun linearToSrgb(value: Float): Int { + val v = value.coerceIn(0f, 1f) + return if (v <= 0.0031308f) { + (v * 12.92f * 255f + 0.5f).toInt() + } else { + ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() + } + } + + private const val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" +} From dd24b6e66df6cce951b9fab07660eac74961ae3d Mon Sep 17 00:00:00 2001 From: Niels van Velzen Date: Tue, 16 Jul 2024 13:50:11 +0200 Subject: [PATCH 3/3] Tweak formatting --- .../androidtv/util/BlurHashDecoder.kt | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt b/app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt index 1935d0cbc1..7cf0157d8b 100644 --- a/app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt +++ b/app/src/main/java/org/jellyfin/androidtv/util/BlurHashDecoder.kt @@ -9,42 +9,43 @@ import kotlin.math.withSign @Suppress("MagicNumber", "NestedBlockDepth") object BlurHashDecoder { + private const val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" /** * Decode a blur hash into a new bitmap. */ fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { - if (blurHash == null || blurHash.length < 6) { - return null - } + if (blurHash == null || blurHash.length < 6) return null + val numCompEnc = decode83(blurHash, 0, 1) val numCompX = (numCompEnc % 9) + 1 val numCompY = (numCompEnc / 9) + 1 val totalComp = numCompX * numCompY - if (blurHash.length != 4 + 2 * totalComp) { - return null - } + if (blurHash.length != 4 + 2 * totalComp) return null + val maxAcEnc = decode83(blurHash, 1, 2) val maxAc = (maxAcEnc + 1) / 166f val colors = FloatArray(totalComp * 3) var colorEnc = decode83(blurHash, 2, 6) decodeDc(colorEnc, colors) + for (i in 1 until totalComp) { val from = 4 + i * 2 colorEnc = decode83(blurHash, from, from + 2) decodeAc(colorEnc, maxAc * punch, colors, i * 3) } + return composeBitmap(width, height, numCompX, numCompY, colors) } private fun decode83(str: String, from: Int, to: Int): Int { var result = 0 + for (i in from until to) { val index = CHARS.indexOf(str[i]) - if (index != -1) { - result = result * 83 + index - } + if (index != -1) result = result * 83 + index } + return result } @@ -52,6 +53,7 @@ object BlurHashDecoder { val r = (colorEnc shr 16) and 0xFF val g = (colorEnc shr 8) and 0xFF val b = colorEnc and 0xFF + outArray[0] = srgbToLinear(r) outArray[1] = srgbToLinear(g) outArray[2] = srgbToLinear(b) @@ -59,10 +61,10 @@ object BlurHashDecoder { private fun srgbToLinear(colorEnc: Int): Float { val v = colorEnc / 255f - return if (v <= 0.04045f) { - (v / 12.92f) - } else { - ((v + 0.055f) / 1.055f).pow(2.4f) + + return when { + v <= 0.04045f -> (v / 12.92f) + else -> ((v + 0.055f) / 1.055f).pow(2.4f) } } @@ -70,6 +72,7 @@ object BlurHashDecoder { val r = value / (19 * 19) val g = (value / 19) % 19 val b = value % 19 + outArray[outIndex] = signedPow2((r - 9) / 9.0f) * maxAc outArray[outIndex + 1] = signedPow2((g - 9) / 9.0f) * maxAc outArray[outIndex + 2] = signedPow2((b - 9) / 9.0f) * maxAc @@ -77,26 +80,24 @@ object BlurHashDecoder { private fun signedPow2(value: Float) = (value * value).withSign(value) - private fun composeBitmap( - width: Int, height: Int, - numCompX: Int, numCompY: Int, - colors: FloatArray - ): Bitmap { + private fun composeBitmap(width: Int, height: Int, numCompX: Int, numCompY: Int, colors: FloatArray): Bitmap { // use an array for better performance when writing pixel colors val imageArray = IntArray(width * height) val cosinesX = createCosines(width, numCompX) - val cosinesY = if (width == height && numCompX == numCompY) { - cosinesX - } else { - createCosines(height, numCompY) + val cosinesY = when { + width == height && numCompX == numCompY -> cosinesX + else -> createCosines(height, numCompY) } + for (y in 0 until height) { for (x in 0 until width) { var r = 0f var g = 0f var b = 0f + for (j in 0 until numCompY) { val cosY = cosinesY[y * numCompY + j] + for (i in 0 until numCompX) { val cosX = cosinesX[x * numCompX + i] val basis = cosX * cosY @@ -106,27 +107,27 @@ object BlurHashDecoder { b += colors[colorIndex + 2] * basis } } - imageArray[x + width * y] = - Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) } } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) } private fun createCosines(size: Int, numComp: Int) = FloatArray(size * numComp) { index -> val x = index / numComp val i = index % numComp + cos(PI * x * i / size).toFloat() } private fun linearToSrgb(value: Float): Int { val v = value.coerceIn(0f, 1f) - return if (v <= 0.0031308f) { - (v * 12.92f * 255f + 0.5f).toInt() - } else { - ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() + + return when { + v <= 0.0031308f -> (v * 12.92f * 255f + 0.5f).toInt() + else -> ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() } } - - private const val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" }