-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Zhirkevich Alexander Y
authored and
Zhirkevich Alexander Y
committed
Feb 12, 2025
1 parent
c038a21
commit 463c894
Showing
7 changed files
with
315 additions
and
4 deletions.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
example/shared/src/androidMain/kotlin/BlurHashDecoder.android.kt
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import android.graphics.Bitmap | ||
import androidx.compose.ui.graphics.ImageBitmap | ||
import androidx.compose.ui.graphics.asImageBitmap | ||
|
||
internal actual fun ImageBitmap.Companion.fromPixmap( | ||
width: Int, | ||
height: Int, | ||
colors: IntArray | ||
) : ImageBitmap { | ||
return Bitmap | ||
.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888) | ||
.asImageBitmap() | ||
} |
146 changes: 146 additions & 0 deletions
146
example/shared/src/commonMain/kotlin/BlurHashDecoder.kt
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
|
||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.ImageBitmap | ||
import androidx.compose.ui.graphics.toArgb | ||
import kotlin.math.PI | ||
import kotlin.math.cos | ||
import kotlin.math.pow | ||
import kotlin.math.withSign | ||
|
||
internal object BlurHashDecoder { | ||
|
||
fun decode( | ||
blurHash: String, | ||
width: Int, | ||
height: Int, | ||
punch: Float = 1f | ||
): ImageBitmap { | ||
val numCompEnc = decode83(blurHash, 0, 1) | ||
val numCompX = (numCompEnc % 9) + 1 | ||
val numCompY = (numCompEnc / 9) + 1 | ||
require(blurHash.length == 4 + 2 * numCompX * numCompY) { | ||
"invalid hash" | ||
} | ||
val maxAcEnc = decode83(blurHash, 1, 2) | ||
val maxAc = (maxAcEnc + 1) / 166f | ||
val colors = Array(numCompX * numCompY) { i -> | ||
if (i == 0) { | ||
val colorEnc = decode83(blurHash, 2, 6) | ||
decodeDc(colorEnc) | ||
} else { | ||
val from = 4 + i * 2 | ||
val colorEnc = decode83(blurHash, from, from + 2) | ||
decodeAc(colorEnc, maxAc * punch) | ||
} | ||
} | ||
|
||
return composeBitmap(width, height, numCompX, numCompY, colors) | ||
} | ||
|
||
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { | ||
var result = 0 | ||
for (i in from until to) { | ||
val index = charMap[str[i]] ?: -1 | ||
if (index != -1) { | ||
result = result * 83 + index | ||
} | ||
} | ||
return result | ||
} | ||
|
||
private fun decodeDc(colorEnc: Int): FloatArray { | ||
val r = colorEnc shr 16 | ||
val g = (colorEnc shr 8) and 255 | ||
val b = colorEnc and 255 | ||
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), 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): FloatArray { | ||
val r = value / (19 * 19) | ||
val g = (value / 19) % 19 | ||
val b = value % 19 | ||
return floatArrayOf( | ||
signedPow2((r - 9) / 9.0f) * maxAc, | ||
signedPow2((g - 9) / 9.0f) * maxAc, | ||
signedPow2((b - 9) / 9.0f) * maxAc | ||
) | ||
} | ||
|
||
private fun signedPow2(value: Float) = value.pow(2f).withSign(value) | ||
|
||
private fun composeBitmap( | ||
width: Int, height: Int, | ||
numCompX: Int, numCompY: Int, | ||
colors: Array<FloatArray> | ||
): ImageBitmap { | ||
// use an array for better performance when writing pixel colors | ||
val imageArray = IntArray(width * height) | ||
val cosinesX = DoubleArray(width * numCompY) | ||
val cosinesY = DoubleArray(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) { | ||
for (i in 0 until numCompX) { | ||
val cosX = cosinesX.getCos(i, numCompX, x, width) | ||
val cosY = cosinesY.getCos(j, numCompY, y, height) | ||
val basis = (cosX * cosY).toFloat() | ||
val color = colors[j * numCompX + i] | ||
r += color[0] * basis | ||
g += color[1] * basis | ||
b += color[2] * basis | ||
} | ||
} | ||
imageArray[x + width * y] = | ||
Color(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)).toArgb() | ||
} | ||
} | ||
|
||
return ImageBitmap.fromPixmap(width, height, imageArray) | ||
} | ||
|
||
private fun DoubleArray.getCos( | ||
x: Int, | ||
numComp: Int, | ||
y: Int, | ||
size: Int | ||
): Double { | ||
return cos(PI * y * x / size).also { | ||
this[x + numComp * y] = it | ||
} | ||
} | ||
|
||
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 val charMap = listOf( | ||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', | ||
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', | ||
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', | ||
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', | ||
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' | ||
).mapIndexed { i, c -> c to i }.toMap() | ||
|
||
} | ||
|
||
internal expect fun ImageBitmap.Companion.fromPixmap( | ||
width : Int, height : Int, colors : IntArray | ||
) : ImageBitmap |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
|
||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.geometry.Size | ||
import androidx.compose.ui.graphics.ImageBitmap | ||
import androidx.compose.ui.graphics.drawscope.DrawScope | ||
import androidx.compose.ui.graphics.painter.Painter | ||
import androidx.compose.ui.unit.IntSize | ||
import kotlin.math.roundToInt | ||
import kotlin.math.sqrt | ||
|
||
@Composable | ||
public fun rememberBlurHashPainter( | ||
hash : String, | ||
width : Int = 0, | ||
height : Int = 0, | ||
quality : Float = 1f, | ||
intensity : Float = 1f | ||
) : BlurHashPainter { | ||
return remember(hash, width, height, quality, intensity) { | ||
BlurHashPainter(hash, width, height, quality, intensity) | ||
} | ||
} | ||
|
||
/** | ||
* @param hash blur hash of an image | ||
* @param width image width. Used for intrinsic size only | ||
* @param height image height. Used for intrinsic size only | ||
* @param quality blur quality. Must be positive | ||
* */ | ||
public class BlurHashPainter( | ||
public val hash : String, | ||
width : Int = 0, | ||
height : Int = 0, | ||
private val quality : Float = 1f, | ||
private val intensity : Float = 1f, | ||
) : Painter() { | ||
|
||
override val intrinsicSize: Size = Size( | ||
width.takeIf { it > 0 }?.toFloat() ?: Float.NaN, | ||
height.takeIf { it > 0 }?.toFloat() ?: Float.NaN, | ||
) | ||
|
||
private var cachedBitmap: ImageBitmap? = null | ||
|
||
override fun DrawScope.onDraw() { | ||
val pixmapSize = (3000f * quality.coerceAtLeast(0f)) | ||
.coerceAtLeast(100f) | ||
val targetScale = sqrt(pixmapSize / (size.width * size.height)).coerceAtMost(1f) | ||
|
||
val targetWidth = (size.width * targetScale).roundToInt() | ||
val targetHeight = (size.height * targetScale).roundToInt() | ||
|
||
val bitmap = cachedBitmap?.takeIf { | ||
it.width >= targetWidth && it.width.toFloat() / it.height == targetWidth.toFloat() / targetHeight | ||
} ?: BlurHashDecoder.decode(hash, targetWidth, targetHeight, intensity).also { | ||
cachedBitmap = it | ||
} | ||
|
||
drawImage( | ||
image = bitmap, | ||
dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt()) | ||
) | ||
} | ||
} |
This file contains 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
This file contains 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
This file contains 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
20 changes: 20 additions & 0 deletions
20
example/shared/src/skikoMain/kotlin/BlurHashDecoder.skiko.kt
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
|
||
import androidx.compose.ui.graphics.ImageBitmap | ||
import androidx.compose.ui.graphics.ImageBitmapConfig | ||
import androidx.compose.ui.graphics.asComposeImageBitmap | ||
import androidx.compose.ui.graphics.asSkiaBitmap | ||
|
||
internal actual fun ImageBitmap.Companion.fromPixmap( | ||
width: Int, | ||
height: Int, | ||
colors: IntArray | ||
) : ImageBitmap { | ||
|
||
val bgra = ByteArray(colors.size * 4) { | ||
colors[it / 4].ushr((it % 4) * 8).toByte() | ||
} | ||
|
||
return ImageBitmap(width, height, ImageBitmapConfig.Argb8888) | ||
.asSkiaBitmap().apply { installPixels(bgra) } | ||
.asComposeImageBitmap() | ||
} |