Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Kotlin encoder #55

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Kotlin/demo/build.gradle
Expand Up @@ -26,4 +26,5 @@ android {
dependencies {
implementation project(path: ':lib')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}
41 changes: 35 additions & 6 deletions Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt
@@ -1,24 +1,53 @@
package com.wolt.blurhashapp

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.wolt.blurhashkt.BlurHashDecoder
import com.wolt.blurhashkt.BlurHashEncoder
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val etInput: EditText = findViewById(R.id.etInput)
val ivResult: ImageView = findViewById(R.id.ivResult)
findViewById<View>(R.id.tvDecode).setOnClickListener {

btnDecode.setOnClickListener {
val bitmap = BlurHashDecoder.decode(etInput.text.toString(), 20, 12)
ivResult.setImageBitmap(bitmap)
}

val buttons = listOf(btnEncode1, btnEncode2, btnEncode3, btnEncode4, btnEncode5)
val drawableResList = listOf(R.drawable.img1, R.drawable.img2, R.drawable.img3, R.drawable.img4, R.drawable.img5)
val onClickListener = View.OnClickListener {
val bitmap = drawableToBitmap(ContextCompat.getDrawable(this, drawableResList[buttons.indexOf(it)])!!)
etInput.setText(BlurHashEncoder.encode(bitmap))
}
for (button in buttons) {
button.setOnClickListener(onClickListener)
}
}

private fun drawableToBitmap(drawable: Drawable): Bitmap {
return drawableToBitmap(drawable, drawable.intrinsicWidth,
drawable.intrinsicHeight)
}

private fun drawableToBitmap(drawable: Drawable, w: Int, h: Int): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}

}
Binary file added Kotlin/demo/src/main/res/drawable-nodpi/img1.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Kotlin/demo/src/main/res/drawable-nodpi/img3.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Kotlin/demo/src/main/res/drawable-nodpi/img4.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Kotlin/demo/src/main/res/drawable-nodpi/img5.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
115 changes: 94 additions & 21 deletions Kotlin/demo/src/main/res/layout/activity_main.xml
@@ -1,43 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">

<EditText
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:autofillHints="@null"
android:gravity="center_horizontal"
android:hint="@string/hint_blurhash"
android:inputType="text"
android:singleLine="true"
android:text="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
android:textColor="@color/colorAccent" />
android:maxLines="1"
android:text="@string/demo_blurhash_string"
android:textColor="@color/textColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tvDecode"
android:layout_width="wrap_content"
<Button
android:id="@+id/btnDecode"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:background="@color/colorPrimary"
android:elevation="8dp"
android:paddingStart="12dp"
android:paddingTop="8dp"
android:paddingEnd="12dp"
android:paddingBottom="8dp"
android:text="@string/title_button_decode"
android:textColor="@color/colorAccent"
android:textSize="16sp" />
android:textColor="@color/textColor"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@id/btnEncode1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etInput" />

<Button
android:id="@+id/btnEncode1"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
hearsilent marked this conversation as resolved.
Show resolved Hide resolved
android:layout_marginTop="12dp"
android:layout_marginRight="8dp"
android:text="@string/title_button_encode1"
android:textColor="@color/textColor"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@id/btnEncode2"
app:layout_constraintStart_toEndOf="@id/btnDecode"
app:layout_constraintTop_toBottomOf="@id/etInput" />

<Button
android:id="@+id/btnEncode2"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_button_encode2"
android:textColor="@color/textColor"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btnEncode1"
app:layout_constraintTop_toBottomOf="@id/etInput" />

<Button
android:id="@+id/btnEncode3"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/title_button_encode3"
android:textColor="@color/textColor"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@id/btnEncode4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnDecode" />

<Button
android:id="@+id/btnEncode4"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginRight="8dp"
android:text="@string/title_button_encode4"
android:textColor="@color/textColor"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@id/btnEncode5"
app:layout_constraintStart_toEndOf="@id/btnEncode3"
app:layout_constraintTop_toBottomOf="@id/btnDecode" />

<Button
android:id="@+id/btnEncode5"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/title_button_encode5"
android:textColor="@color/textColor"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btnEncode4"
app:layout_constraintTop_toBottomOf="@id/btnDecode" />

<ImageView
android:id="@+id/ivResult"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:adjustViewBounds="true" />
android:adjustViewBounds="true"
android:contentDescription="@null"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnEncode5" />

</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
4 changes: 3 additions & 1 deletion Kotlin/demo/src/main/res/values/colors.xml
Expand Up @@ -2,6 +2,8 @@
<resources>
<color name="colorPrimary">#29b6f6</color>
<color name="colorPrimaryDark">#0086c3</color>
<color name="colorAccent">#444444</color>
<color name="colorAccent">#29b6f6</color>

<color name="textColor">#444</color>
</resources>

7 changes: 7 additions & 0 deletions Kotlin/demo/src/main/res/values/strings.xml
Expand Up @@ -2,4 +2,11 @@
<string name="app_name">BlurHash</string>
<string name="hint_blurhash">BlurHash string</string>
<string name="title_button_decode">Decode!</string>
<string name="title_button_encode1">Encode 1!</string>
<string name="title_button_encode2">Encode 2!</string>
<string name="title_button_encode3">Encode 3!</string>
<string name="title_button_encode4">Encode 4!</string>
<string name="title_button_encode5">Encode 5!</string>

<string name="demo_blurhash_string">LEHV6nWB2yk8pyo0adR*.7kCMdnj</string>
</resources>
2 changes: 1 addition & 1 deletion Kotlin/lib/build.gradle
@@ -1,6 +1,6 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {

Expand Down
34 changes: 34 additions & 0 deletions Kotlin/lib/src/main/java/com/wolt/blurhashkt/Base83.kt
@@ -0,0 +1,34 @@
package com.wolt.blurhashkt

internal object Base83 {

private val CHAR_MAP = 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', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)

fun encode(value: Int, length: Int, buffer: CharArray, offset: Int) {
var exp = 1
var i = 1
while (i <= length) {
val digit = (value / exp % 83)
buffer[offset + length - i] = CHAR_MAP[digit]
i++
exp *= 83
}
}

fun decode(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = CHAR_MAP.indexOf(str[i])
if (index != -1) {
result = result * 83 + index
}
}
return result
}
}
54 changes: 7 additions & 47 deletions Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt
Expand Up @@ -2,65 +2,46 @@ package com.wolt.blurhashkt

import android.graphics.Bitmap
import android.graphics.Color
import com.wolt.blurhashkt.Utils.linearToSrgb
import com.wolt.blurhashkt.Utils.signedPow2
import com.wolt.blurhashkt.Utils.srgbToLinear
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign

object BlurHashDecoder {

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 numCompEnc = Base83.decode(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAcEnc = Base83.decode(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
val colorEnc = Base83.decode(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
val colorEnc = Base83.decode(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
Expand All @@ -72,8 +53,6 @@ object BlurHashDecoder {
)
}

private fun signedPow2(value: Float) = value.pow(2f).withSign(value)

private fun composeBitmap(
width: Int, height: Int,
numCompX: Int, numCompY: Int,
Expand All @@ -100,23 +79,4 @@ object BlurHashDecoder {
return bitmap
}

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()

}