expo-image 2.4.0 → 2.5.0-canary-20250729-d8899ae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -7
- package/android/build.gradle +1 -1
- package/android/src/main/java/expo/modules/image/ExpoImageModule.kt +39 -0
- package/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt +24 -5
- package/android/src/main/java/expo/modules/image/blurhash/BlurhashDecoder.kt +1 -10
- package/android/src/main/java/expo/modules/image/blurhash/BlurhashEncoder.kt +112 -0
- package/android/src/main/java/expo/modules/image/blurhash/BlurhashHelpers.kt +38 -0
- package/android/src/main/java/expo/modules/image/okhttp/GlideUrlWrapperLoader.kt +1 -1
- package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashDecoder.kt +1 -146
- package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashEncoder.kt +184 -0
- package/build/ExpoImage.web.d.ts +1 -1
- package/build/ExpoImage.web.d.ts.map +1 -1
- package/build/Image.d.ts +11 -2
- package/build/Image.d.ts.map +1 -1
- package/build/Image.types.d.ts +9 -5
- package/build/Image.types.d.ts.map +1 -1
- package/build/web/ImageWrapper.d.ts.map +1 -1
- package/expo-module.config.json +1 -1
- package/ios/ExpoImage.podspec +1 -1
- package/ios/ImageModule.swift +33 -12
- package/ios/ImageSource.swift +1 -2
- package/ios/ImageUtils.swift +4 -8
- package/ios/ImageView.swift +1 -1
- package/local-maven-repo/BareExpo/expo.modules.image/{2.4.0/expo.modules.image-2.4.0-sources.jar → 2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae-sources.jar} +0 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae-sources.jar.md5 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae-sources.jar.sha1 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae-sources.jar.sha256 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae-sources.jar.sha512 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.aar +0 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.aar.md5 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.aar.sha1 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.aar.sha256 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.aar.sha512 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/{2.4.0/expo.modules.image-2.4.0.module → 2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.module} +24 -24
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.module.md5 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.module.sha1 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.module.sha256 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.module.sha512 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/{2.4.0/expo.modules.image-2.4.0.pom → 2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.pom} +2 -2
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.pom.md5 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.pom.sha1 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.pom.sha256 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250729-d8899ae/expo.modules.image-2.5.0-canary-20250729-d8899ae.pom.sha512 +1 -0
- package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml +4 -4
- package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha512 +1 -1
- package/package.json +4 -5
- package/src/ExpoImage.web.tsx +2 -1
- package/src/Image.tsx +15 -3
- package/src/Image.types.ts +9 -5
- package/src/web/ImageWrapper.tsx +8 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.md5 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.sha1 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.sha256 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.sha512 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar +0 -0
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.md5 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.sha1 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.sha256 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.sha512 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.md5 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.sha1 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.sha256 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.sha512 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.pom.md5 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.pom.sha1 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.pom.sha256 +0 -1
- package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.pom.sha512 +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,11 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
### 🎉 New features
|
|
8
8
|
|
|
9
|
+
- Add `generateThumbhashAsync` ([#38090](https://github.com/expo/expo/pull/38090) by [@Wenszel](https://github.com/Wenszel))
|
|
10
|
+
- Add support for `ImageRef` source in `generateBlurhashAsync` ([#37901](https://github.com/expo/expo/pull/37901) by [@Wenszel](https://github.com/Wenszel))
|
|
11
|
+
- [Android] Add generateBlurhashAsync ([#37817](https://github.com/expo/expo/pull/37817) by [@Wenszel](https://github.com/Wenszel))
|
|
12
|
+
|
|
9
13
|
### 🐛 Bug fixes
|
|
10
14
|
|
|
15
|
+
- [Android] Fix animation resuming by casting image to GifDrawable. ([#37363](https://github.com/expo/expo/pull/37363) by [@Wenszel](https://github.com/Wenszel))
|
|
16
|
+
- [Web] Fix `alt` as an alias for `accessibilityLabel` ([#37682](https://github.com/expo/expo/pull/37682) by [@huextrat](https://github.com/huextrat))
|
|
17
|
+
- [iOS] Fix caching resized images from Photo Library. ([#38105](https://github.com/expo/expo/pull/38105) by [@jakex7](https://github.com/jakex7))
|
|
18
|
+
- [iOS] Fix `generatePlaceholder` method syntax error by removing unwanted trailing comma. ([#38318](https://github.com/expo/expo/pull/38318) by [@bortolilucas](https://github.com/bortolilucas))
|
|
19
|
+
|
|
11
20
|
### 💡 Others
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
### 📚 3rd party library updates
|
|
23
|
+
|
|
24
|
+
## 2.4.0 - 2025-07-17
|
|
14
25
|
|
|
15
26
|
### 🎉 New features
|
|
16
27
|
|
|
@@ -21,13 +32,13 @@
|
|
|
21
32
|
- [iOS] Speed up displaying local assets. ([#37795](https://github.com/expo/expo/pull/37795) by [@aleqsio](https://github.com/aleqsio))
|
|
22
33
|
- [iOS] Fix some operation were incorrectly cancelled. ([#37987](https://github.com/expo/expo/pull/37987) by [@lukmccall](https://github.com/lukmccall))
|
|
23
34
|
|
|
24
|
-
## 2.3.2
|
|
35
|
+
## 2.3.2 - 2025-07-01
|
|
25
36
|
|
|
26
37
|
### 🐛 Bug fixes
|
|
27
38
|
|
|
28
39
|
- [iOS] Use specified cache type when no transformation is applied ([#37777](https://github.com/expo/expo/pull/37777) by [@jakex7](https://github.com/jakex7))
|
|
29
40
|
|
|
30
|
-
## 2.3.1
|
|
41
|
+
## 2.3.1 - 2025-07-01
|
|
31
42
|
|
|
32
43
|
### 🐛 Bug fixes
|
|
33
44
|
|
|
@@ -37,17 +48,21 @@
|
|
|
37
48
|
|
|
38
49
|
- [Android] Bumped GIF Glide plugin to 3.0.5 for Android 16KB page size support. ([#37454](https://github.com/expo/expo/pull/37454) by [@kudo](https://github.com/kudo))
|
|
39
50
|
|
|
40
|
-
## 2.3.0
|
|
51
|
+
## 2.3.0 - 2025-06-11
|
|
41
52
|
|
|
42
53
|
### 🛠 Breaking changes
|
|
43
54
|
|
|
44
55
|
- [iOS] `useAppleWebpCodec` has been moved from the source object to the component's prop to make it usable with the local assets. ([#37300](https://github.com/expo/expo/pull/37300) by [@tsapeta](https://github.com/tsapeta))
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
### 🐛 Bug fixes
|
|
58
|
+
|
|
59
|
+
- [iOS] Fix blurry images when using `tintColor` by scaling `imageThumbnailPixelSize` with screen density. ([#37235](https://github.com/expo/expo/pull/37235) by [@hirbod](https://github.com/hirbod))
|
|
60
|
+
|
|
61
|
+
## 2.2.1 - 2025-06-10
|
|
47
62
|
|
|
48
63
|
_This version does not introduce any user-facing changes._
|
|
49
64
|
|
|
50
|
-
## 2.2.0
|
|
65
|
+
## 2.2.0 - 2025-06-04
|
|
51
66
|
|
|
52
67
|
### 🎉 New features
|
|
53
68
|
|
|
@@ -58,7 +73,7 @@ _This version does not introduce any user-facing changes._
|
|
|
58
73
|
- Fix React Server Components support. ([#36801](https://github.com/expo/expo/pull/36801) by [@EvanBacon](https://github.com/EvanBacon))
|
|
59
74
|
- [iOS] Fix PhotoLibrary assets being scaled twice. ([#36776](https://github.com/expo/expo/pull/36776) by [@alanjhughes](https://github.com/alanjhughes))
|
|
60
75
|
- [iOS] Don't add transformers when unnecessary. ([#36884](https://github.com/expo/expo/pull/36884) by [@jakex7](https://github.com/jakex7))
|
|
61
|
-
- [
|
|
76
|
+
- [Web] Fix `tintColor` in React 19. ([#37133](https://github.com/expo/expo/pull/37133) by [@bradleyayers](https://github.com/bradleyayers))
|
|
62
77
|
|
|
63
78
|
## 2.1.7 — 2025-05-06
|
|
64
79
|
|
package/android/build.gradle
CHANGED
|
@@ -8,7 +8,7 @@ android {
|
|
|
8
8
|
namespace "expo.modules.image"
|
|
9
9
|
defaultConfig {
|
|
10
10
|
versionCode 1
|
|
11
|
-
versionName "2.
|
|
11
|
+
versionName "2.5.0-canary-20250729-d8899ae"
|
|
12
12
|
consumerProguardFiles("proguard-rules.pro")
|
|
13
13
|
|
|
14
14
|
buildConfigField("boolean", "ALLOW_GLIDE_LOGS", project.properties.get("EXPO_ALLOW_GLIDE_LOGS", "false"))
|
|
@@ -5,6 +5,8 @@ package expo.modules.image
|
|
|
5
5
|
import android.graphics.Bitmap
|
|
6
6
|
import android.graphics.drawable.BitmapDrawable
|
|
7
7
|
import android.graphics.drawable.Drawable
|
|
8
|
+
import android.util.Base64
|
|
9
|
+
import androidx.core.graphics.drawable.toBitmap
|
|
8
10
|
import androidx.core.graphics.drawable.toBitmapOrNull
|
|
9
11
|
import androidx.core.view.doOnDetach
|
|
10
12
|
import com.bumptech.glide.Glide
|
|
@@ -19,6 +21,7 @@ import com.bumptech.glide.request.target.Target
|
|
|
19
21
|
import com.github.penfeizhou.animation.apng.APNGDrawable
|
|
20
22
|
import com.github.penfeizhou.animation.gif.GifDrawable
|
|
21
23
|
import com.github.penfeizhou.animation.webp.WebPDrawable
|
|
24
|
+
import expo.modules.image.blurhash.BlurhashEncoder
|
|
22
25
|
import expo.modules.image.enums.ContentFit
|
|
23
26
|
import expo.modules.image.enums.Priority
|
|
24
27
|
import expo.modules.image.records.CachePolicy
|
|
@@ -28,6 +31,7 @@ import expo.modules.image.records.DecodedSource
|
|
|
28
31
|
import expo.modules.image.records.ImageLoadOptions
|
|
29
32
|
import expo.modules.image.records.ImageTransition
|
|
30
33
|
import expo.modules.image.records.SourceMap
|
|
34
|
+
import expo.modules.image.thumbhash.ThumbhashEncoder
|
|
31
35
|
import expo.modules.kotlin.Promise
|
|
32
36
|
import expo.modules.kotlin.apifeatures.EitherType
|
|
33
37
|
import expo.modules.kotlin.exception.Exceptions
|
|
@@ -36,8 +40,12 @@ import expo.modules.kotlin.functions.Queues
|
|
|
36
40
|
import expo.modules.kotlin.modules.Module
|
|
37
41
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
38
42
|
import expo.modules.kotlin.sharedobjects.SharedRef
|
|
43
|
+
import expo.modules.kotlin.types.Either
|
|
39
44
|
import expo.modules.kotlin.types.EitherOfThree
|
|
40
45
|
import expo.modules.kotlin.types.toKClass
|
|
46
|
+
import kotlinx.coroutines.Dispatchers
|
|
47
|
+
import kotlinx.coroutines.withContext
|
|
48
|
+
import java.net.URL
|
|
41
49
|
|
|
42
50
|
class ExpoImageModule : Module() {
|
|
43
51
|
override fun definition() = ModuleDefinition {
|
|
@@ -112,6 +120,37 @@ class ExpoImageModule : Module() {
|
|
|
112
120
|
ImageLoadTask(appContext, source, options ?: ImageLoadOptions()).load()
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
suspend fun generatePlaceholder(
|
|
124
|
+
source: Either<URL, Image>,
|
|
125
|
+
encoder: (Bitmap) -> String
|
|
126
|
+
): String {
|
|
127
|
+
val image = source.let {
|
|
128
|
+
if (it.`is`(Image::class)) {
|
|
129
|
+
it.get(Image::class)
|
|
130
|
+
} else {
|
|
131
|
+
ImageLoadTask(appContext, SourceMap(uri = it.get(URL::class).toString()), ImageLoadOptions()).load()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return withContext(Dispatchers.Default) {
|
|
135
|
+
encoder(image.ref.toBitmap())
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
AsyncFunction("generateBlurhashAsync") Coroutine { source: Either<URL, Image>, numberOfComponents: Pair<Int, Int> ->
|
|
140
|
+
generatePlaceholder(source) { bitmap ->
|
|
141
|
+
BlurhashEncoder.encode(bitmap, numberOfComponents)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
AsyncFunction("generateThumbhashAsync") Coroutine { source: Either<URL, Image> ->
|
|
146
|
+
generatePlaceholder(source) { bitmap ->
|
|
147
|
+
Base64.encodeToString(
|
|
148
|
+
ThumbhashEncoder.encode(bitmap),
|
|
149
|
+
Base64.NO_WRAP
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
115
154
|
Class(Image::class) {
|
|
116
155
|
Property("width") { image: Image ->
|
|
117
156
|
image.ref.intrinsicWidth
|
|
@@ -18,6 +18,7 @@ import com.bumptech.glide.RequestManager
|
|
|
18
18
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
19
19
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
|
20
20
|
import com.bumptech.glide.request.RequestOptions
|
|
21
|
+
import com.github.penfeizhou.animation.gif.GifDrawable
|
|
21
22
|
import expo.modules.image.enums.ContentFit
|
|
22
23
|
import expo.modules.image.enums.Priority
|
|
23
24
|
import expo.modules.image.events.GlideRequestListener
|
|
@@ -169,14 +170,32 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
|
|
|
169
170
|
internal var cachePolicy: CachePolicy = CachePolicy.DISK
|
|
170
171
|
|
|
171
172
|
fun setIsAnimating(setAnimating: Boolean) {
|
|
172
|
-
|
|
173
|
+
// Animatable animations always start from the beginning when resumed.
|
|
174
|
+
// So we check first if the resource is a GifDrawable, because it can continue
|
|
175
|
+
// from where it was paused.
|
|
176
|
+
when (val resource = activeView.drawable) {
|
|
177
|
+
is GifDrawable -> setIsAnimating(resource, setAnimating)
|
|
178
|
+
is Animatable -> setIsAnimating(resource, setAnimating)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
173
181
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
private fun setIsAnimating(resource: GifDrawable, setAnimating: Boolean) {
|
|
183
|
+
if (setAnimating) {
|
|
184
|
+
if (resource.isPaused) {
|
|
185
|
+
resource.resume()
|
|
177
186
|
} else {
|
|
178
|
-
resource.
|
|
187
|
+
resource.start()
|
|
179
188
|
}
|
|
189
|
+
} else {
|
|
190
|
+
resource.pause()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private fun setIsAnimating(resource: Animatable, setAnimating: Boolean) {
|
|
195
|
+
if (setAnimating) {
|
|
196
|
+
resource.start()
|
|
197
|
+
} else {
|
|
198
|
+
resource.stop()
|
|
180
199
|
}
|
|
181
200
|
}
|
|
182
201
|
|
|
@@ -74,16 +74,7 @@ object BlurhashDecoder {
|
|
|
74
74
|
val r = colorEnc shr 16
|
|
75
75
|
val g = (colorEnc shr 8) and 255
|
|
76
76
|
val b = colorEnc and 255
|
|
77
|
-
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private fun srgbToLinear(colorEnc: Int): Float {
|
|
81
|
-
val v = colorEnc / 255f
|
|
82
|
-
return if (v <= 0.04045f) {
|
|
83
|
-
(v / 12.92f)
|
|
84
|
-
} else {
|
|
85
|
-
((v + 0.055f) / 1.055f).pow(2.4f)
|
|
86
|
-
}
|
|
77
|
+
return floatArrayOf(BlurhashHelpers.srgbToLinear(r), BlurhashHelpers.srgbToLinear(g), BlurhashHelpers.srgbToLinear(b))
|
|
87
78
|
}
|
|
88
79
|
|
|
89
80
|
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package expo.modules.image.blurhash
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import kotlin.math.*
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Rewritten in kotlin from https://github.com/woltapp/blurhash/blob/master/Swift/BlurHashEncode.swift
|
|
9
|
+
*/
|
|
10
|
+
object BlurhashEncoder {
|
|
11
|
+
fun encode(image: Bitmap, numberOfComponents: Pair<Int, Int>): String {
|
|
12
|
+
val pixels = IntArray(image.width * image.height)
|
|
13
|
+
image.getPixels(pixels, 0, image.width, 0, 0, image.width, image.height)
|
|
14
|
+
|
|
15
|
+
val factors = calculateBlurFactors(pixels, image.width, image.height, numberOfComponents)
|
|
16
|
+
|
|
17
|
+
val dc = factors.first()
|
|
18
|
+
val ac = factors.drop(1)
|
|
19
|
+
val hashBuilder = StringBuilder()
|
|
20
|
+
|
|
21
|
+
encodeFlag(numberOfComponents, hashBuilder)
|
|
22
|
+
val maximumValue = encodeMaximumValue(ac, hashBuilder)
|
|
23
|
+
hashBuilder.append(encode83(encodeDC(dc), 4))
|
|
24
|
+
for (factor in ac) {
|
|
25
|
+
hashBuilder.append(encode83(encodeAC(factor, maximumValue), 2))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return hashBuilder.toString()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private fun encodeFlag(numberOfComponents: Pair<Int, Int>, hashBuilder: StringBuilder) {
|
|
32
|
+
val sizeFlag = (numberOfComponents.first - 1) + (numberOfComponents.second - 1) * 9
|
|
33
|
+
hashBuilder.append(encode83(sizeFlag, 1))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private fun encodeMaximumValue(ac: List<Triple<Float, Float, Float>>, hash: StringBuilder): Float {
|
|
37
|
+
val maximumValue: Float
|
|
38
|
+
if (ac.isNotEmpty()) {
|
|
39
|
+
val actualMaximumValue = ac.maxOf { t -> max(max(abs(t.first), abs(t.second)), abs(t.third)) }
|
|
40
|
+
val quantisedMaximumValue = max(0f, min(82f, floor(actualMaximumValue * 166f - 0.5f))).toInt()
|
|
41
|
+
maximumValue = (quantisedMaximumValue + 1).toFloat() / 166f
|
|
42
|
+
hash.append(encode83(quantisedMaximumValue, 1))
|
|
43
|
+
} else {
|
|
44
|
+
maximumValue = 1f
|
|
45
|
+
hash.append(encode83(0, 1))
|
|
46
|
+
}
|
|
47
|
+
return maximumValue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private fun calculateBlurFactors(pixels: IntArray, width: Int, height: Int, numberOfComponents: Pair<Int, Int>): List<Triple<Float, Float, Float>> {
|
|
51
|
+
val factors = mutableListOf<Triple<Float, Float, Float>>()
|
|
52
|
+
for (y in 0 until numberOfComponents.second) {
|
|
53
|
+
for (x in 0 until numberOfComponents.first) {
|
|
54
|
+
val normalisation = if (x == 0 && y == 0) 1f else 2f
|
|
55
|
+
val factor = multiplyBasisFunction(pixels, width, height, x, y, normalisation)
|
|
56
|
+
factors.add(factor)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return factors
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun encode83(value: Int, length: Int): String {
|
|
63
|
+
var result = ""
|
|
64
|
+
for (i in 1..length) {
|
|
65
|
+
val digit = (value / 83f.pow((length - i).toFloat())) % 83f
|
|
66
|
+
result += ENCODE_CHARACTERS[digit.toInt()]
|
|
67
|
+
}
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private const val ENCODE_CHARACTERS =
|
|
72
|
+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
|
|
73
|
+
|
|
74
|
+
private fun encodeDC(value: Triple<Float, Float, Float>): Int {
|
|
75
|
+
val roundedR = BlurhashHelpers.linearTosRGB(value.first)
|
|
76
|
+
val roundedG = BlurhashHelpers.linearTosRGB(value.second)
|
|
77
|
+
val roundedB = BlurhashHelpers.linearTosRGB(value.third)
|
|
78
|
+
return (roundedR shl 16) + (roundedG shl 8) + roundedB
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private fun encodeAC(value: Triple<Float, Float, Float>, maximumValue: Float): Int {
|
|
82
|
+
val quantR = max(0f, min(18f, floor(BlurhashHelpers.signPow(value.first / maximumValue, 0.5f) * 9f + 9.5f)))
|
|
83
|
+
val quantG = max(0f, min(18f, floor(BlurhashHelpers.signPow(value.second / maximumValue, 0.5f) * 9f + 9.5f)))
|
|
84
|
+
val quantB = max(0f, min(18f, floor(BlurhashHelpers.signPow(value.third / maximumValue, 0.5f) * 9f + 9.5f)))
|
|
85
|
+
|
|
86
|
+
return (quantR * 19f * 19f + quantG * 19f + quantB).toInt()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun multiplyBasisFunction(pixels: IntArray, width: Int, height: Int, x: Int, y: Int, normalisation: Float): Triple<Float, Float, Float> {
|
|
90
|
+
var r = 0f
|
|
91
|
+
var g = 0f
|
|
92
|
+
var b = 0f
|
|
93
|
+
|
|
94
|
+
for (j in 0 until height) {
|
|
95
|
+
for (i in 0 until width) {
|
|
96
|
+
val basis = normalisation * cos(PI.toFloat() * x * i / width) * cos(PI.toFloat() * y * j / height)
|
|
97
|
+
|
|
98
|
+
val pixel = pixels[i + j * width]
|
|
99
|
+
val pr = BlurhashHelpers.srgbToLinear(Color.red(pixel))
|
|
100
|
+
val pg = BlurhashHelpers.srgbToLinear(Color.green(pixel))
|
|
101
|
+
val pb = BlurhashHelpers.srgbToLinear(Color.blue(pixel))
|
|
102
|
+
|
|
103
|
+
r += basis * pr
|
|
104
|
+
g += basis * pg
|
|
105
|
+
b += basis * pb
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
val scale = 1f / (width * height)
|
|
110
|
+
return Triple(r * scale, g * scale, b * scale)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package expo.modules.image.blurhash
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import kotlin.math.*
|
|
5
|
+
|
|
6
|
+
object BlurhashHelpers {
|
|
7
|
+
fun srgbToLinear(colorEnc: Int): Float {
|
|
8
|
+
val v = colorEnc / 255f
|
|
9
|
+
return if (v <= 0.04045f) {
|
|
10
|
+
(v / 12.92f)
|
|
11
|
+
} else {
|
|
12
|
+
((v + 0.055f) / 1.055f).pow(2.4f)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fun linearTosRGB(value: Float): Int {
|
|
17
|
+
val v = max(0f, min(1f, value))
|
|
18
|
+
return if (v <= 0.0031308) {
|
|
19
|
+
(v * 12.92 * 255 + 0.5).toInt()
|
|
20
|
+
} else {
|
|
21
|
+
(1.055 * (v.pow(1f / 2.4f) - 0.055) * 255 + 0.5).toInt()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun signPow(value: Float, exp: Float): Float {
|
|
26
|
+
return abs(value).pow(exp) * sign(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fun getBitsPerPixel(bitmap: Bitmap): Int {
|
|
30
|
+
return when (bitmap.config) {
|
|
31
|
+
Bitmap.Config.ARGB_8888 -> 32
|
|
32
|
+
Bitmap.Config.RGB_565 -> 16
|
|
33
|
+
Bitmap.Config.ALPHA_8 -> 8
|
|
34
|
+
Bitmap.Config.ARGB_4444 -> 16
|
|
35
|
+
else -> 0
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -28,7 +28,7 @@ class GlideUrlWrapperLoader(
|
|
|
28
28
|
originalResponse
|
|
29
29
|
.newBuilder()
|
|
30
30
|
.body(
|
|
31
|
-
ProgressResponseBody(originalResponse.body) { bytesWritten, contentLength, done ->
|
|
31
|
+
ProgressResponseBody(requireNotNull(originalResponse.body)) { bytesWritten, contentLength, done ->
|
|
32
32
|
model.progressListener?.onProgress(bytesWritten, contentLength, done)
|
|
33
33
|
}
|
|
34
34
|
)
|
|
@@ -5,113 +5,6 @@ import android.graphics.Color
|
|
|
5
5
|
|
|
6
6
|
// ThumbHash Java implementation (converted to kotlin) thanks to @evanw https://github.com/evanw/thumbhash
|
|
7
7
|
object ThumbhashDecoder {
|
|
8
|
-
/**
|
|
9
|
-
* Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A.
|
|
10
|
-
*
|
|
11
|
-
* @param w The width of the input image. Must be ≤100px.
|
|
12
|
-
* @param h The height of the input image. Must be ≤100px.
|
|
13
|
-
* @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements.
|
|
14
|
-
* @return The ThumbHash as a byte array.
|
|
15
|
-
*/
|
|
16
|
-
fun rgbaToThumbHash(w: Int, h: Int, rgba: ByteArray): ByteArray {
|
|
17
|
-
// Encoding an image larger than 100x100 is slow with no benefit
|
|
18
|
-
require(!(w > 100 || h > 100)) { w.toString() + "x" + h + " doesn't fit in 100x100" }
|
|
19
|
-
|
|
20
|
-
// Determine the average color
|
|
21
|
-
var avg_r = 0f
|
|
22
|
-
var avg_g = 0f
|
|
23
|
-
var avg_b = 0f
|
|
24
|
-
var avg_a = 0f
|
|
25
|
-
var i = 0
|
|
26
|
-
var j = 0
|
|
27
|
-
while (i < w * h) {
|
|
28
|
-
val alpha = (rgba[j + 3].toInt() and 255) / 255.0f
|
|
29
|
-
avg_r += alpha / 255.0f * (rgba[j].toInt() and 255)
|
|
30
|
-
avg_g += alpha / 255.0f * (rgba[j + 1].toInt() and 255)
|
|
31
|
-
avg_b += alpha / 255.0f * (rgba[j + 2].toInt() and 255)
|
|
32
|
-
avg_a += alpha
|
|
33
|
-
i++
|
|
34
|
-
j += 4
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (avg_a > 0) {
|
|
38
|
-
avg_r /= avg_a
|
|
39
|
-
avg_g /= avg_a
|
|
40
|
-
avg_b /= avg_a
|
|
41
|
-
}
|
|
42
|
-
val hasAlpha = avg_a < w * h
|
|
43
|
-
val l_limit = if (hasAlpha) 5 else 7 // Use fewer luminance bits if there's alpha
|
|
44
|
-
val lx = Math.max(1, Math.round((l_limit * w).toFloat() / Math.max(w, h).toFloat()))
|
|
45
|
-
val ly = Math.max(1, Math.round((l_limit * h).toFloat() / Math.max(w, h).toFloat()))
|
|
46
|
-
val l = FloatArray(w * h) // luminance
|
|
47
|
-
val p = FloatArray(w * h) // yellow - blue
|
|
48
|
-
val q = FloatArray(w * h) // red - green
|
|
49
|
-
val a = FloatArray(w * h) // alpha
|
|
50
|
-
|
|
51
|
-
// Convert the image from RGBA to LPQA (composite atop the average color)
|
|
52
|
-
i = 0
|
|
53
|
-
j = 0
|
|
54
|
-
while (i < w * h) {
|
|
55
|
-
val alpha = (rgba[j + 3].toInt() and 255) / 255.0f
|
|
56
|
-
val r = avg_r * (1.0f - alpha) + alpha / 255.0f * (rgba[j].toInt() and 255)
|
|
57
|
-
val g = avg_g * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 1].toInt() and 255)
|
|
58
|
-
val b = avg_b * (1.0f - alpha) + alpha / 255.0f * (rgba[j + 2].toInt() and 255)
|
|
59
|
-
l[i] = (r + g + b) / 3.0f
|
|
60
|
-
p[i] = (r + g) / 2.0f - b
|
|
61
|
-
q[i] = r - g
|
|
62
|
-
a[i] = alpha
|
|
63
|
-
i++
|
|
64
|
-
j += 4
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Encode using the DCT into DC (constant) and normalized AC (varying) terms
|
|
68
|
-
val l_channel = Channel(Math.max(3, lx), Math.max(3, ly)).encode(w, h, l)
|
|
69
|
-
val p_channel = Channel(3, 3).encode(w, h, p)
|
|
70
|
-
val q_channel = Channel(3, 3).encode(w, h, q)
|
|
71
|
-
val a_channel = if (hasAlpha) Channel(5, 5).encode(w, h, a) else null
|
|
72
|
-
|
|
73
|
-
// Write the constants
|
|
74
|
-
val isLandscape = w > h
|
|
75
|
-
val header24 = (
|
|
76
|
-
Math.round(63.0f * l_channel.dc)
|
|
77
|
-
or (Math.round(31.5f + 31.5f * p_channel.dc) shl 6)
|
|
78
|
-
or (Math.round(31.5f + 31.5f * q_channel.dc) shl 12)
|
|
79
|
-
or (Math.round(31.0f * l_channel.scale) shl 18)
|
|
80
|
-
or if (hasAlpha) 1 shl 23 else 0
|
|
81
|
-
)
|
|
82
|
-
val header16 = (
|
|
83
|
-
(if (isLandscape) ly else lx)
|
|
84
|
-
or (Math.round(63.0f * p_channel.scale) shl 3)
|
|
85
|
-
or (Math.round(63.0f * q_channel.scale) shl 9)
|
|
86
|
-
or if (isLandscape) 1 shl 15 else 0
|
|
87
|
-
)
|
|
88
|
-
val ac_start = if (hasAlpha) 6 else 5
|
|
89
|
-
val ac_count = (
|
|
90
|
-
l_channel.ac.size + p_channel.ac.size + q_channel.ac.size +
|
|
91
|
-
if (hasAlpha) a_channel!!.ac.size else 0
|
|
92
|
-
)
|
|
93
|
-
val hash = ByteArray(ac_start + (ac_count + 1) / 2)
|
|
94
|
-
hash[0] = header24.toByte()
|
|
95
|
-
hash[1] = (header24 shr 8).toByte()
|
|
96
|
-
hash[2] = (header24 shr 16).toByte()
|
|
97
|
-
hash[3] = header16.toByte()
|
|
98
|
-
hash[4] = (header16 shr 8).toByte()
|
|
99
|
-
if (hasAlpha) {
|
|
100
|
-
hash[5] = (
|
|
101
|
-
Math.round(15.0f * a_channel!!.dc)
|
|
102
|
-
or (Math.round(15.0f * a_channel.scale) shl 4)
|
|
103
|
-
).toByte()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Write the varying factors
|
|
107
|
-
var ac_index = 0
|
|
108
|
-
ac_index = l_channel.writeTo(hash, ac_start, ac_index)
|
|
109
|
-
ac_index = p_channel.writeTo(hash, ac_start, ac_index)
|
|
110
|
-
ac_index = q_channel.writeTo(hash, ac_start, ac_index)
|
|
111
|
-
if (hasAlpha) a_channel!!.writeTo(hash, ac_start, ac_index)
|
|
112
|
-
return hash
|
|
113
|
-
}
|
|
114
|
-
|
|
115
8
|
/**
|
|
116
9
|
* Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
|
|
117
10
|
*
|
|
@@ -302,11 +195,8 @@ object ThumbhashDecoder {
|
|
|
302
195
|
|
|
303
196
|
class Image(var width: Int, var height: Int, var rgba: ByteArray)
|
|
304
197
|
class RGBA(var r: Float, var g: Float, var b: Float, var a: Float)
|
|
305
|
-
private class Channel
|
|
306
|
-
var dc = 0f
|
|
198
|
+
private class Channel(nx: Int, ny: Int) {
|
|
307
199
|
var ac: FloatArray
|
|
308
|
-
var scale = 0f
|
|
309
|
-
|
|
310
200
|
init {
|
|
311
201
|
var n = 0
|
|
312
202
|
for (cy in 0 until ny) {
|
|
@@ -319,32 +209,6 @@ object ThumbhashDecoder {
|
|
|
319
209
|
ac = FloatArray(n)
|
|
320
210
|
}
|
|
321
211
|
|
|
322
|
-
fun encode(w: Int, h: Int, channel: FloatArray): Channel {
|
|
323
|
-
var n = 0
|
|
324
|
-
val fx = FloatArray(w)
|
|
325
|
-
for (cy in 0 until ny) {
|
|
326
|
-
var cx = 0
|
|
327
|
-
while (cx * ny < nx * (ny - cy)) {
|
|
328
|
-
var f = 0f
|
|
329
|
-
for (x in 0 until w) fx[x] = Math.cos(Math.PI / w * cx * (x + 0.5f)).toFloat()
|
|
330
|
-
for (y in 0 until h) {
|
|
331
|
-
val fy = Math.cos(Math.PI / h * cy * (y + 0.5f)).toFloat()
|
|
332
|
-
for (x in 0 until w) f += channel[x + y * w] * fx[x] * fy
|
|
333
|
-
}
|
|
334
|
-
f /= (w * h).toFloat()
|
|
335
|
-
if (cx > 0 || cy > 0) {
|
|
336
|
-
ac[n++] = f
|
|
337
|
-
scale = Math.max(scale, Math.abs(f))
|
|
338
|
-
} else {
|
|
339
|
-
dc = f
|
|
340
|
-
}
|
|
341
|
-
cx++
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (scale > 0) for (i in ac.indices) ac[i] = 0.5f + 0.5f / scale * ac[i]
|
|
345
|
-
return this
|
|
346
|
-
}
|
|
347
|
-
|
|
348
212
|
fun decode(hash: ByteArray, start: Int, index: Int, scale: Float): Int {
|
|
349
213
|
var currentIndex = index
|
|
350
214
|
for (i in ac.indices) {
|
|
@@ -354,14 +218,5 @@ object ThumbhashDecoder {
|
|
|
354
218
|
}
|
|
355
219
|
return currentIndex
|
|
356
220
|
}
|
|
357
|
-
|
|
358
|
-
fun writeTo(hash: ByteArray, start: Int, index: Int): Int {
|
|
359
|
-
var currentIndex = index
|
|
360
|
-
for (v in ac) {
|
|
361
|
-
hash[start + (currentIndex shr 1)] = (hash[start + (currentIndex shr 1)].toInt() or (Math.round(15.0f * v) shl (currentIndex and 1 shl 2))).toByte()
|
|
362
|
-
currentIndex++
|
|
363
|
-
}
|
|
364
|
-
return currentIndex
|
|
365
|
-
}
|
|
366
221
|
}
|
|
367
222
|
}
|