Skip to content

Instantly share code, notes, and snippets.

@romainguy
Last active March 1, 2026 13:47
Show Gist options
  • Select an option

  • Save romainguy/da7118a6543b0386b577c872756f812b to your computer and use it in GitHub Desktop.

Select an option

Save romainguy/da7118a6543b0386b577c872756f812b to your computer and use it in GitHub Desktop.
Optimized blur hash decoder from https://github.com/woltapp/blurhash
package com.wolt.blurhashkt
import android.graphics.Bitmap
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.withSign
@Suppress("NOTHING_TO_INLINE")
object BlurHashDecoder {
private val SrgbToLinear = FloatArray(256) {
srgbToLinear(it)
}
// Note: We could use as many entries as we want to increase precision
private val LinearToSrgb = IntArray(256) {
linearToSrgb(it / 255f)
}
/**
* 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
val lut = IndexOfChars
for (i in from until to) {
result = result * 83 + lut[str[i].code]
}
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
val lut = SrgbToLinear
outArray[0] = lut[r]
outArray[1] = lut[g]
outArray[2] = lut[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) {
// ART gets rid of the modulos
val d = value / 19
val r = d / 19
val g = d % 19
val b = value % 19
val s = maxAc * (1f / (9f * 9f))
outArray[outIndex + 0] = signedPow2((r - 9).toFloat()) * s
outArray[outIndex + 1] = signedPow2((g - 9).toFloat()) * s
outArray[outIndex + 2] = signedPow2((b - 9).toFloat()) * s
}
private inline fun signedPow2(value: Float) = (value * value).withSign(value)
private fun composeBitmap(
width: Int, height: Int,
numCompX: Int, numCompY: Int,
colors: FloatArray
): Bitmap {
val componentCount = numCompX * numCompY
val cosinesX = FloatArray(width * componentCount)
computeCosinesX(cosinesX, width, numCompX, numCompY, 1f / (width * 2.0f))
val cosinesY = FloatArray(height * componentCount)
computeCosinesY(cosinesY, height, numCompX, numCompY, 1f / (height * 2.0f))
val lut = LinearToSrgb
val imageArray = IntArray(width * height)
for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (i in 0..<componentCount) {
val cosX = cosinesX[x * componentCount + i]
val cosY = cosinesY[y * componentCount + i]
val basis = cosX * cosY
val index = i * 3
r += colors[index + 0] * basis
g += colors[index + 1] * basis
b += colors[index + 2] * basis
}
val red = lut[(r * 255.0f + 0.5f).toInt()]
val green = lut[(g * 255.0f + 0.5f).toInt()]
val blue = lut[(b * 255.0f + 0.5f).toInt()]
imageArray[x + width * y] = 0xff000000.toInt() or (red shl 16) or (green shl 8) or blue
}
}
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
}
private fun linearToSrgb(v: Float): Int {
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()
}
}
inline fun normalizedAngleSin(normalizedDegrees: Float): Float {
val degrees = normalizedDegrees - floor(normalizedDegrees + 0.5f)
val x = abs(2.0f * degrees)
val a = 1.0f - x
return 8.0f * degrees * a / (1.25f - x * a)
}
inline fun normalizedAngleCos(normalizedDegrees: Float): Float =
normalizedAngleSin(normalizedDegrees + 0.25f)
private fun computeCosinesX(
dst: FloatArray,
size: Int,
componentX: Int,
componentY: Int,
divisor: Float
) {
for (x in 0 until size) {
val xIndex = componentX * componentY * x
for (i in 0 until componentX) {
dst[xIndex + i] = normalizedAngleCos(x * i * divisor)
}
for (j in 1 until componentY) {
val index = xIndex + j * componentX
for (i in 0 until componentX) {
dst[index + i] = dst[xIndex + i]
}
}
}
}
private fun computeCosinesY(
dst: FloatArray,
size: Int,
componentX: Int,
componentY: Int,
divisor: Float
) {
for (y in 0 until size) {
val yIndex = componentX * componentY * y
for (i in 0 until componentY) {
val cos = normalizedAngleCos(y * i * divisor)
val index = i * componentX + yIndex
for (j in 0 until componentX) {
dst[index + j] = cos
}
}
}
}
// CHARS.indexOf(charCode)
private val IndexOfChars = byteArrayOf(
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
62,
63,
64,
0,
0,
0,
0,
65,
66,
67,
68,
69,
0,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
70,
71,
0,
72,
0,
73,
74,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
75,
0,
76,
77,
78,
0,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
79,
80,
81,
82,
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment