diff --git a/Kotlin/blurhashkt-lib/build.gradle b/Kotlin/blurhashkt-lib/build.gradle new file mode 100644 index 00000000..a29454aa --- /dev/null +++ b/Kotlin/blurhashkt-lib/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'kotlin-kapt' + id 'maven-publish' +} + +android { + + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 31 + versionCode 2 + versionName "1.0.$versionCode" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + androidTestImplementation "junit:junit:4.13.2" +} + + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + groupId = 'com.github.wolt' + artifactId = 'blurhash-lib' + version = '1.0.2' + } + } + } +} \ No newline at end of file diff --git a/Kotlin/lib/proguard-rules.pro b/Kotlin/blurhashkt-lib/proguard-rules.pro similarity index 100% rename from Kotlin/lib/proguard-rules.pro rename to Kotlin/blurhashkt-lib/proguard-rules.pro diff --git a/Kotlin/lib/src/androidTest/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt b/Kotlin/blurhashkt-lib/src/androidTest/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt similarity index 100% rename from Kotlin/lib/src/androidTest/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt rename to Kotlin/blurhashkt-lib/src/androidTest/java/com/wolt/blurhashkt/BlurHashDecoderTest.kt diff --git a/Kotlin/lib/src/main/AndroidManifest.xml b/Kotlin/blurhashkt-lib/src/main/AndroidManifest.xml similarity index 100% rename from Kotlin/lib/src/main/AndroidManifest.xml rename to Kotlin/blurhashkt-lib/src/main/AndroidManifest.xml diff --git a/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt b/Kotlin/blurhashkt-lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt similarity index 100% rename from Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt rename to Kotlin/blurhashkt-lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt diff --git a/Kotlin/blurhashkt-lib/src/main/java/com/wolt/blurhashkt/BlurHashEncoder.kt b/Kotlin/blurhashkt-lib/src/main/java/com/wolt/blurhashkt/BlurHashEncoder.kt new file mode 100644 index 00000000..5772e7ff --- /dev/null +++ b/Kotlin/blurhashkt-lib/src/main/java/com/wolt/blurhashkt/BlurHashEncoder.kt @@ -0,0 +1,169 @@ +package com.wolt.blurhashkt + +import android.graphics.Bitmap +import java.lang.Math.* +import java.nio.IntBuffer +import kotlin.math.PI +import kotlin.math.pow + +object BlurHashEncoder { + + fun blurHash(bitmap: Bitmap, components: Pair): String? { + if (components.first !in 1..9 || components.second !in 1..9) { + return null + } + + + val width = bitmap.width + val height = bitmap.height + val bytesPerRow = bitmap.rowBytes + var pixels: IntArray? = null + bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) + + if (pixels == null) { + return null + } + + val factors: MutableList> = mutableListOf() + for (y in 0 until components.second) { + for (x in 0 until components.first) { + val normalisation: Float = if (x == 0 && y == 0) 1f else 2f + val factor = + multiplyBasisFunction(pixels, width, height, bytesPerRow, 4, 0) { a, b -> + (normalisation * kotlin.math.cos(PI * x * a / width) * kotlin.math.cos(PI * y * b / height)).toFloat() + } + factors.add(factor) + } + } + + val dc = factors.removeAt(0) + val ac = factors + + var hash = "" + + val sizeFlag = (components.first - 1) + (components.second - 1) * 9 + hash += sizeFlag.encode83(1) + + val maximumValue: Float + if (ac.size > 0) { + val actualMaximumValue = ac.map { it.maxOrNull() }.maxByOrNull { it!! }!! + val quantisedMaximumValue = + 0.0.coerceAtLeast(82.0.coerceAtMost(kotlin.math.floor(actualMaximumValue * 166 - 0.5))) + .toInt() + maximumValue = (quantisedMaximumValue + 1) / 166.0f + hash += quantisedMaximumValue.encode83(1) + } else { + maximumValue = 1f + hash += 0.encode83(1) + } + + hash += encodeDC(dc).encode83(4) + + for (factor in ac) { + hash += encodeAC(factor, maximumValue).encode83(2) + } + + return hash + } + + private fun multiplyBasisFunction( + pixels: IntArray, + width: Int, + height: Int, + bytesPerRow: Int, + bytesPerPixel: Int, + pixelOffset: Int, + basisFunction: (Float, Float) -> Float + ): Array { + var r = 0f + var g = 0f + var b = 0f + + val buffer = IntBuffer.wrap(pixels, pixels.size, height * bytesPerRow) + + for (x in 0 until width) { + for (y in 0 until height) { + val basis = basisFunction(x.toFloat(), y.toFloat()) + r += basis * sRgbToLinear(buffer[bytesPerPixel * x + pixelOffset + 0 + y * bytesPerRow]) + g += basis * sRgbToLinear(buffer[bytesPerPixel * x + pixelOffset + 1 + y * bytesPerRow]) + b += basis * sRgbToLinear(buffer[bytesPerPixel * x + pixelOffset + 2 + y * bytesPerRow]) + } + } + + val scale = 1 / (width * height).toFloat() + + return arrayOf(r * scale, g * scale, b * scale) + } + + + private val encodeCharacters: List = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { it.toString() } + + private fun encodeDC(value: Array): Int { + val roundedR = linearToSRgb(value[0]) + val roundedG = linearToSRgb(value[1]) + val roundedB = linearToSRgb(value[2]) + return (roundedR shl 16) + (roundedG shl 8) + roundedB + } + + private fun encodeAC(value: Array, maximumValue: Float): Int { + 0.0.coerceAtLeast( + 18.0.coerceAtMost( + kotlin.math.floor( + (value[0] / maximumValue.toDouble()).pow(0.5) * 9 + 9.5 + ) + ) + ) + val quantR = 0.0.coerceAtLeast( + 18.0.coerceAtMost( + kotlin.math.floor( + (value[0] / maximumValue.toDouble()).pow(0.5) * 9 + 9.5 + ) + ) + ).toInt() + val quantG = 0.0.coerceAtLeast( + 18.0.coerceAtMost( + kotlin.math.floor( + (value[1] / maximumValue.toDouble()).pow(0.5) * 9 + 9.5 + ) + ) + ).toInt() + val quantB = 0.0.coerceAtLeast( + 18.0.coerceAtMost( + kotlin.math.floor( + (value[2] / maximumValue.toDouble()).pow(0.5) * 9 + 9.5 + ) + ) + ).toInt() + + return quantR * 19 * 19 + quantG * 19 + quantB + } + + private fun sRgbToLinear(value: Int): Float { + val v = value / 255f + return if (v <= 0.04045) (v / 12.92f) else (pow((v + 0.055) / 1.055, 2.4).toFloat()) + } + + private fun linearToSRgb(value: Float): Int { + val v = 0f.coerceAtLeast(1f.coerceAtMost(value)) + return if (v <= 0.0031308f) { + (v * 12.92f * 255 + 0.5f).toInt() + } else { + ((1.055f * v.toDouble().pow(1 / 2.4) - 0.055f) * 255 + 0.5f).toInt() + } + } + + + private fun Int.encode83(length: Int): String { + var result = "" + for (i in 1..length) { + val digit = (this / myPow(83, (length - i))) % 83 + result += encodeCharacters[digit] + } + return result + } + + private fun myPow(base: Int, exponent: Int): Int { + return (0 until exponent).fold(1) { acc, _ -> acc * base } + } +} diff --git a/Kotlin/build.gradle b/Kotlin/build.gradle index b971d828..6e2b8b1f 100644 --- a/Kotlin/build.gradle +++ b/Kotlin/build.gradle @@ -1,7 +1,7 @@ buildscript { - ext.kotlin_version = '1.3.72' + ext.kotlin_version = '1.5.31' repositories { google() @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } diff --git a/Kotlin/demo/build.gradle b/Kotlin/demo/build.gradle index 6ccb0378..23b1cca2 100644 --- a/Kotlin/demo/build.gradle +++ b/Kotlin/demo/build.gradle @@ -1,19 +1,27 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + id 'maven-publish' +} android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { applicationId "com.wolt.blurhash" minSdkVersion 14 - targetSdkVersion 29 - versionCode 1 + targetSdkVersion 31 + versionCode 2 versionName "1.0" } + buildFeatures { + dataBinding true + } + + buildTypes { release { minifyEnabled false @@ -24,6 +32,7 @@ android { } dependencies { - implementation project(path: ':lib') - implementation 'androidx.appcompat:appcompat:1.1.0' + implementation project(path: ':blurhashkt-lib') + implementation "androidx.appcompat:appcompat:1.3.1" } + diff --git a/Kotlin/demo/src/main/AndroidManifest.xml b/Kotlin/demo/src/main/AndroidManifest.xml index 3be7349e..759897a6 100644 --- a/Kotlin/demo/src/main/AndroidManifest.xml +++ b/Kotlin/demo/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com.wolt.blurhashapp"> - + - + - + diff --git a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt index 3d48edba..5402ee91 100644 --- a/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt +++ b/Kotlin/demo/src/main/java/com/wolt/blurhashapp/MainActivity.kt @@ -4,21 +4,27 @@ import android.graphics.Bitmap import android.os.Bundle import android.os.SystemClock import androidx.appcompat.app.AppCompatActivity +import com.wolt.blurhashapp.databinding.ActivityMainBinding import com.wolt.blurhashkt.BlurHashDecoder -import kotlinx.android.synthetic.main.activity_main.* + class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - tvDecode.setOnClickListener { + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.tvDecode.setOnClickListener { var bitmap: Bitmap? = null val time = timed { - bitmap = BlurHashDecoder.decode(etInput.text.toString(), 20, 12) + bitmap = BlurHashDecoder.decode(binding.etInput.text.toString(), 20, 12) } - ivResult.setImageBitmap(bitmap) - ivResultTime.text = "Time: $time ms" + binding.ivResult.setImageBitmap(bitmap) + binding.ivResultTime.text = "Time: $time ms" } } diff --git a/Kotlin/demo/src/main/res/layout/activity_main.xml b/Kotlin/demo/src/main/res/layout/activity_main.xml index 3b22c57d..100d12fa 100644 --- a/Kotlin/demo/src/main/res/layout/activity_main.xml +++ b/Kotlin/demo/src/main/res/layout/activity_main.xml @@ -1,53 +1,57 @@ - + + + + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="24dp"> - + - + - - + + + + + \ No newline at end of file diff --git a/Kotlin/gradle.properties b/Kotlin/gradle.properties index 8de50581..dc6984dd 100644 --- a/Kotlin/gradle.properties +++ b/Kotlin/gradle.properties @@ -6,10 +6,15 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -android.enableJetifier=true -android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +android.enableJetifier=true \ No newline at end of file diff --git a/Kotlin/gradle/wrapper/gradle-wrapper.properties b/Kotlin/gradle/wrapper/gradle-wrapper.properties index 032d0433..db4c5393 100644 --- a/Kotlin/gradle/wrapper/gradle-wrapper.properties +++ b/Kotlin/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Jul 01 10:02:38 EEST 2019 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/Kotlin/jitpack.yml b/Kotlin/jitpack.yml new file mode 100644 index 00000000..1bfc0d7d --- /dev/null +++ b/Kotlin/jitpack.yml @@ -0,0 +1,4 @@ +jdk: + - openjdk11 +before_install: + - ./scripts/prepareJitpackEnvironment.sh \ No newline at end of file diff --git a/Kotlin/lib/build.gradle b/Kotlin/lib/build.gradle deleted file mode 100644 index 35dd930e..00000000 --- a/Kotlin/lib/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-android' - -android { - - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 14 - targetSdkVersion 29 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - androidTestImplementation 'junit:junit:4.13' - androidTestImplementation 'androidx.test:runner:1.2.0' -} diff --git a/Kotlin/settings.gradle b/Kotlin/settings.gradle index 42198e65..7af76dcc 100644 --- a/Kotlin/settings.gradle +++ b/Kotlin/settings.gradle @@ -1 +1 @@ -include ':demo', ':lib' +include ':demo', ':blurhashkt-lib'