feat: Add expect/actual for blurhash and base64Image in commons
- Create PlatformImage expect/actual abstracting Bitmap/BufferedImage - Move pure blurhash algorithms (Base83, SRGB, CosineCache, BlurHashEncoder) to commonMain - Rewrite BlurHashDecoder to return PlatformImage instead of Bitmap - Add PlatformImage.toBlurhash() extension in commonMain - Add platform-specific Bitmap.toBlurhash() and BufferedImage.toBlurhash() - Add base64Image toPlatformImage() for both Android and JVM 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+8
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.commons.base64Image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage
|
||||
import java.util.Base64
|
||||
|
||||
fun Base64Image.toBitmap(content: String): Bitmap {
|
||||
@@ -35,3 +37,9 @@ fun Base64Image.toBitmap(content: String): Bitmap {
|
||||
|
||||
throw Exception("Unable to convert base64 to image $content")
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a base64 image data URI to a PlatformImage.
|
||||
* Delegates to toBitmap and wraps the result.
|
||||
*/
|
||||
fun Base64Image.toPlatformImage(content: String): PlatformImage = toBitmap(content).toPlatformImage()
|
||||
|
||||
+5
-37
@@ -21,41 +21,9 @@
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Bitmap.toBlurhash(): String {
|
||||
val aspectRatio = this.width.toFloat() / this.height.toFloat()
|
||||
|
||||
if (this.width > 100 && this.height > 100) {
|
||||
return Bitmap.createScaledBitmap(this, 100, (100 / aspectRatio).toInt(), false).toBlurhash()
|
||||
}
|
||||
|
||||
val intArray = IntArray(width * height)
|
||||
this.getPixels(intArray, 0, width, 0, 0, width, height)
|
||||
|
||||
val numX =
|
||||
if (aspectRatio > 1) {
|
||||
9
|
||||
} else if (aspectRatio < 1) {
|
||||
(9 * aspectRatio).roundToInt()
|
||||
} else {
|
||||
4
|
||||
}
|
||||
|
||||
val numY =
|
||||
if (aspectRatio > 1) {
|
||||
(9 * (1 / aspectRatio)).roundToInt()
|
||||
} else if (aspectRatio < 1) {
|
||||
9
|
||||
} else {
|
||||
4
|
||||
}
|
||||
|
||||
return BlurHashEncoder().encode(
|
||||
intArray,
|
||||
width,
|
||||
height,
|
||||
numX,
|
||||
numY,
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Encodes this Android Bitmap to a blurhash string.
|
||||
* Delegates to PlatformImage.toBlurhash() for the actual encoding.
|
||||
*/
|
||||
fun Bitmap.toBlurhash(): String = this.toPlatformImage().toBlurhash()
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
actual class PlatformImage(
|
||||
val bitmap: Bitmap,
|
||||
) {
|
||||
actual val width: Int get() = bitmap.width
|
||||
actual val height: Int get() = bitmap.height
|
||||
|
||||
actual fun getPixels(
|
||||
pixels: IntArray,
|
||||
offset: Int,
|
||||
stride: Int,
|
||||
x: Int,
|
||||
y: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
bitmap.getPixels(pixels, offset, stride, x, y, width, height)
|
||||
}
|
||||
|
||||
actual fun scale(
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage = PlatformImage(Bitmap.createScaledBitmap(bitmap, width, height, false))
|
||||
|
||||
actual companion object {
|
||||
actual fun create(
|
||||
pixels: IntArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage = PlatformImage(Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension to convert Android Bitmap to PlatformImage.
|
||||
*/
|
||||
fun Bitmap.toPlatformImage(): PlatformImage = PlatformImage(this)
|
||||
|
||||
/**
|
||||
* Extension to get the underlying Android Bitmap.
|
||||
*/
|
||||
fun PlatformImage.toAndroidBitmap(): Bitmap = this.bitmap
|
||||
+3
-4
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.linearToSrgb
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.SRGB.Companion.srgbToLinear
|
||||
import kotlin.math.pow
|
||||
@@ -66,7 +65,7 @@ object BlurHashDecoder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a blur hash into a new bitmap.
|
||||
* Decode a blur hash into a new PlatformImage.
|
||||
*
|
||||
* @param useCache use in memory cache for the calculated math, reused by images with same size.
|
||||
* if the cache does not exist yet it will be created and populated with new calculations. By
|
||||
@@ -76,7 +75,7 @@ object BlurHashDecoder {
|
||||
blurHash: String?,
|
||||
width: Int,
|
||||
useCache: Boolean = true,
|
||||
): Bitmap? {
|
||||
): PlatformImage? {
|
||||
if (blurHash == null || blurHash.length < 6) {
|
||||
return null
|
||||
}
|
||||
@@ -87,7 +86,7 @@ object BlurHashDecoder {
|
||||
val height = (width * (1 / (numCompX.toFloat() / numCompY.toFloat()))).roundToInt()
|
||||
val colors = computeColors(numCompX, numCompY, blurHash)
|
||||
val imageArray = composeImageArray(width, height, numCompX, numCompY, colors, useCache)
|
||||
return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888)
|
||||
return PlatformImage.create(imageArray, width, height)
|
||||
}
|
||||
|
||||
private fun decodeDc(colorEnc: Int): FloatArray {
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Encodes this PlatformImage to a blurhash string.
|
||||
* The image will be scaled down if larger than 100x100 for performance.
|
||||
*/
|
||||
fun PlatformImage.toBlurhash(): String {
|
||||
val aspectRatio = this.width.toFloat() / this.height.toFloat()
|
||||
|
||||
if (this.width > 100 && this.height > 100) {
|
||||
return this.scale(100, (100 / aspectRatio).toInt()).toBlurhash()
|
||||
}
|
||||
|
||||
val intArray = IntArray(width * height)
|
||||
this.getPixels(intArray, 0, width, 0, 0, width, height)
|
||||
|
||||
val numX =
|
||||
if (aspectRatio > 1) {
|
||||
9
|
||||
} else if (aspectRatio < 1) {
|
||||
(9 * aspectRatio).roundToInt()
|
||||
} else {
|
||||
4
|
||||
}
|
||||
|
||||
val numY =
|
||||
if (aspectRatio > 1) {
|
||||
(9 * (1 / aspectRatio)).roundToInt()
|
||||
} else if (aspectRatio < 1) {
|
||||
9
|
||||
} else {
|
||||
4
|
||||
}
|
||||
|
||||
return BlurHashEncoder().encode(
|
||||
intArray,
|
||||
width,
|
||||
height,
|
||||
numX,
|
||||
numY,
|
||||
)
|
||||
}
|
||||
+3
-2
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
|
||||
object CosineCache {
|
||||
@@ -54,7 +55,7 @@ object CosineCache {
|
||||
DoubleArray(height * numCompY) {
|
||||
val y = it / numCompY
|
||||
val j = it % numCompY
|
||||
cos(Math.PI * y * j / height)
|
||||
cos(PI * y * j / height)
|
||||
}.also {
|
||||
cacheCosinesY.put(height * numCompY, it)
|
||||
}
|
||||
@@ -73,7 +74,7 @@ object CosineCache {
|
||||
DoubleArray(width * numCompX) {
|
||||
val x = it / numCompX
|
||||
val i = it % numCompX
|
||||
cos(Math.PI * x * i / width)
|
||||
cos(PI * x * i / width)
|
||||
}.also { cacheCosinesX.put(width * numCompX, it) }
|
||||
}
|
||||
else -> cacheCosinesX[width * numCompX]!!
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
/**
|
||||
* Platform-agnostic image wrapper for blurhash encoding/decoding.
|
||||
*
|
||||
* On Android: wraps android.graphics.Bitmap
|
||||
* On Desktop JVM: wraps java.awt.image.BufferedImage
|
||||
*/
|
||||
expect class PlatformImage {
|
||||
val width: Int
|
||||
val height: Int
|
||||
|
||||
/**
|
||||
* Read ARGB pixels into the provided array.
|
||||
* Pixels are in ARGB format (0xAARRGGBB).
|
||||
*/
|
||||
fun getPixels(
|
||||
pixels: IntArray,
|
||||
offset: Int,
|
||||
stride: Int,
|
||||
x: Int,
|
||||
y: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a scaled copy of this image.
|
||||
*/
|
||||
fun scale(
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Create a new image from ARGB pixel array.
|
||||
*/
|
||||
fun create(
|
||||
pixels: IntArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.base64Image
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.Base64
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
/**
|
||||
* Converts a base64 image data URI to a PlatformImage (BufferedImage wrapper).
|
||||
*/
|
||||
fun Base64Image.toPlatformImage(content: String): PlatformImage {
|
||||
val matcher = pattern.matcher(content)
|
||||
|
||||
if (matcher.find()) {
|
||||
val base64String = matcher.group(2)
|
||||
val byteArray = Base64.getDecoder().decode(base64String)
|
||||
val bufferedImage =
|
||||
ImageIO.read(ByteArrayInputStream(byteArray))
|
||||
?: throw Exception("Unable to decode base64 image: $content")
|
||||
return bufferedImage.toPlatformImage()
|
||||
}
|
||||
|
||||
throw Exception("Unable to convert base64 to image $content")
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
/**
|
||||
* Encodes this BufferedImage to a blurhash string.
|
||||
* Delegates to PlatformImage.toBlurhash() for the actual encoding.
|
||||
*/
|
||||
fun BufferedImage.toBlurhash(): String = this.toPlatformImage().toBlurhash()
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.blurhash
|
||||
|
||||
import java.awt.Image
|
||||
import java.awt.image.BufferedImage
|
||||
|
||||
actual class PlatformImage(
|
||||
val image: BufferedImage,
|
||||
) {
|
||||
actual val width: Int get() = image.width
|
||||
actual val height: Int get() = image.height
|
||||
|
||||
actual fun getPixels(
|
||||
pixels: IntArray,
|
||||
offset: Int,
|
||||
stride: Int,
|
||||
x: Int,
|
||||
y: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
image.getRGB(x, y, width, height, pixels, offset, stride)
|
||||
}
|
||||
|
||||
actual fun scale(
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage {
|
||||
val scaled = image.getScaledInstance(width, height, Image.SCALE_FAST)
|
||||
val buffered = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
|
||||
buffered.graphics.drawImage(scaled, 0, 0, null)
|
||||
return PlatformImage(buffered)
|
||||
}
|
||||
|
||||
actual companion object {
|
||||
actual fun create(
|
||||
pixels: IntArray,
|
||||
width: Int,
|
||||
height: Int,
|
||||
): PlatformImage {
|
||||
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
|
||||
image.setRGB(0, 0, width, height, pixels, 0, width)
|
||||
return PlatformImage(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension to convert BufferedImage to PlatformImage.
|
||||
*/
|
||||
fun BufferedImage.toPlatformImage(): PlatformImage = PlatformImage(this)
|
||||
|
||||
/**
|
||||
* Extension to get the underlying BufferedImage.
|
||||
*/
|
||||
fun PlatformImage.toBufferedImage(): BufferedImage = this.image
|
||||
Reference in New Issue
Block a user