-
-
Save romainguy/da7118a6543b0386b577c872756f812b to your computer and use it in GitHub Desktop.
Optimized blur hash decoder from https://github.com/woltapp/blurhash
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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