├── .editorconfig ├── .github ├── funding.yml └── workflows │ ├── build.yml │ ├── publish-release.yml │ └── publish-snapshot.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── blurhash.podspec ├── blurhash ├── .gitignore ├── api │ └── current.txt ├── blurhash.podspec ├── build.gradle.kts ├── gradle.properties ├── images │ ├── badge-blurred.png │ ├── badge.png │ ├── black-blurred.png │ ├── black.png │ ├── flag-blurred.png │ ├── flag.png │ ├── lorikeet-blurred.png │ ├── lorikeet.jpg │ ├── road-blurred.png │ ├── road.png │ ├── small1x1-blurred.png │ ├── small1x1.png │ ├── website1-blurred.png │ ├── website1.jpg │ ├── website2-blurred.png │ ├── website2.jpg │ ├── website3-blurred.png │ ├── website3.jpg │ ├── website4-blurred.png │ ├── website4.jpg │ ├── white-blurred.png │ └── white.png └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ └── BlurHash.kt │ ├── androidUnitTest │ └── kotlin │ │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ └── BlurHashTest.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ ├── Base83.kt │ │ ├── BlurHash.kt │ │ ├── CommonBlurHash.kt │ │ ├── PixelReader.kt │ │ ├── PixelReaderArgb8888.kt │ │ ├── PixelWriter.kt │ │ ├── PixelWriterArgb8888.kt │ │ └── Utils.kt │ ├── commonTest │ └── kotlin │ │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ └── Base83Test.kt │ ├── iosMain │ └── kotlin │ │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ └── BlurHash.kt │ ├── jvmMain │ └── kotlin │ │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ └── BlurHash.kt │ ├── jvmTest │ └── kotlin │ │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ └── BlurHashTest.kt │ └── test │ └── snapshots │ └── images │ └── com.vanniktech.blurhash_BlurHashTest_encodeDecode.png ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lint.xml ├── renovate.json ├── sample-android.png ├── sample-android ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── vanniktech │ │ └── blurhash │ │ └── sample │ │ └── android │ │ ├── BlurHashApplication.kt │ │ └── BlurHashMainActivity.kt │ └── res │ ├── drawable-nodpi │ └── blueberries.jpg │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-v23 │ └── themes.xml │ ├── values-v27 │ └── themes.xml │ └── values │ ├── strings.xml │ └── styles.xml ├── sample-ios.png ├── sample-ios ├── Podfile ├── Podfile.lock ├── ios.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── ios.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── ios │ ├── App.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── blueberries.imageset │ │ ├── Contents.json │ │ └── blueberries.jpg │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── sample-jvm.png ├── sample-jvm ├── build.gradle.kts ├── images │ ├── blueberries-blurred.png │ └── blueberries.jpg └── src │ └── main │ └── java │ └── com │ └── vanniktech │ └── blurhash │ └── sample │ └── jvm │ └── BlurHashJvm.kt └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ktlint_code_style=intellij_idea 3 | indent_size=2 4 | continuation_indent_size=2 5 | ij_kotlin_allow_trailing_comma=true 6 | ij_kotlin_allow_trailing_comma_on_call_site=true 7 | insert_final_newline=true 8 | ktlint_standard_annotation=disabled 9 | ktlint_standard_max-line-length=disabled 10 | ktlint_standard_filename=disabled 11 | ktlint_standard_spacing-between-declarations-with-annotations=disabled 12 | ktlint_standard_blank-line-between-when-conditions=disabled 13 | ktlint_standard_backing-property-naming=disabled 14 | ktlint_standard_kdoc=disabled 15 | ktlint_standard_condition-wrapping=disabled 16 | ktlint_experimental=enabled -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [vanniktech] -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request, merge_group] 4 | 5 | jobs: 6 | build: 7 | name: JDK ${{ matrix.java_version }} 8 | runs-on: macOS-latest 9 | 10 | strategy: 11 | matrix: 12 | java_version: [17] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Gradle Wrapper Validation 19 | uses: gradle/actions/wrapper-validation@v4 20 | 21 | - name: Setup gradle 22 | uses: gradle/gradle-build-action@v3 23 | 24 | - name: Install JDK ${{ matrix.java_version }} 25 | uses: actions/setup-java@v4 26 | with: 27 | distribution: 'zulu' 28 | java-version: ${{ matrix.java_version }} 29 | 30 | - name: Build with Gradle 31 | run: ./gradlew licensee jvmTest ktlint testDebug build --stacktrace 32 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: macOS-latest 12 | if: github.repository == 'vanniktech/blurhash' 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | 18 | - name: Install JDK 17 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'zulu' 22 | java-version: 17 23 | 24 | - name: Setup gradle 25 | uses: gradle/gradle-build-action@v3 26 | 27 | - name: Publish release 28 | run: ./gradlew publishAllPublicationsToMavenCentralRepository 29 | env: 30 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 31 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 32 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} 33 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 34 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: macOS-latest 12 | if: github.repository == 'vanniktech/blurhash' 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | 18 | - name: Install JDK 17 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'zulu' 22 | java-version: 17 23 | 24 | - name: Setup gradle 25 | uses: gradle/gradle-build-action@v3 26 | 27 | - name: Retrieve version 28 | run: | 29 | echo "VERSION_NAME=$(cat gradle.properties | grep -w "VERSION_NAME" | cut -d'=' -f2)" >> $GITHUB_ENV 30 | 31 | - name: Publish snapshot 32 | run: ./gradlew publishAllPublicationsToMavenCentralRepository 33 | if: endsWith(env.VERSION_NAME, '-SNAPSHOT') 34 | env: 35 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 36 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Android Studio 26 | .idea 27 | .gradle 28 | build/ 29 | *.iml 30 | 31 | # iOS 32 | xcuserdata/ 33 | *.ipa 34 | Pods/ 35 | 36 | ios/fastlane/report.xml 37 | ios/fastlane/README.md 38 | *.dSYM.zip 39 | 40 | # Windows thumbnail db 41 | Thumbs.db 42 | 43 | # OSX files 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | Version 0.3.0 *(2024-03-21)* 4 | ---------------------------- 5 | 6 | - API: BlurHash\#averageColor to get the average sRGB color for a blurhash. [\#119](https://github.com/vanniktech/blurhash/pull/119) ([vanniktech](https://github.com/vanniktech)) 7 | - Technical: Use Float instead of Double cache. [\#118](https://github.com/vanniktech/blurhash/pull/118) ([vanniktech](https://github.com/vanniktech)) 8 | 9 | Version 0.2.0 *(2023-12-11)* 10 | ---------------------------- 11 | 12 | - Technical: Kotlin 1.9.21 & Target Android 34. [\#99](https://github.com/vanniktech/blurhash/pull/99) ([vanniktech](https://github.com/vanniktech)) 13 | 14 | Version 0.1.0 *(2022-09-11)* 15 | ---------------------------- 16 | 17 | - Initial release 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Niklas Baudy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | blurhash 2 | ======== 3 | 4 | A Kotlin Multiplatform library to use [blurhash](https://blurha.sh/) in your Android App, iOS / Mac App & JVM Backend. 5 | 6 | | `Android` | `iOS` | `JVM` | 7 | |:--------------------------------------|:------------------------------|:------------------------------| 8 | | ![Sample Android](sample-android.png) | ![Sample iOS](sample-ios.png) | ![Sample JVM](sample-jvm.png) | 9 | 10 | # Why? 11 | 12 | If you've tried using [blurhash](https://blurha.sh/), you qickly stumple upon the [main repository](https://github.com/woltapp/blurhash). They provide sources for [Swift](https://github.com/woltapp/blurhash/tree/master/Swift), [Typescript](https://github.com/woltapp/blurhash/tree/master/TypeScript), [Python](https://github.com/woltapp/blurhash-python), [Kotlin](https://github.com/woltapp/blurhash/tree/master/Kotlin) and [C](https://github.com/woltapp/blurhash/tree/master/C). However: 13 | 14 | - Implementations produce [different hashes for the same picture](https://github.com/woltapp/blurhash/issues/196) 15 | - There are no artifacts to consume i.e. no Cocoa Pod or Maven dependency 16 | - Not all implementations provide both encoding and decoding support 17 | - Missing sample apps with consistent images and blur hashes 18 | 19 | The goal of this library is to solve all of the above mentioned problems, provide a common API and good samples for each platform. 20 | 21 | # Usage 22 | 23 | From Kotlin Multiplatform: 24 | 25 | ```groovy 26 | kotlin { 27 | sourceSets { 28 | val commonMain by getting { 29 | dependencies { 30 | implementation("com.vanniktech:blurhash:0.4.0-SNAPSHOT") 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | From Android / JVM Multiplatform: 38 | 39 | ```groovy 40 | dependencies { 41 | implementation("com.vanniktech:blurhash:0.4.0-SNAPSHOT") 42 | } 43 | ``` 44 | 45 | From iOS: 46 | 47 | ```ruby 48 | pod 'BlurHash', :git => 'https://github.com/vanniktech/blurhash', :tag => "0.4.0-SNAPSHOT" 49 | ``` 50 | 51 | # API 52 | 53 | Use `com.vanniktech.blurhash.BlurHash` directly in your platform specific code to `encode` as well as `decode`: 54 | 55 | - [sample-android](./sample-android/src/main/kotlin/com/vanniktech/blurhash/sample/android/BlurHashMainActivity.kt): Works with `Bitmap` 56 | - [sample-ios](./sample-ios/ios/App.swift): Works with `UIImage` (Use `import blurhash` & `BlurHash.shared`) 57 | - [sample-jvm](sample-jvm/src/main/java/com/vanniktech/blurhash/sample/jvm/BlurHashJvm.kt): Works with `BufferedImage` 58 | 59 | # Thanks 60 | 61 | Without them this would not exist! 62 | 63 | - [woltapp](https://github.com/woltapp) for creating [blurbash](https://github.com/woltapp/blurhash) 64 | - [Hendrik Schnepel](https://github.com/hsch) for the [encoding implementation](https://github.com/hsch/blurhash-java) 65 | 66 | # License 67 | 68 | Copyright (C) 2022 - Niklas Baudy 69 | 70 | Licensed under the MIT License 71 | -------------------------------------------------------------------------------- /blurhash.podspec: -------------------------------------------------------------------------------- 1 | blurhash/blurhash.podspec -------------------------------------------------------------------------------- /blurhash/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /blurhash/api/current.txt: -------------------------------------------------------------------------------- 1 | // Signature format: 4.0 2 | package com.vanniktech.blurhash { 3 | 4 | public final class BlurHash { 5 | method public Integer? averageColor(String blurHash, optional float punch); 6 | method public void clearCache(); 7 | method public android.graphics.Bitmap? decode(String blurHash, int width, int height, optional float punch, optional boolean useCache); 8 | method public String encode(android.graphics.Bitmap bitmap, int componentX, int componentY); 9 | field public static final com.vanniktech.blurhash.BlurHash INSTANCE; 10 | } 11 | 12 | public final class BlurHash { 13 | method public Integer? averageColor(String blurHash, optional float punch); 14 | method public void clearCache(); 15 | method public error.NonExistentClass? decode(String blurHash, error.NonExistentClass width, error.NonExistentClass height, optional float punch, optional boolean useCache); 16 | method public String? encode(error.NonExistentClass uiImage, int componentX, int componentY); 17 | field public static final com.vanniktech.blurhash.BlurHash INSTANCE; 18 | } 19 | 20 | public final class BlurHash { 21 | method public Integer? averageColor(String blurHash, optional float punch); 22 | method public void clearCache(); 23 | method public error.NonExistentClass? decode(String blurHash, int width, int height, optional float punch, optional boolean useCache); 24 | method public String encode(error.NonExistentClass bufferedImage, int componentX, int componentY); 25 | field public static final com.vanniktech.blurhash.BlurHash INSTANCE; 26 | } 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /blurhash/blurhash.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'BlurHash' 3 | spec.version = '0.4.0-SNAPSHOT' 4 | spec.homepage = 'https://github.com/vanniktech/blurhash' 5 | spec.source = { :http=> ''} 6 | spec.authors = 'Niklas Baudy' 7 | spec.license = 'MIT' 8 | spec.summary = 'BlurHash support for iOS, Android and JVM via Kotlin Multiplatform' 9 | spec.vendored_frameworks = 'build/cocoapods/framework/blurhash.framework' 10 | spec.libraries = 'c++' 11 | 12 | 13 | 14 | if !Dir.exist?('build/cocoapods/framework/blurhash.framework') || Dir.empty?('build/cocoapods/framework/blurhash.framework') 15 | raise " 16 | 17 | Kotlin framework 'blurhash' doesn't exist yet, so a proper Xcode project can't be generated. 18 | 'pod install' should be executed after running ':generateDummyFramework' Gradle task: 19 | 20 | ./gradlew :blurhash:generateDummyFramework 21 | 22 | Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" 23 | end 24 | 25 | spec.xcconfig = { 26 | 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO', 27 | } 28 | 29 | spec.pod_target_xcconfig = { 30 | 'KOTLIN_PROJECT_PATH' => ':blurhash', 31 | 'PRODUCT_MODULE_NAME' => 'blurhash', 32 | } 33 | 34 | spec.script_phases = [ 35 | { 36 | :name => 'Build BlurHash', 37 | :execution_position => :before_compile, 38 | :shell_path => '/bin/sh', 39 | :script => <<-SCRIPT 40 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then 41 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" 42 | exit 0 43 | fi 44 | set -ev 45 | REPO_ROOT="$PODS_TARGET_SRCROOT" 46 | "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ 47 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ 48 | -Pkotlin.native.cocoapods.archs="$ARCHS" \ 49 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" 50 | SCRIPT 51 | } 52 | ] 53 | 54 | end -------------------------------------------------------------------------------- /blurhash/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.dokka") 3 | id("org.jetbrains.kotlin.multiplatform") 4 | id("org.jetbrains.kotlin.native.cocoapods") 5 | id("com.android.library") 6 | id("org.jetbrains.kotlin.plugin.parcelize") 7 | id("me.tylerbwong.gradle.metalava") 8 | id("com.vanniktech.maven.publish") 9 | id("app.cash.licensee") 10 | id("app.cash.paparazzi") 11 | } 12 | 13 | licensee { 14 | allow("Apache-2.0") 15 | } 16 | 17 | metalava { 18 | filename.set("api/current.txt") 19 | } 20 | 21 | kotlin { 22 | applyDefaultHierarchyTemplate() 23 | 24 | androidTarget { 25 | publishLibraryVariants("release") 26 | } 27 | jvm() 28 | jvmToolchain(11) 29 | iosX64() 30 | iosArm64() 31 | iosSimulatorArm64() 32 | 33 | targets.withType { 34 | compilations["main"].kotlinOptions.freeCompilerArgs += "-Xexport-kdoc" 35 | } 36 | 37 | sourceSets { 38 | val commonTest by getting { 39 | dependencies { 40 | implementation(libs.kotlin.test.common) 41 | implementation(libs.kotlin.test.annotations.common) 42 | } 43 | } 44 | 45 | val androidUnitTest by getting { 46 | dependencies { 47 | implementation(libs.kotlin.test.junit) 48 | } 49 | } 50 | 51 | val jvmTest by getting { 52 | dependencies { 53 | implementation(libs.kotlin.test.junit) 54 | } 55 | } 56 | } 57 | 58 | cocoapods { 59 | summary = "BlurHash support for iOS, Android and JVM via Kotlin Multiplatform" 60 | homepage = "https://github.com/vanniktech/blurhash" 61 | license = "MIT" 62 | name = "BlurHash" 63 | authors = "Niklas Baudy" 64 | version = project.property("VERSION_NAME").toString() 65 | 66 | framework { 67 | isStatic = true 68 | } 69 | } 70 | } 71 | 72 | android { 73 | namespace = "com.vanniktech.blurhash" 74 | 75 | compileSdk = libs.versions.compileSdk.get().toInt() 76 | 77 | defaultConfig { 78 | minSdk = libs.versions.minSdk.get().toInt() 79 | } 80 | 81 | compileOptions { 82 | sourceCompatibility = JavaVersion.VERSION_11 83 | targetCompatibility = JavaVersion.VERSION_11 84 | } 85 | 86 | resourcePrefix = "blurhash_" 87 | } 88 | 89 | // Workaround https://github.com/cashapp/paparazzi/issues/1231 90 | plugins.withId("app.cash.paparazzi") { 91 | // Defer until afterEvaluate so that testImplementation is created by Android plugin. 92 | afterEvaluate { 93 | dependencies.constraints { 94 | add("testImplementation", "com.google.guava:guava") { 95 | attributes { 96 | attribute( 97 | TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE, 98 | objects.named(TargetJvmEnvironment::class.java, TargetJvmEnvironment.STANDARD_JVM), 99 | ) 100 | } 101 | because( 102 | "LayoutLib and sdk-common depend on Guava's -jre published variant." + 103 | "See https://github.com/cashapp/paparazzi/issues/906.", 104 | ) 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /blurhash/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=BlurHash 2 | POM_ARTIFACT_ID=blurhash 3 | -------------------------------------------------------------------------------- /blurhash/images/badge-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/badge-blurred.png -------------------------------------------------------------------------------- /blurhash/images/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/badge.png -------------------------------------------------------------------------------- /blurhash/images/black-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/black-blurred.png -------------------------------------------------------------------------------- /blurhash/images/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/black.png -------------------------------------------------------------------------------- /blurhash/images/flag-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/flag-blurred.png -------------------------------------------------------------------------------- /blurhash/images/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/flag.png -------------------------------------------------------------------------------- /blurhash/images/lorikeet-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/lorikeet-blurred.png -------------------------------------------------------------------------------- /blurhash/images/lorikeet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/lorikeet.jpg -------------------------------------------------------------------------------- /blurhash/images/road-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/road-blurred.png -------------------------------------------------------------------------------- /blurhash/images/road.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/road.png -------------------------------------------------------------------------------- /blurhash/images/small1x1-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/small1x1-blurred.png -------------------------------------------------------------------------------- /blurhash/images/small1x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/small1x1.png -------------------------------------------------------------------------------- /blurhash/images/website1-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website1-blurred.png -------------------------------------------------------------------------------- /blurhash/images/website1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website1.jpg -------------------------------------------------------------------------------- /blurhash/images/website2-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website2-blurred.png -------------------------------------------------------------------------------- /blurhash/images/website2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website2.jpg -------------------------------------------------------------------------------- /blurhash/images/website3-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website3-blurred.png -------------------------------------------------------------------------------- /blurhash/images/website3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website3.jpg -------------------------------------------------------------------------------- /blurhash/images/website4-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website4-blurred.png -------------------------------------------------------------------------------- /blurhash/images/website4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/website4.jpg -------------------------------------------------------------------------------- /blurhash/images/white-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/white-blurred.png -------------------------------------------------------------------------------- /blurhash/images/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/images/white.png -------------------------------------------------------------------------------- /blurhash/src/androidMain/kotlin/com/vanniktech/blurhash/BlurHash.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | import android.graphics.Bitmap 4 | 5 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 6 | actual object BlurHash { 7 | /** 8 | * Clear in-memory calculations. 9 | * The cache is not big, but will increase when many image sizes are decoded, using [decode]. 10 | * If the app needs memory, it is recommended to clear it. 11 | */ 12 | actual fun clearCache() = CommonBlurHash.clearCache() 13 | 14 | /** 15 | * Returns the average sRGB color for the given [blurHash] in respect to its [punch]. 16 | */ 17 | actual fun averageColor( 18 | blurHash: String, 19 | punch: Float, 20 | ) = CommonBlurHash.averageColor(blurHash, punch) 21 | 22 | /** 23 | * Calculates the blur hash from the given [bitmap]. 24 | * 25 | * [componentX] number of components in the x dimension 26 | * [componentY] number of components in the y dimension 27 | */ 28 | fun encode( 29 | bitmap: Bitmap, 30 | componentX: Int, 31 | componentY: Int, 32 | ): String { 33 | val width = bitmap.width 34 | val height = bitmap.height 35 | val pixels = IntArray(width * height) 36 | bitmap.getPixels(pixels, 0, width, 0, 0, width, height) 37 | 38 | return CommonBlurHash.encode( 39 | pixelReader = PixelReaderArgb8888(pixels = pixels, width = width), 40 | width = width, 41 | height = height, 42 | componentX = componentX, 43 | componentY = componentY, 44 | ) 45 | } 46 | 47 | /** 48 | * If [blurHash] is a valid blur hash, the method will return a [Bitmap], 49 | * with the requested [width] as well as [height]. 50 | * 51 | * [useCache] to control the caching which will improve performance (with a slight memory impact) 52 | */ 53 | fun decode( 54 | blurHash: String, 55 | width: Int, 56 | height: Int, 57 | punch: Float = DEFAULT_PUNCH, 58 | useCache: Boolean = true, 59 | ): Bitmap? { 60 | val pixels = CommonBlurHash.decode( 61 | blurHash = blurHash, 62 | pixelWriter = PixelWriterArgb8888(width = width, height = height), 63 | width = width, 64 | height = height, 65 | punch = punch, 66 | useCache = useCache, 67 | ) 68 | 69 | return pixels?.let { Bitmap.createBitmap(it, width, height, Bitmap.Config.ARGB_8888) } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /blurhash/src/androidUnitTest/kotlin/com/vanniktech/blurhash/BlurHashTest.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | import android.graphics.BitmapFactory 4 | import android.widget.ImageView 5 | import android.widget.LinearLayout 6 | import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 7 | import app.cash.paparazzi.Paparazzi 8 | import org.junit.Rule 9 | import java.io.File 10 | import kotlin.test.Test 11 | import kotlin.test.assertEquals 12 | 13 | class BlurHashTest { 14 | @get:Rule 15 | val paparazzi = Paparazzi( 16 | deviceConfig = PIXEL_5, 17 | theme = "android:Theme.Material.Light.NoActionBar", 18 | ) 19 | 20 | @Test fun encodeDecode() { 21 | val blurHash = BlurHash.encode(BitmapFactory.decodeStream(workspaceDirectory().resolve("sample-android/src/main/res/drawable-nodpi/blueberries.jpg").inputStream()), 5, 4) 22 | assertEquals(expected = "V4BhTMQyJ]iErP8wM^%ht?tboYkPtSiWXtMKSrxcSst7", actual = blurHash) 23 | 24 | val imageView = ImageView(paparazzi.context) 25 | val density = paparazzi.context.resources.displayMetrics.density 26 | val width = (300 * density).toInt() 27 | val height = (200 * density).toInt() 28 | imageView.setImageBitmap(BlurHash.decode(blurHash, width, height)) 29 | paparazzi.snapshot( 30 | LinearLayout(paparazzi.context).apply { 31 | addView(imageView, LinearLayout.LayoutParams(width, height)) 32 | }, 33 | ) 34 | } 35 | 36 | private fun workspaceDirectory() = File(BlurHashTest::class.java.classLoader?.getResource(".")?.file!!) 37 | .parentFile 38 | ?.parentFile 39 | ?.parentFile 40 | ?.parentFile 41 | ?.parentFile!! 42 | } 43 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/Base83.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | internal object Base83 { 4 | val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".toCharArray() 5 | 6 | internal fun encode83( 7 | value: Int, 8 | length: Int, 9 | buffer: CharArray, 10 | offset: Int, 11 | ) { 12 | var exp = 1 13 | var i = 1 14 | while (i <= length) { 15 | val digit = (value / exp % CHARS.size) 16 | buffer[offset + length - i] = CHARS[digit] 17 | i++ 18 | exp *= CHARS.size 19 | } 20 | } 21 | 22 | fun decode83(value: String, from: Int = 0, to: Int = value.length): Int { 23 | var result = 0 24 | val chars = value.toCharArray() 25 | for (i in from until to) { 26 | result = result * CHARS.size + CHARS.indexOf(chars[i]) 27 | } 28 | return result 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/BlurHash.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | internal const val DEFAULT_PUNCH = 1f 4 | 5 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 6 | expect object BlurHash { 7 | /** 8 | * Clears calculations stored in memory cache. 9 | * The cache is not big, but will increase when many image sizes are used, 10 | * If the app needs memory, it is recommended to clear it. 11 | */ 12 | fun clearCache() 13 | 14 | /** 15 | * Returns the average sRGB color for the given [blurHash] in respect to its [punch]. 16 | */ 17 | fun averageColor(blurHash: String, punch: Float = DEFAULT_PUNCH): Int? 18 | } 19 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/CommonBlurHash.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("BlurHash") 2 | 3 | package com.vanniktech.blurhash 4 | 5 | import com.vanniktech.blurhash.Utils.applyBasisFunction 6 | import com.vanniktech.blurhash.Utils.encodeAc 7 | import com.vanniktech.blurhash.Utils.encodeDc 8 | import kotlin.jvm.JvmName 9 | import kotlin.math.PI 10 | import kotlin.math.cos 11 | import kotlin.math.floor 12 | import kotlin.math.roundToInt 13 | 14 | internal class BlurHashInfo internal constructor( 15 | val colors: Array, 16 | val componentX: Int, 17 | val componentY: Int, 18 | ) 19 | 20 | internal object CommonBlurHash { 21 | // Cache Math.cos() calculations to improve performance. 22 | // The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps 23 | // The cache is enabled by default, it is recommended to disable it only when just a few images are displayed 24 | private val cacheCosinesX = HashMap() 25 | private val cacheCosinesY = HashMap() 26 | 27 | internal fun clearCache() { 28 | cacheCosinesX.clear() 29 | cacheCosinesY.clear() 30 | } 31 | 32 | internal fun averageColor( 33 | blurHash: String, 34 | punch: Float, 35 | ): Int? { 36 | val blurHashInfo = info( 37 | blurHash = blurHash, 38 | punch = punch, 39 | ) ?: return null 40 | 41 | val dc = blurHashInfo.colors.firstOrNull() ?: return null 42 | val red = Utils.linearToSrgb(dc[0]) 43 | val green = Utils.linearToSrgb(dc[1]) 44 | val blue = Utils.linearToSrgb(dc[2]) 45 | return 255 shl 24 or (red shl 16) or (green shl 8) or blue 46 | } 47 | 48 | internal fun decode( 49 | blurHash: String, 50 | pixelWriter: PixelWriter, 51 | width: Int, 52 | height: Int, 53 | punch: Float, 54 | useCache: Boolean, 55 | ): T? { 56 | val blurHashInfo = info( 57 | blurHash = blurHash, 58 | punch = punch, 59 | ) ?: return null 60 | 61 | return composePixels( 62 | pixelWriter = pixelWriter, 63 | width = width, 64 | height = height, 65 | componentX = blurHashInfo.componentX, 66 | componentY = blurHashInfo.componentY, 67 | colors = blurHashInfo.colors, 68 | useCache = useCache, 69 | ) 70 | } 71 | 72 | private fun info( 73 | blurHash: String, 74 | punch: Float, 75 | ): BlurHashInfo? { 76 | if (blurHash.length < 6) { 77 | return null 78 | } 79 | 80 | val numCompEnc = Base83.decode83(blurHash, 0, 1) 81 | val componentX = (numCompEnc % 9) + 1 82 | val componentY = (numCompEnc / 9) + 1 83 | 84 | if (blurHash.length != 4 + 2 * componentX * componentY) { 85 | return null 86 | } 87 | 88 | val maxAcEnc = Base83.decode83(blurHash, 1, 2) 89 | val maxAc = (maxAcEnc + 1) / 166f 90 | val colors = Array(componentX * componentY) { i -> 91 | if (i == 0) { 92 | val colorEnc = Base83.decode83(blurHash, 2, 6) 93 | Utils.decodeDc(colorEnc) 94 | } else { 95 | val from = 4 + i * 2 96 | val colorEnc = Base83.decode83(blurHash, from, from + 2) 97 | Utils.decodeAc(colorEnc, maxAc * punch) 98 | } 99 | } 100 | 101 | return BlurHashInfo( 102 | colors = colors, 103 | componentX = componentX, 104 | componentY = componentY, 105 | ) 106 | } 107 | 108 | private fun composePixels( 109 | pixelWriter: PixelWriter, 110 | width: Int, 111 | height: Int, 112 | componentX: Int, 113 | componentY: Int, 114 | colors: Array, 115 | useCache: Boolean, 116 | ): T { 117 | val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * componentX) 118 | val cosinesX = getArrayForCosinesX(calculateCosX, width, componentX) 119 | val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * componentY) 120 | val cosinesY = getArrayForCosinesY(calculateCosY, height, componentY) 121 | for (y in 0 until height) { 122 | for (x in 0 until width) { 123 | var r = 0f 124 | var g = 0f 125 | var b = 0f 126 | for (j in 0 until componentY) { 127 | for (i in 0 until componentX) { 128 | val cosX = cosinesX.getCos(calculateCosX, i, componentX, x, width) 129 | val cosY = cosinesY.getCos(calculateCosY, j, componentY, y, height) 130 | val basis = (cosX * cosY) 131 | val color = colors[j * componentX + i] 132 | r += color[0] * basis 133 | g += color[1] * basis 134 | b += color[2] * basis 135 | } 136 | } 137 | 138 | pixelWriter.write( 139 | x = x, 140 | y = y, 141 | width = width, 142 | red = Utils.linearToSrgb(r), 143 | green = Utils.linearToSrgb(g), 144 | blue = Utils.linearToSrgb(b), 145 | ) 146 | } 147 | } 148 | 149 | return pixelWriter.get() 150 | } 151 | 152 | private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when { 153 | calculate -> FloatArray(height * numCompY).also { cacheCosinesY[height * numCompY] = it } 154 | else -> cacheCosinesY[height * numCompY]!! 155 | } 156 | 157 | private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when { 158 | calculate -> FloatArray(width * numCompX).also { cacheCosinesX[width * numCompX] = it } 159 | else -> cacheCosinesX[width * numCompX]!! 160 | } 161 | 162 | private fun FloatArray.getCos( 163 | calculate: Boolean, 164 | x: Int, 165 | numComp: Int, 166 | y: Int, 167 | size: Int, 168 | ): Float { 169 | val index = x + numComp * y 170 | if (calculate) { 171 | this[index] = cos(PI * y * x / size).toFloat() 172 | } 173 | 174 | return this[index] 175 | } 176 | 177 | internal fun encode( 178 | pixelReader: PixelReader, 179 | width: Int, 180 | height: Int, 181 | componentX: Int, 182 | componentY: Int, 183 | ): String { 184 | require(componentX in 1..9 && componentY in 1..9) { "Blur Hash must have components between 1 and 9" } 185 | 186 | val factors = Array(componentX * componentY) { FloatArray(3) } 187 | 188 | for (y in 0 until componentY) { 189 | for (x in 0 until componentX) { 190 | val normalisation = if (x == 0 && y == 0) 1f else 2f 191 | applyBasisFunction( 192 | pixelReader, 193 | width, 194 | height, 195 | normalisation, 196 | x, 197 | y, 198 | factors, 199 | y * componentX + x, 200 | ) 201 | } 202 | } 203 | 204 | val hash = CharArray(1 + 1 + 4 + 2 * (factors.size - 1)) // size flag + max AC + DC + 2 * AC components 205 | val sizeFlag = (componentX - 1 + (componentY - 1) * 9) 206 | Base83.encode83(sizeFlag, 1, hash, 0) 207 | 208 | val maximumValue: Float 209 | 210 | if (factors.size > 1) { 211 | val actualMaximumValue = Utils.max(factors, 1, factors.size) 212 | val quantisedMaximumValue = floor((actualMaximumValue * 166f - 0.5f).coerceIn(0f, 82f)) 213 | maximumValue = (quantisedMaximumValue + 1) / 166 214 | Base83.encode83(quantisedMaximumValue.roundToInt(), 1, hash, 1) 215 | } else { 216 | maximumValue = 1f 217 | Base83.encode83(0, 1, hash, 1) 218 | } 219 | 220 | val dc = factors[0] 221 | Base83.encode83(encodeDc(dc), 4, hash, 2) 222 | 223 | for (i in 1 until factors.size) { 224 | Base83.encode83(encodeAc(factors[i], maximumValue), 2, hash, 6 + 2 * (i - 1)) 225 | } 226 | 227 | return hash.concatToString() 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/PixelReader.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | internal interface PixelReader { 4 | /** Returns the red components of the pixel at the [x]-[y] coordinate. */ 5 | fun readRed(x: Int, y: Int): Int 6 | 7 | /** Returns the green components of the pixel at the [x]-[y] coordinate. */ 8 | fun readGreen(x: Int, y: Int): Int 9 | 10 | /** Returns the blue components of the pixel at the [x]-[y] coordinate. */ 11 | fun readBlue(x: Int, y: Int): Int 12 | } 13 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/PixelReaderArgb8888.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | internal class PixelReaderArgb8888( 4 | private val pixels: IntArray, 5 | private val width: Int, 6 | ) : PixelReader { 7 | override fun readRed(x: Int, y: Int): Int = pixels[y * width + x] shr 16 and 0xff 8 | 9 | override fun readGreen(x: Int, y: Int): Int = pixels[y * width + x] shr 8 and 0xff 10 | 11 | override fun readBlue(x: Int, y: Int): Int = pixels[y * width + x] and 0xff 12 | } 13 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/PixelWriter.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | /** Counterpart of [PixelReader]. */ 4 | internal interface PixelWriter { 5 | fun write(x: Int, y: Int, width: Int, red: Int, green: Int, blue: Int) 6 | 7 | fun get(): T 8 | } 9 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/PixelWriterArgb8888.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | internal class PixelWriterArgb8888( 4 | width: Int, 5 | height: Int, 6 | ) : PixelWriter { 7 | private val pixels = IntArray(width * height) 8 | 9 | override fun write( 10 | x: Int, 11 | y: Int, 12 | width: Int, 13 | red: Int, 14 | green: Int, 15 | blue: Int, 16 | ) { 17 | pixels[x + width * y] = 0xFF000000.toInt() or (red shl 16) or (green shl 8) or blue 18 | } 19 | 20 | override fun get() = pixels 21 | } 22 | -------------------------------------------------------------------------------- /blurhash/src/commonMain/kotlin/com/vanniktech/blurhash/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | import kotlin.math.PI 4 | import kotlin.math.abs 5 | import kotlin.math.cos 6 | import kotlin.math.floor 7 | import kotlin.math.pow 8 | import kotlin.math.roundToInt 9 | import kotlin.math.withSign 10 | 11 | internal object Utils { 12 | private fun srgbToLinear(value: Int): Float { 13 | val v = value / 255f 14 | return if (v <= 0.04045f) { 15 | v / 12.92f 16 | } else { 17 | ((v + 0.055f) / 1.055f).pow(2.4f) 18 | } 19 | } 20 | 21 | internal fun linearToSrgb(value: Float): Int { 22 | val v = value.coerceIn(0f, 1f) 23 | return if (v <= 0.0031308f) { 24 | (v * 12.92f * 255f + 0.5f).toInt() 25 | } else { 26 | ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() 27 | } 28 | } 29 | 30 | private fun signPow(value: Float, exp: Float) = abs(value).pow(exp).withSign(value) 31 | 32 | internal fun max( 33 | values: Array, 34 | from: Int, 35 | endExclusive: Int, 36 | ): Float { 37 | var result = Float.NEGATIVE_INFINITY 38 | for (i in from until endExclusive) { 39 | for (j in values[i].indices) { 40 | val value = values[i][j] 41 | if (value > result) { 42 | result = value 43 | } 44 | } 45 | } 46 | return result 47 | } 48 | 49 | internal fun encodeAc( 50 | value: FloatArray, 51 | maximumValue: Float, 52 | ): Int { 53 | val quantR = floor((signPow(value[0] / maximumValue, 0.5f) * 9f + 9.5f).coerceIn(0f, 18f)) 54 | val quantG = floor((signPow(value[1] / maximumValue, 0.5f) * 9f + 9.5f).coerceIn(0f, 18f)) 55 | val quantB = floor((signPow(value[2] / maximumValue, 0.5f) * 9f + 9.5f).coerceIn(0f, 18f)) 56 | return (quantR * 19 * 19 + quantG * 19 + quantB).roundToInt() 57 | } 58 | 59 | internal fun decodeAc(value: Int, maxAc: Float): FloatArray { 60 | val r = value / (19 * 19) 61 | val g = (value / 19) % 19 62 | val b = value % 19 63 | return floatArrayOf( 64 | signPow((r - 9) / 9f, 2f) * maxAc, 65 | signPow((g - 9) / 9f, 2f) * maxAc, 66 | signPow((b - 9) / 9f, 2f) * maxAc, 67 | ) 68 | } 69 | 70 | internal fun applyBasisFunction( 71 | pixelReader: PixelReader, 72 | width: Int, 73 | height: Int, 74 | normalisation: Float, 75 | i: Int, 76 | j: Int, 77 | factors: Array, 78 | index: Int, 79 | ) { 80 | var r = 0f 81 | var g = 0f 82 | var b = 0f 83 | 84 | for (x in 0 until width) { 85 | for (y in 0 until height) { 86 | val basis = (normalisation * cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat() 87 | r += basis * srgbToLinear(pixelReader.readRed(x = x, y = y)) 88 | g += basis * srgbToLinear(pixelReader.readGreen(x = x, y = y)) 89 | b += basis * srgbToLinear(pixelReader.readBlue(x = x, y = y)) 90 | } 91 | } 92 | 93 | val scale = 1f / (width * height) 94 | factors[index][0] = r * scale 95 | factors[index][1] = g * scale 96 | factors[index][2] = b * scale 97 | } 98 | 99 | internal fun encodeDc(value: FloatArray): Int { 100 | val r = linearToSrgb(value[0]) 101 | val g = linearToSrgb(value[1]) 102 | val b = linearToSrgb(value[2]) 103 | return (r shl 16) + (g shl 8) + b 104 | } 105 | 106 | internal fun decodeDc(colorEnc: Int): FloatArray { 107 | val r = (colorEnc shr 16) and 255 108 | val g = (colorEnc shr 8) and 255 109 | val b = colorEnc and 255 110 | return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /blurhash/src/commonTest/kotlin/com/vanniktech/blurhash/Base83Test.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class Base83Test { 7 | @Test fun singleDigits() { 8 | for (i in 0..82) { 9 | val expected = Base83.CHARS.concatToString(i, i + 1) 10 | 11 | assertEquals( 12 | message = "$i encodes", 13 | expected = expected, 14 | actual = encode83(i, 1), 15 | ) 16 | } 17 | } 18 | 19 | @Test fun encode0000() { 20 | assertEquals( 21 | expected = "0000", 22 | actual = encode83(0, 4), 23 | ) 24 | } 25 | 26 | @Test fun encode0001() { 27 | assertEquals( 28 | expected = "0001", 29 | actual = encode83(1, 4), 30 | ) 31 | } 32 | 33 | @Test fun encode0010() { 34 | assertEquals( 35 | expected = "0010", 36 | actual = encode83(83, 4), 37 | ) 38 | } 39 | 40 | @Test fun encode0011() { 41 | assertEquals( 42 | expected = "0011", 43 | actual = encode83((83 + 1), 4), 44 | ) 45 | } 46 | 47 | @Test fun encode00X0() { 48 | assertEquals( 49 | expected = "00~0", 50 | actual = encode83((83 * 82), 4), 51 | ) 52 | } 53 | 54 | @Test fun encode0100() { 55 | assertEquals( 56 | expected = "0100", 57 | actual = encode83((83 * 83), 4), 58 | ) 59 | } 60 | 61 | @Test fun encode00XX() { 62 | assertEquals( 63 | expected = "00~~", 64 | actual = encode83((83 * 82 + 82), 4), 65 | ) 66 | } 67 | 68 | @Test fun decode0XXX() { 69 | assertEquals( 70 | expected = (82 + 82 * 83 + 82 * 83 * 83), 71 | actual = Base83.decode83("0~~~"), 72 | ) 73 | } 74 | 75 | private fun encode83( 76 | value: Int, 77 | length: Int, 78 | ): String { 79 | val buffer = CharArray(length) 80 | Base83.encode83(value, length, buffer, 0) 81 | return buffer.concatToString() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /blurhash/src/iosMain/kotlin/com/vanniktech/blurhash/BlurHash.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | import kotlinx.cinterop.get 4 | import kotlinx.cinterop.readValue 5 | import kotlinx.cinterop.set 6 | import kotlinx.cinterop.useContents 7 | import platform.CoreFoundation.CFDataCreateMutable 8 | import platform.CoreFoundation.CFDataGetBytePtr 9 | import platform.CoreFoundation.CFDataGetMutableBytePtr 10 | import platform.CoreFoundation.CFDataSetLength 11 | import platform.CoreFoundation.kCFAllocatorDefault 12 | import platform.CoreGraphics.CGBitmapContextCreateImage 13 | import platform.CoreGraphics.CGBitmapContextCreateWithData 14 | import platform.CoreGraphics.CGColorRenderingIntent 15 | import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB 16 | import platform.CoreGraphics.CGColorSpaceCreateWithName 17 | import platform.CoreGraphics.CGContextScaleCTM 18 | import platform.CoreGraphics.CGContextTranslateCTM 19 | import platform.CoreGraphics.CGDataProviderCopyData 20 | import platform.CoreGraphics.CGDataProviderCreateWithCFData 21 | import platform.CoreGraphics.CGFloat 22 | import platform.CoreGraphics.CGImageAlphaInfo 23 | import platform.CoreGraphics.CGImageCreate 24 | import platform.CoreGraphics.CGImageGetBitsPerPixel 25 | import platform.CoreGraphics.CGImageGetBytesPerRow 26 | import platform.CoreGraphics.CGImageGetDataProvider 27 | import platform.CoreGraphics.CGImageGetHeight 28 | import platform.CoreGraphics.CGImageGetWidth 29 | import platform.CoreGraphics.CGPointZero 30 | import platform.CoreGraphics.kCGColorSpaceSRGB 31 | import platform.UIKit.UIGraphicsPopContext 32 | import platform.UIKit.UIGraphicsPushContext 33 | import platform.UIKit.UIImage 34 | import kotlin.math.roundToLong 35 | 36 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 37 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) 38 | actual object BlurHash { 39 | /** 40 | * Clear in-memory calculations. 41 | * The cache is not big, but will increase when many image sizes are decoded, using [decode]. 42 | * If the app needs memory, it is recommended to clear it. 43 | */ 44 | actual fun clearCache() = CommonBlurHash.clearCache() 45 | 46 | /** 47 | * Returns the average sRGB color for the given [blurHash] in respect to its [punch]. 48 | */ 49 | actual fun averageColor( 50 | blurHash: String, 51 | punch: Float, 52 | ) = CommonBlurHash.averageColor(blurHash, punch) 53 | 54 | /** 55 | * Calculates the blur hash from the given [uiImage]. 56 | * 57 | * [componentX] number of components in the x dimension 58 | * [componentY] number of components in the y dimension 59 | */ 60 | fun encode( 61 | uiImage: UIImage, 62 | componentX: Int, 63 | componentY: Int, 64 | ): String? { 65 | val uiWidth = uiImage.size.useContents { width } 66 | val uiScale = uiImage.scale 67 | val uiHeight = uiImage.size.useContents { height } 68 | 69 | val pixelWidth = (uiWidth * uiScale).roundToLong().toULong() 70 | val pixelHeight = (uiHeight * uiScale).roundToLong().toULong() 71 | 72 | val context = CGBitmapContextCreateWithData( 73 | data = null, 74 | width = pixelWidth, 75 | height = pixelHeight, 76 | bitsPerComponent = 8u, 77 | bytesPerRow = pixelWidth * 4uL, 78 | space = CGColorSpaceCreateWithName(name = kCGColorSpaceSRGB), 79 | bitmapInfo = CGImageAlphaInfo.kCGImageAlphaPremultipliedLast.value, 80 | releaseCallback = null, 81 | releaseInfo = null, 82 | ) ?: return null 83 | 84 | CGContextScaleCTM(context, uiScale, -uiScale) 85 | CGContextTranslateCTM(context, 0.0, -uiHeight) 86 | 87 | UIGraphicsPushContext(context) 88 | uiImage.drawAtPoint(CGPointZero.readValue()) 89 | UIGraphicsPopContext() 90 | 91 | val cgImage = CGBitmapContextCreateImage(context) ?: return null 92 | val dataProvider = CGImageGetDataProvider(cgImage) ?: return null 93 | val data = CGDataProviderCopyData(dataProvider) ?: return null 94 | val pixels = CFDataGetBytePtr(data) ?: return null 95 | 96 | val cgWidth = CGImageGetWidth(cgImage).toInt() 97 | val cgHeight = CGImageGetHeight(cgImage).toInt() 98 | val cgBytesPerRow = CGImageGetBytesPerRow(cgImage).toInt() 99 | val cgBitsPerPixel = CGImageGetBitsPerPixel(cgImage).toInt() 100 | val cgBytesPerPixel = cgBitsPerPixel / 8 101 | 102 | return CommonBlurHash.encode( 103 | pixelReader = object : PixelReader { 104 | override fun readRed(x: Int, y: Int) = pixels[cgBytesPerPixel * x + 0 + y * cgBytesPerRow].toInt() 105 | override fun readGreen(x: Int, y: Int) = pixels[cgBytesPerPixel * x + 1 + y * cgBytesPerRow].toInt() 106 | override fun readBlue(x: Int, y: Int) = pixels[cgBytesPerPixel * x + 2 + y * cgBytesPerRow].toInt() 107 | }, 108 | width = cgWidth, 109 | height = cgHeight, 110 | componentY = componentY, 111 | componentX = componentX, 112 | ) 113 | } 114 | 115 | /** 116 | * If [blurHash] is a valid blur hash, the method will return a [UIImage], 117 | * with the requested [width] as well as [height]. 118 | * 119 | * [useCache] to control the caching which will improve performance (with a slight memory impact) 120 | */ 121 | fun decode( 122 | blurHash: String, 123 | width: CGFloat, 124 | height: CGFloat, 125 | punch: Float = DEFAULT_PUNCH, 126 | useCache: Boolean = true, 127 | ): UIImage? { 128 | val imageWidth = width.toLong() 129 | val imageHeight = height.toLong() 130 | val bytesPerRow = imageWidth * 3L 131 | 132 | val data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * imageHeight) ?: return null 133 | CFDataSetLength(data, bytesPerRow * imageHeight) 134 | val pixels = CFDataGetMutableBytePtr(data) ?: return null 135 | 136 | val pixelWriter = object : PixelWriter { 137 | override fun write( 138 | x: Int, 139 | y: Int, 140 | width: Int, 141 | red: Int, 142 | green: Int, 143 | blue: Int, 144 | ) { 145 | pixels[3 * x + 0 + y * bytesPerRow.toInt()] = red.toUByte() 146 | pixels[3 * x + 1 + y * bytesPerRow.toInt()] = green.toUByte() 147 | pixels[3 * x + 2 + y * bytesPerRow.toInt()] = blue.toUByte() 148 | } 149 | 150 | override fun get() = Unit 151 | } 152 | 153 | CommonBlurHash.decode( 154 | blurHash = blurHash, 155 | pixelWriter = pixelWriter, 156 | width = width.toInt(), 157 | height = height.toInt(), 158 | punch = punch, 159 | useCache = useCache, 160 | ) ?: return null 161 | 162 | val provider = CGDataProviderCreateWithCFData(data) ?: return null 163 | 164 | val cgImage = CGImageCreate( 165 | width = imageWidth.toULong(), 166 | height = imageHeight.toULong(), 167 | bitsPerComponent = 8u, 168 | bitsPerPixel = 24u, 169 | bytesPerRow = bytesPerRow.toULong(), 170 | space = CGColorSpaceCreateDeviceRGB(), 171 | bitmapInfo = CGImageAlphaInfo.kCGImageAlphaNone.value, 172 | provider = provider, 173 | decode = null, 174 | shouldInterpolate = true, 175 | intent = CGColorRenderingIntent.kCGRenderingIntentDefault, 176 | ) ?: return null 177 | 178 | return UIImage.imageWithCGImage(cgImage) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /blurhash/src/jvmMain/kotlin/com/vanniktech/blurhash/BlurHash.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | import java.awt.Point 4 | import java.awt.image.BufferedImage 5 | import java.awt.image.ColorModel 6 | import java.awt.image.DataBuffer 7 | import java.awt.image.DataBufferInt 8 | import java.awt.image.Raster 9 | import java.awt.image.SinglePixelPackedSampleModel 10 | 11 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 12 | actual object BlurHash { 13 | /** 14 | * Clear in-memory calculations. 15 | * The cache is not big, but will increase when many image sizes are decoded, using [decode]. 16 | * If the app needs memory, it is recommended to clear it. 17 | */ 18 | actual fun clearCache() = CommonBlurHash.clearCache() 19 | 20 | /** 21 | * Returns the average sRGB color for the given [blurHash] in respect to its [punch]. 22 | */ 23 | actual fun averageColor( 24 | blurHash: String, 25 | punch: Float, 26 | ) = CommonBlurHash.averageColor(blurHash, punch) 27 | 28 | /** 29 | * Calculates the blur hash from the given [bufferedImage]. 30 | * 31 | * [componentX] number of components in the x dimension 32 | * [componentY] number of components in the y dimension 33 | */ 34 | fun encode( 35 | bufferedImage: BufferedImage, 36 | componentX: Int, 37 | componentY: Int, 38 | ): String { 39 | val width = bufferedImage.width 40 | val height = bufferedImage.height 41 | val pixels = bufferedImage.getRGB(0, 0, width, height, null, 0, width) 42 | return CommonBlurHash.encode( 43 | pixelReader = PixelReaderArgb8888(pixels = pixels, width = width), 44 | width = width, 45 | height = height, 46 | componentX = componentX, 47 | componentY = componentY, 48 | ) 49 | } 50 | 51 | /** 52 | * If [blurHash] is a valid blur hash, the method will return a [BufferedImage], 53 | * with the requested [width] as well as [height]. 54 | * 55 | * [useCache] to control the caching which will improve performance (with a slight memory impact) 56 | */ 57 | fun decode( 58 | blurHash: String, 59 | width: Int, 60 | height: Int, 61 | punch: Float = DEFAULT_PUNCH, 62 | useCache: Boolean = true, 63 | ): BufferedImage? { 64 | val pixels = CommonBlurHash.decode( 65 | blurHash = blurHash, 66 | pixelWriter = PixelWriterArgb8888(width = width, height = height), 67 | width = width, 68 | height = height, 69 | punch = punch, 70 | useCache = useCache, 71 | ) 72 | 73 | if (pixels != null) { 74 | val bitMasks = intArrayOf(0xFF0000, 0xFF00, 0xFF, 0xFF000000.toInt()) 75 | val singlePixelPackedSampleModel = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, width, height, bitMasks) 76 | val dataBufferInt = DataBufferInt(pixels, pixels.size) 77 | val writableRaster = Raster.createWritableRaster(singlePixelPackedSampleModel, dataBufferInt, Point()) 78 | return BufferedImage(ColorModel.getRGBdefault(), writableRaster, false, null) 79 | } 80 | 81 | return null 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /blurhash/src/jvmTest/kotlin/com/vanniktech/blurhash/BlurHashTest.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash 2 | 3 | import java.io.File 4 | import javax.imageio.ImageIO 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class BlurHashTest { 9 | @Test fun lorikeet() { 10 | assert( 11 | onBlurhaBlurHash = "UFDS:O_LNs#pVrMyX6Vu9]RRw[OXOZxaxWNH", 12 | expectedBlurHash = "UFDcT@_LNs#pVrIVX6Vu9]RRw[OXOZxaxWNH", 13 | expectedAverageColor = "rgb(117, 116, 86)", 14 | name = "lorikeet.jpg", 15 | componentX = 4, 16 | componentY = 4, 17 | ) 18 | } 19 | 20 | @Test fun black() { 21 | assert( 22 | expectedBlurHash = "U00000fQfQfQfQfQfQfQfQfQfQfQfQfQfQfQ", 23 | expectedAverageColor = "rgb(0, 0, 0)", 24 | name = "black.png", 25 | componentX = 4, 26 | componentY = 4, 27 | ) 28 | } 29 | 30 | @Test fun small1x1() { 31 | assert( 32 | onBlurhaBlurHash = "U1TSUA?bfQ?b~qj[fQj[fQfQfQfQ~qj[fQj[", 33 | expectedBlurHash = "U~TSUA~q~q~q~q~q~q~q~q~q~q~q~q~q~q~q", 34 | expectedAverageColor = "rgb(255, 255, 255)", 35 | name = "small1x1.png", 36 | componentX = 4, 37 | componentY = 4, 38 | ) 39 | } 40 | 41 | @Test fun white() { 42 | assert( 43 | onBlurhaBlurHash = "U1TSUA?bfQ?b~qj[fQj[fQfQfQfQ~qj[fQj[", 44 | expectedBlurHash = "U2TSUA~qfQ~q~qj[fQj[fQfQfQfQ~qj[fQj[", 45 | expectedAverageColor = "rgb(255, 255, 255)", 46 | name = "white.png", 47 | componentX = 4, 48 | componentY = 4, 49 | ) 50 | } 51 | 52 | @Test fun website1() { 53 | assert( 54 | onBlurhaBlurHash = "LEHLh[WB2yk8pyoJadR*.7kCMdnj", 55 | expectedBlurHash = "LEHV6nWB2yk8pyo0adR*.7kCMdnj", 56 | expectedAverageColor = "rgb(151, 150, 149)", 57 | name = "website1.jpg", 58 | componentX = 4, 59 | componentY = 3, 60 | ) 61 | } 62 | 63 | @Test fun website2() { 64 | assert( 65 | onBlurhaBlurHash = "LGF5?xYk^6#M@-5c,1J5@[or[Q6.", 66 | expectedBlurHash = "LGFFaXYk^6#M@-5c,1Ex@@or[j6o", 67 | expectedAverageColor = "rgb(132, 126, 153)", 68 | name = "website2.jpg", 69 | componentX = 4, 70 | componentY = 3, 71 | ) 72 | } 73 | 74 | @Test fun website3() { 75 | assert( 76 | onBlurhaBlurHash = "L6PZfSi_.AyE_3t7t7R**0o#DgR4", 77 | expectedBlurHash = "L6Pj0^nh.AyE?vt7t7R**0o#DgR4", 78 | expectedAverageColor = "rgb(222, 217, 213)", 79 | name = "website3.jpg", 80 | componentX = 4, 81 | componentY = 3, 82 | ) 83 | } 84 | 85 | @Test fun website4() { 86 | assert( 87 | onBlurhaBlurHash = "LKN]Rv%2Tw=w]~RBVZRi};RPxuwH", 88 | expectedBlurHash = "LKO2?V%2Tw=^]~RBVZRi};RPxuwH", 89 | expectedAverageColor = "rgb(209, 178, 164)", 90 | name = "website4.jpg", 91 | componentX = 4, 92 | componentY = 3, 93 | ) 94 | } 95 | 96 | @Test fun flag() { 97 | assert( 98 | expectedBlurHash = "LuK+yz_M.7xu2xwbs9n\$IBIVWCt7", 99 | expectedAverageColor = "rgb(181, 130, 159)", 100 | name = "flag.png", 101 | componentX = 4, 102 | componentY = 3, 103 | ) 104 | } 105 | 106 | @Test fun road() { 107 | assert( 108 | expectedBlurHash = "UKMF]oj[01R*~oaz0NWC0NWVIpoe=@WV-ns.", 109 | expectedAverageColor = "rgb(193, 158, 111)", 110 | name = "road.png", 111 | componentX = 4, 112 | componentY = 4, 113 | ) 114 | } 115 | 116 | @Test fun badge() { 117 | assert( 118 | expectedBlurHash = "U48VCUjv0va{NSfQ-Gju5yfP$+fPszfQEsa{", 119 | expectedAverageColor = "rgb(73, 18, 145)", 120 | name = "badge.png", 121 | componentX = 4, 122 | componentY = 4, 123 | ) 124 | } 125 | 126 | private fun assert( 127 | name: String, 128 | expectedBlurHash: String, 129 | expectedAverageColor: String, 130 | componentX: Int, 131 | componentY: Int, 132 | /** The blur hash on https://blurha.sh/ - for quite a few images it's slightly different */ 133 | @Suppress("UNUSED_PARAMETER") onBlurhaBlurHash: String? = null, 134 | ) { 135 | val file = File("images/$name") 136 | val bufferedImage = ImageIO.read(file) 137 | val actualBlurHash = BlurHash.encode(bufferedImage, componentX, componentY) 138 | val image = BlurHash.decode( 139 | blurHash = actualBlurHash, 140 | width = bufferedImage.width, 141 | height = bufferedImage.height, 142 | ) 143 | 144 | val fileName = file.nameWithoutExtension + "-blurred.png" 145 | val resolve = file.parentFile.resolve(fileName) 146 | assertEquals( 147 | expected = true, 148 | actual = ImageIO.write(image!!, "png", resolve), 149 | ) 150 | 151 | assertEquals( 152 | expected = expectedBlurHash, 153 | actual = actualBlurHash, 154 | ) 155 | 156 | val averageColor = BlurHash.averageColor(expectedBlurHash)!! 157 | val red = averageColor shr 16 and 0xFF 158 | val green = averageColor shr 8 and 0xFF 159 | val blue = averageColor and 0xFF 160 | 161 | assertEquals( 162 | expected = expectedAverageColor, 163 | actual = "rgb($red, $green, $blue)", 164 | ) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /blurhash/src/test/snapshots/images/com.vanniktech.blurhash_BlurHashTest_encodeDecode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/blurhash/src/test/snapshots/images/com.vanniktech.blurhash_BlurHashTest_encodeDecode.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath(libs.plugin.android.cache.fix) 4 | classpath(libs.plugin.androidgradleplugin) 5 | classpath(libs.plugin.dokka) 6 | classpath(libs.plugin.kotlin) 7 | classpath(libs.plugin.licensee) 8 | classpath(libs.plugin.metalava) 9 | classpath(libs.plugin.paparazzi) 10 | classpath(libs.plugin.publish) 11 | } 12 | } 13 | 14 | plugins { 15 | alias(libs.plugins.codequalitytools) 16 | alias(libs.plugins.dependencygraphgenerator) 17 | } 18 | 19 | codeQualityTools { 20 | checkstyle { 21 | enabled = false // Kotlin only. 22 | } 23 | pmd { 24 | enabled = false // Kotlin only. 25 | } 26 | ktlint { 27 | toolVersion = libs.versions.ktlint.get() 28 | } 29 | detekt { 30 | enabled = false // Don"t want. 31 | } 32 | cpd { 33 | enabled = false // Kotlin only. 34 | } 35 | lint { 36 | lintConfig = rootProject.file("lint.xml") 37 | checkAllWarnings = true 38 | } 39 | kotlin { 40 | allWarningsAsErrors = true 41 | } 42 | } 43 | 44 | allprojects { 45 | repositories { 46 | google() 47 | mavenCentral() 48 | gradlePluginPortal() 49 | } 50 | 51 | afterEvaluate { 52 | // To avoid: 53 | // The Kotlin source set androidAndroidTestRelease was configured but not added to any Kotlin compilation. You can add a source set to a target"s compilation by connecting it with the compilation"s default source set using "dependsOn". 54 | // See https://kotlinlang.org/docs/reference/building-mpp-with-gradle.html#connecting-source-sets 55 | project.extensions.findByType(org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension::class)?.sourceSets?.removeAll { 56 | setOf( 57 | "androidAndroidTestRelease", 58 | "androidTestFixtures", 59 | "androidTestFixturesDebug", 60 | "androidTestFixturesRelease" 61 | ).contains(it.name) 62 | } 63 | } 64 | } 65 | 66 | subprojects { 67 | plugins.withType { 68 | apply(plugin = "org.gradle.android.cache-fix") 69 | } 70 | 71 | tasks.withType(Test::class.java).all { 72 | testLogging.exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.vanniktech 2 | VERSION_NAME=0.4.0-SNAPSHOT 3 | 4 | POM_DESCRIPTION=BlurHash support for iOS, Android and JVM via Kotlin Multiplatform 5 | 6 | POM_URL=https://github.com/vanniktech/blurhash 7 | POM_SCM_URL=https://github.com/vanniktech/blurhash 8 | POM_SCM_CONNECTION=scm:git:git://github.com/vanniktech/blurhash.git 9 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/vanniktech/blurhash.git 10 | 11 | POM_LICENCE_NAME=MIT 12 | POM_LICENCE_URL=https://opensource.org/licenses/MIT 13 | POM_LICENCE_DIST=repo 14 | 15 | POM_DEVELOPER_ID=vanniktech 16 | POM_DEVELOPER_NAME=Niklas Baudy 17 | 18 | android.useAndroidX=true 19 | # We are solely relying on AndroidX. 20 | android.enableJetifier=false 21 | 22 | android.experimental.enableSourceSetPathsMap=true 23 | android.experimental.cacheCompileLibResources=true 24 | android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.experimental.enableSourceSetPathsMap,android.experimental.cacheCompileLibResources 25 | 26 | SONATYPE_HOST=DEFAULT 27 | SONATYPE_AUTOMATIC_RELEASE=true 28 | RELEASE_SIGNING_ENABLED=true 29 | 30 | org.gradle.jvmargs=-Xmx2048m 31 | 32 | android.defaults.buildfeatures.buildconfig=false 33 | android.defaults.buildfeatures.aidl=false 34 | android.defaults.buildfeatures.renderscript=false 35 | android.defaults.buildfeatures.resvalues=false 36 | android.defaults.buildfeatures.shaders=false 37 | 38 | kotlin.mpp.stability.nowarn=true 39 | kotlin.mpp.androidSourceSetLayoutVersion=2 40 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | minSdk = "21" 3 | compileSdk = "34" 4 | targetSdk = "34" 5 | 6 | androidgradleplugin = "8.9.2" 7 | kotlin = "2.1.21" 8 | ktlint = "1.4.1" 9 | 10 | [libraries] 11 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" } 12 | kotlin-test-annotations-common = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } 13 | kotlin-test-common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } 14 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 15 | leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.14" } 16 | material = { module = "com.google.android.material:material", version = "1.12.0" } 17 | plugin-android-cache-fix = { module = "org.gradle.android.cache-fix:org.gradle.android.cache-fix.gradle.plugin", version = "3.0.1" } 18 | plugin-androidgradleplugin = { module = "com.android.tools.build:gradle", version.ref = "androidgradleplugin" } 19 | plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "2.0.0" } 20 | plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 21 | plugin-licensee = { module = "app.cash.licensee:licensee-gradle-plugin", version = "1.13.0" } 22 | plugin-metalava = { module = "me.tylerbwong.gradle.metalava:plugin", version = "0.4.0-alpha03" } 23 | plugin-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.32.0" } 24 | plugin-paparazzi = { module = "app.cash.paparazzi:paparazzi-gradle-plugin", version = "1.3.3" } 25 | timber = { module = "com.jakewharton.timber:timber", version = "5.0.1" } 26 | 27 | [plugins] 28 | codequalitytools = { id = "com.vanniktech.code.quality.tools", version = "0.24.0" } 29 | dependencygraphgenerator = { id = "com.vanniktech.dependency.graph.generator", version = "0.8.0" } 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":disableDependencyDashboard" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /sample-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-android.png -------------------------------------------------------------------------------- /sample-android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | kotlin { 7 | jvmToolchain { 8 | languageVersion.set(JavaLanguageVersion.of(11)) 9 | } 10 | } 11 | 12 | android { 13 | namespace = "com.vanniktech.blurhash.sample.android" 14 | 15 | compileSdk = libs.versions.compileSdk.get().toInt() 16 | 17 | defaultConfig { 18 | applicationId = "com.vanniktech.blurhash.sample.android" 19 | vectorDrawables.useSupportLibrary = true 20 | minSdk = libs.versions.minSdk.get().toInt() 21 | targetSdk = libs.versions.targetSdk.get().toInt() 22 | versionCode = 1 23 | versionName = project.property("VERSION_NAME").toString() 24 | 25 | vectorDrawables.useSupportLibrary = true 26 | 27 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 28 | } 29 | 30 | buildFeatures { 31 | viewBinding = true 32 | } 33 | 34 | buildTypes { 35 | release { 36 | isMinifyEnabled = false 37 | isShrinkResources = false 38 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) 39 | } 40 | } 41 | 42 | compileOptions { 43 | sourceCompatibility = JavaVersion.VERSION_11 44 | targetCompatibility = JavaVersion.VERSION_11 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation(project(":blurhash")) 50 | implementation(libs.timber) 51 | implementation(libs.material) 52 | } 53 | 54 | dependencies { 55 | debugImplementation(libs.leakcanary.android) 56 | } 57 | -------------------------------------------------------------------------------- /sample-android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample-android/src/main/kotlin/com/vanniktech/blurhash/sample/android/BlurHashApplication.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash.sample.android 2 | 3 | import android.app.Application 4 | import com.vanniktech.blurhash.BlurHash 5 | import timber.log.Timber 6 | 7 | class BlurHashApplication : Application() { 8 | override fun onCreate() { 9 | super.onCreate() 10 | Timber.plant(Timber.DebugTree()) 11 | } 12 | 13 | override fun onLowMemory() { 14 | super.onLowMemory() 15 | BlurHash.clearCache() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sample-android/src/main/kotlin/com/vanniktech/blurhash/sample/android/BlurHashMainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash.sample.android 2 | 3 | import android.graphics.BitmapFactory 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.vanniktech.blurhash.BlurHash 7 | import com.vanniktech.blurhash.sample.android.databinding.ActivityMainBinding 8 | 9 | class BlurHashMainActivity : AppCompatActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | val binding = ActivityMainBinding.inflate(layoutInflater) 14 | setContentView(binding.root) 15 | 16 | // Sample image. 17 | val bitmap = BitmapFactory.decodeResource(resources, R.drawable.blueberries) 18 | binding.original.setImageBitmap(bitmap) 19 | binding.original.layoutParams.height = bitmap.height 20 | binding.original.layoutParams.width = bitmap.width 21 | 22 | // Blur hashing. 23 | val blurHash = BlurHash.encode(bitmap, componentX = 5, componentY = 4) 24 | binding.blurHash.text = blurHash 25 | 26 | // Create blurred version. 27 | // We don't need to create a Bitmap in its full size. 28 | // Let Android scale it up for us as scaling is cheaper than generating a larger image. 29 | binding.blurred.setImageBitmap( 30 | BlurHash.decode( 31 | blurHash = blurHash, 32 | width = bitmap.width / 4, 33 | height = bitmap.height / 4, 34 | ), 35 | ) 36 | binding.blurred.layoutParams.height = bitmap.height 37 | binding.blurred.layoutParams.width = bitmap.width 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sample-android/src/main/res/drawable-nodpi/blueberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-android/src/main/res/drawable-nodpi/blueberries.jpg -------------------------------------------------------------------------------- /sample-android/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 17 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /sample-android/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-android/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-android/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-android/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-android/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-android/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-android/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-android/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-android/src/main/res/values-v23/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /sample-android/src/main/res/values-v27/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /sample-android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | BlurHash 4 | false 5 | 6 | -------------------------------------------------------------------------------- /sample-android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /sample-ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-ios.png -------------------------------------------------------------------------------- /sample-ios/Podfile: -------------------------------------------------------------------------------- 1 | source "https://cdn.cocoapods.org/" 2 | platform :ios, '14.0' 3 | workspace "ios" 4 | 5 | # Comment this line if you're not using Swift and don't want to use dynamic frameworks. 6 | use_frameworks! 7 | 8 | target 'ios' do 9 | project 'ios' 10 | 11 | pod "BlurHash", :path => "../blurhash" 12 | end -------------------------------------------------------------------------------- /sample-ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - BlurHash (0.4.0-SNAPSHOT) 3 | 4 | DEPENDENCIES: 5 | - BlurHash (from `../blurhash`) 6 | 7 | EXTERNAL SOURCES: 8 | BlurHash: 9 | :path: "../blurhash" 10 | 11 | SPEC CHECKSUMS: 12 | BlurHash: cdfab3e37b970b4aee966789f324002d9dcc8ef4 13 | 14 | PODFILE CHECKSUM: 15700ecada551b00eac0e2e014d648eaaaccb171 15 | 16 | COCOAPODS: 1.15.2 17 | -------------------------------------------------------------------------------- /sample-ios/ios.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 850D4FB228CCEBA100714308 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850D4FB128CCEBA100714308 /* App.swift */; }; 11 | 850D4FB628CCEBA100714308 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 850D4FB528CCEBA100714308 /* Assets.xcassets */; }; 12 | 850D4FB928CCEBA100714308 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 850D4FB828CCEBA100714308 /* Preview Assets.xcassets */; }; 13 | D65BB7D28364C04EC850180D /* Pods_ios.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93C0936F297D4B91B3ABA725 /* Pods_ios.framework */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 208331B5D6E6A7716D6C289D /* Pods-BlurHash.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlurHash.release.xcconfig"; path = "Target Support Files/Pods-BlurHash/Pods-BlurHash.release.xcconfig"; sourceTree = ""; }; 18 | 6AFCE7532F78DBD821C8E78D /* Pods-BlurHash.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlurHash.debug.xcconfig"; path = "Target Support Files/Pods-BlurHash/Pods-BlurHash.debug.xcconfig"; sourceTree = ""; }; 19 | 850D4FAE28CCEBA100714308 /* ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ios.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 850D4FB128CCEBA100714308 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 21 | 850D4FB528CCEBA100714308 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | 850D4FB828CCEBA100714308 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 23 | 8F886CA92368476133371299 /* Pods-ios.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios.debug.xcconfig"; path = "Target Support Files/Pods-ios/Pods-ios.debug.xcconfig"; sourceTree = ""; }; 24 | 93C0936F297D4B91B3ABA725 /* Pods_ios.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ios.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | E8CBC299A962EF4A7EF2E9D2 /* Pods-ios.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios.release.xcconfig"; path = "Target Support Files/Pods-ios/Pods-ios.release.xcconfig"; sourceTree = ""; }; 26 | /* End PBXFileReference section */ 27 | 28 | /* Begin PBXFrameworksBuildPhase section */ 29 | 850D4FAB28CCEBA100714308 /* Frameworks */ = { 30 | isa = PBXFrameworksBuildPhase; 31 | buildActionMask = 2147483647; 32 | files = ( 33 | D65BB7D28364C04EC850180D /* Pods_ios.framework in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 2D6FE175FA70558B6EE8AE2B /* Frameworks */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 93C0936F297D4B91B3ABA725 /* Pods_ios.framework */, 44 | ); 45 | name = Frameworks; 46 | sourceTree = ""; 47 | }; 48 | 850D4FA528CCEBA100714308 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 850D4FB028CCEBA100714308 /* ios */, 52 | 850D4FAF28CCEBA100714308 /* Products */, 53 | 9BDC6B73A6A1A714F2D6297C /* Pods */, 54 | 2D6FE175FA70558B6EE8AE2B /* Frameworks */, 55 | ); 56 | sourceTree = ""; 57 | }; 58 | 850D4FAF28CCEBA100714308 /* Products */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 850D4FAE28CCEBA100714308 /* ios.app */, 62 | ); 63 | name = Products; 64 | sourceTree = ""; 65 | }; 66 | 850D4FB028CCEBA100714308 /* ios */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 850D4FB128CCEBA100714308 /* App.swift */, 70 | 850D4FB528CCEBA100714308 /* Assets.xcassets */, 71 | 850D4FB728CCEBA100714308 /* Preview Content */, 72 | ); 73 | path = ios; 74 | sourceTree = ""; 75 | }; 76 | 850D4FB728CCEBA100714308 /* Preview Content */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 850D4FB828CCEBA100714308 /* Preview Assets.xcassets */, 80 | ); 81 | path = "Preview Content"; 82 | sourceTree = ""; 83 | }; 84 | 9BDC6B73A6A1A714F2D6297C /* Pods */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 6AFCE7532F78DBD821C8E78D /* Pods-BlurHash.debug.xcconfig */, 88 | 208331B5D6E6A7716D6C289D /* Pods-BlurHash.release.xcconfig */, 89 | 8F886CA92368476133371299 /* Pods-ios.debug.xcconfig */, 90 | E8CBC299A962EF4A7EF2E9D2 /* Pods-ios.release.xcconfig */, 91 | ); 92 | path = Pods; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | 850D4FAD28CCEBA100714308 /* ios */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = 850D4FBC28CCEBA100714308 /* Build configuration list for PBXNativeTarget "ios" */; 101 | buildPhases = ( 102 | 7BF1B2F561E1594EC096416E /* [CP] Check Pods Manifest.lock */, 103 | 850D4FAA28CCEBA100714308 /* Sources */, 104 | 850D4FAB28CCEBA100714308 /* Frameworks */, 105 | 850D4FAC28CCEBA100714308 /* Resources */, 106 | ); 107 | buildRules = ( 108 | ); 109 | dependencies = ( 110 | ); 111 | name = ios; 112 | productName = BlurHash; 113 | productReference = 850D4FAE28CCEBA100714308 /* ios.app */; 114 | productType = "com.apple.product-type.application"; 115 | }; 116 | /* End PBXNativeTarget section */ 117 | 118 | /* Begin PBXProject section */ 119 | 850D4FA628CCEBA100714308 /* Project object */ = { 120 | isa = PBXProject; 121 | attributes = { 122 | BuildIndependentTargetsInParallel = 1; 123 | LastSwiftUpdateCheck = 1340; 124 | LastUpgradeCheck = 1340; 125 | TargetAttributes = { 126 | 850D4FAD28CCEBA100714308 = { 127 | CreatedOnToolsVersion = 13.4.1; 128 | }; 129 | }; 130 | }; 131 | buildConfigurationList = 850D4FA928CCEBA100714308 /* Build configuration list for PBXProject "ios" */; 132 | compatibilityVersion = "Xcode 13.0"; 133 | developmentRegion = en; 134 | hasScannedForEncodings = 0; 135 | knownRegions = ( 136 | en, 137 | Base, 138 | ); 139 | mainGroup = 850D4FA528CCEBA100714308; 140 | productRefGroup = 850D4FAF28CCEBA100714308 /* Products */; 141 | projectDirPath = ""; 142 | projectRoot = ""; 143 | targets = ( 144 | 850D4FAD28CCEBA100714308 /* ios */, 145 | ); 146 | }; 147 | /* End PBXProject section */ 148 | 149 | /* Begin PBXResourcesBuildPhase section */ 150 | 850D4FAC28CCEBA100714308 /* Resources */ = { 151 | isa = PBXResourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | 850D4FB928CCEBA100714308 /* Preview Assets.xcassets in Resources */, 155 | 850D4FB628CCEBA100714308 /* Assets.xcassets in Resources */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | /* End PBXResourcesBuildPhase section */ 160 | 161 | /* Begin PBXShellScriptBuildPhase section */ 162 | 7BF1B2F561E1594EC096416E /* [CP] Check Pods Manifest.lock */ = { 163 | isa = PBXShellScriptBuildPhase; 164 | buildActionMask = 2147483647; 165 | files = ( 166 | ); 167 | inputFileListPaths = ( 168 | ); 169 | inputPaths = ( 170 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 171 | "${PODS_ROOT}/Manifest.lock", 172 | ); 173 | name = "[CP] Check Pods Manifest.lock"; 174 | outputFileListPaths = ( 175 | ); 176 | outputPaths = ( 177 | "$(DERIVED_FILE_DIR)/Pods-ios-checkManifestLockResult.txt", 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | shellPath = /bin/sh; 181 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 182 | showEnvVarsInLog = 0; 183 | }; 184 | /* End PBXShellScriptBuildPhase section */ 185 | 186 | /* Begin PBXSourcesBuildPhase section */ 187 | 850D4FAA28CCEBA100714308 /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 850D4FB228CCEBA100714308 /* App.swift in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | 850D4FBA28CCEBA100714308 /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_ANALYZER_NONNULL = YES; 203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 205 | CLANG_ENABLE_MODULES = YES; 206 | CLANG_ENABLE_OBJC_ARC = YES; 207 | CLANG_ENABLE_OBJC_WEAK = YES; 208 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 209 | CLANG_WARN_BOOL_CONVERSION = YES; 210 | CLANG_WARN_COMMA = YES; 211 | CLANG_WARN_CONSTANT_CONVERSION = YES; 212 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 213 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 214 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 215 | CLANG_WARN_EMPTY_BODY = YES; 216 | CLANG_WARN_ENUM_CONVERSION = YES; 217 | CLANG_WARN_INFINITE_RECURSION = YES; 218 | CLANG_WARN_INT_CONVERSION = YES; 219 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 220 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 221 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 222 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 223 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 224 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 225 | CLANG_WARN_STRICT_PROTOTYPES = YES; 226 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 227 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 228 | CLANG_WARN_UNREACHABLE_CODE = YES; 229 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 230 | COPY_PHASE_STRIP = NO; 231 | DEBUG_INFORMATION_FORMAT = dwarf; 232 | ENABLE_STRICT_OBJC_MSGSEND = YES; 233 | ENABLE_TESTABILITY = YES; 234 | GCC_C_LANGUAGE_STANDARD = gnu11; 235 | GCC_DYNAMIC_NO_PIC = NO; 236 | GCC_NO_COMMON_BLOCKS = YES; 237 | GCC_OPTIMIZATION_LEVEL = 0; 238 | GCC_PREPROCESSOR_DEFINITIONS = ( 239 | "DEBUG=1", 240 | "$(inherited)", 241 | ); 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 250 | MTL_FAST_MATH = YES; 251 | ONLY_ACTIVE_ARCH = YES; 252 | SDKROOT = iphoneos; 253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 255 | }; 256 | name = Debug; 257 | }; 258 | 850D4FBB28CCEBA100714308 /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | ALWAYS_SEARCH_USER_PATHS = NO; 262 | CLANG_ANALYZER_NONNULL = YES; 263 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 264 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 265 | CLANG_ENABLE_MODULES = YES; 266 | CLANG_ENABLE_OBJC_ARC = YES; 267 | CLANG_ENABLE_OBJC_WEAK = YES; 268 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 269 | CLANG_WARN_BOOL_CONVERSION = YES; 270 | CLANG_WARN_COMMA = YES; 271 | CLANG_WARN_CONSTANT_CONVERSION = YES; 272 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 273 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 274 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INFINITE_RECURSION = YES; 278 | CLANG_WARN_INT_CONVERSION = YES; 279 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 281 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 283 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 285 | CLANG_WARN_STRICT_PROTOTYPES = YES; 286 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 287 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 288 | CLANG_WARN_UNREACHABLE_CODE = YES; 289 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 290 | COPY_PHASE_STRIP = NO; 291 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 292 | ENABLE_NS_ASSERTIONS = NO; 293 | ENABLE_STRICT_OBJC_MSGSEND = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu11; 295 | GCC_NO_COMMON_BLOCKS = YES; 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 303 | MTL_ENABLE_DEBUG_INFO = NO; 304 | MTL_FAST_MATH = YES; 305 | SDKROOT = iphoneos; 306 | SWIFT_COMPILATION_MODE = wholemodule; 307 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 308 | VALIDATE_PRODUCT = YES; 309 | }; 310 | name = Release; 311 | }; 312 | 850D4FBD28CCEBA100714308 /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | baseConfigurationReference = 8F886CA92368476133371299 /* Pods-ios.debug.xcconfig */; 315 | buildSettings = { 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 318 | CODE_SIGN_STYLE = Automatic; 319 | CURRENT_PROJECT_VERSION = 1; 320 | DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\""; 321 | DEVELOPMENT_TEAM = 779A4D7K9R; 322 | ENABLE_PREVIEWS = YES; 323 | GENERATE_INFOPLIST_FILE = YES; 324 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 325 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 326 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 327 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 328 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 329 | LD_RUNPATH_SEARCH_PATHS = ( 330 | "$(inherited)", 331 | "@executable_path/Frameworks", 332 | ); 333 | MARKETING_VERSION = 1.0; 334 | PRODUCT_BUNDLE_IDENTIFIER = com.vanniktech.blurhash.sample.ios; 335 | PRODUCT_NAME = "$(TARGET_NAME)"; 336 | SWIFT_EMIT_LOC_STRINGS = YES; 337 | SWIFT_VERSION = 5.0; 338 | TARGETED_DEVICE_FAMILY = "1,2"; 339 | }; 340 | name = Debug; 341 | }; 342 | 850D4FBE28CCEBA100714308 /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | baseConfigurationReference = E8CBC299A962EF4A7EF2E9D2 /* Pods-ios.release.xcconfig */; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 348 | CODE_SIGN_STYLE = Automatic; 349 | CURRENT_PROJECT_VERSION = 1; 350 | DEVELOPMENT_ASSET_PATHS = "\"ios/Preview Content\""; 351 | DEVELOPMENT_TEAM = 779A4D7K9R; 352 | ENABLE_PREVIEWS = YES; 353 | GENERATE_INFOPLIST_FILE = YES; 354 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 355 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 356 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 357 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 358 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 359 | LD_RUNPATH_SEARCH_PATHS = ( 360 | "$(inherited)", 361 | "@executable_path/Frameworks", 362 | ); 363 | MARKETING_VERSION = 1.0; 364 | PRODUCT_BUNDLE_IDENTIFIER = com.vanniktech.blurhash.sample.ios; 365 | PRODUCT_NAME = "$(TARGET_NAME)"; 366 | SWIFT_EMIT_LOC_STRINGS = YES; 367 | SWIFT_VERSION = 5.0; 368 | TARGETED_DEVICE_FAMILY = "1,2"; 369 | }; 370 | name = Release; 371 | }; 372 | /* End XCBuildConfiguration section */ 373 | 374 | /* Begin XCConfigurationList section */ 375 | 850D4FA928CCEBA100714308 /* Build configuration list for PBXProject "ios" */ = { 376 | isa = XCConfigurationList; 377 | buildConfigurations = ( 378 | 850D4FBA28CCEBA100714308 /* Debug */, 379 | 850D4FBB28CCEBA100714308 /* Release */, 380 | ); 381 | defaultConfigurationIsVisible = 0; 382 | defaultConfigurationName = Release; 383 | }; 384 | 850D4FBC28CCEBA100714308 /* Build configuration list for PBXNativeTarget "ios" */ = { 385 | isa = XCConfigurationList; 386 | buildConfigurations = ( 387 | 850D4FBD28CCEBA100714308 /* Debug */, 388 | 850D4FBE28CCEBA100714308 /* Release */, 389 | ); 390 | defaultConfigurationIsVisible = 0; 391 | defaultConfigurationName = Release; 392 | }; 393 | /* End XCConfigurationList section */ 394 | }; 395 | rootObject = 850D4FA628CCEBA100714308 /* Project object */; 396 | } 397 | -------------------------------------------------------------------------------- /sample-ios/ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample-ios/ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample-ios/ios.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sample-ios/ios.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample-ios/ios/App.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import blurhash 3 | 4 | @main 5 | struct SampleApp : App { 6 | @UIApplicationDelegateAdaptor(SampleDelegate.self) var appDelegate 7 | 8 | var body: some Scene { 9 | WindowGroup { 10 | ContentView() 11 | } 12 | } 13 | } 14 | 15 | class SampleDelegate: NSObject, UIApplicationDelegate { 16 | func applicationDidReceiveMemoryWarning(_ application: UIApplication) { 17 | BlurHash.shared.clearCache() 18 | } 19 | } 20 | 21 | struct ContentView: View { 22 | var body: some View { 23 | VStack { 24 | // Sample image. 25 | let image = UIImage(named: "blueberries")! 26 | Image(uiImage: image) 27 | 28 | // Blur hashing. 29 | let blurHash = BlurHash.shared.encode( 30 | uiImage: image, 31 | componentX: 5, 32 | componentY: 4 33 | ) 34 | Text(blurHash ?? "Invalid image") 35 | .padding() 36 | 37 | // Create blurred version. 38 | // We don't need to create an UIImage in its full size. 39 | // Let iOS scale it up for us as scaling is cheaper than generating a larger image. 40 | if let blurHash = blurHash, let blurred = BlurHash.shared.decode( 41 | blurHash: blurHash, 42 | width: image.size.width / 4, 43 | height: image.size.height / 4, 44 | punch: 1.0, 45 | useCache: true 46 | ) { 47 | Image(uiImage: blurred) 48 | .scaleEffect(4) 49 | .frame(width: image.size.width, height: image.size.height) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sample-ios/ios/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /sample-ios/ios/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample-ios/ios/Assets.xcassets/blueberries.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "blueberries.jpg", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample-ios/ios/Assets.xcassets/blueberries.imageset/blueberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-ios/ios/Assets.xcassets/blueberries.imageset/blueberries.jpg -------------------------------------------------------------------------------- /sample-ios/ios/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample-jvm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-jvm.png -------------------------------------------------------------------------------- /sample-jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | application 3 | id("org.jetbrains.kotlin.jvm") 4 | } 5 | 6 | kotlin { 7 | jvmToolchain(11) 8 | } 9 | 10 | dependencies { 11 | implementation(project(":blurhash")) 12 | } 13 | 14 | dependencies { 15 | testImplementation(libs.kotlin.test.junit) 16 | } 17 | 18 | application { 19 | mainClass.set("com.vanniktech.blurhash.sample.jvm.BlurHashJvmKt") 20 | } 21 | -------------------------------------------------------------------------------- /sample-jvm/images/blueberries-blurred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-jvm/images/blueberries-blurred.png -------------------------------------------------------------------------------- /sample-jvm/images/blueberries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanniktech/blurhash/aff17e3c53bee8fccb087b2113828d74b230b4cc/sample-jvm/images/blueberries.jpg -------------------------------------------------------------------------------- /sample-jvm/src/main/java/com/vanniktech/blurhash/sample/jvm/BlurHashJvm.kt: -------------------------------------------------------------------------------- 1 | package com.vanniktech.blurhash.sample.jvm 2 | 3 | import com.vanniktech.blurhash.BlurHash 4 | import java.io.File 5 | import javax.imageio.ImageIO 6 | 7 | fun main() { 8 | // So it's executable both in IntelliJ and on Terminal. 9 | val directory = File("sample-jvm/images/").takeIf { it.exists() } ?: File("images/") 10 | 11 | // Sample image. 12 | val input = directory.resolve("blueberries.jpg") 13 | val image = ImageIO.read(input) 14 | 15 | // Blur hashing. 16 | val blurHash = BlurHash.encode( 17 | bufferedImage = image, 18 | componentX = 5, 19 | componentY = 4, 20 | ) 21 | 22 | println("Generating blur hash for:") 23 | println(input.absolutePath) 24 | 25 | println() 26 | println("Yielded:") 27 | println(blurHash) 28 | 29 | val blurred = BlurHash.decode( 30 | blurHash = blurHash, 31 | width = image.width, 32 | height = image.height, 33 | ) 34 | 35 | // Create blurred version. 36 | val output = directory.resolve("blueberries-blurred.png") 37 | ImageIO.write(blurred!!, "png", output) 38 | println() 39 | println("A generated blurred image can be found here:") 40 | println(output.absolutePath) 41 | } 42 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | google() 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | include(":blurhash") 10 | include(":sample-android") 11 | include(":sample-jvm") 12 | --------------------------------------------------------------------------------