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.
Files changed (70) hide show
  1. package/CHANGELOG.md +22 -7
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/expo/modules/image/ExpoImageModule.kt +39 -0
  4. package/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt +24 -5
  5. package/android/src/main/java/expo/modules/image/blurhash/BlurhashDecoder.kt +1 -10
  6. package/android/src/main/java/expo/modules/image/blurhash/BlurhashEncoder.kt +112 -0
  7. package/android/src/main/java/expo/modules/image/blurhash/BlurhashHelpers.kt +38 -0
  8. package/android/src/main/java/expo/modules/image/okhttp/GlideUrlWrapperLoader.kt +1 -1
  9. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashDecoder.kt +1 -146
  10. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashEncoder.kt +184 -0
  11. package/build/ExpoImage.web.d.ts +1 -1
  12. package/build/ExpoImage.web.d.ts.map +1 -1
  13. package/build/Image.d.ts +11 -2
  14. package/build/Image.d.ts.map +1 -1
  15. package/build/Image.types.d.ts +9 -5
  16. package/build/Image.types.d.ts.map +1 -1
  17. package/build/web/ImageWrapper.d.ts.map +1 -1
  18. package/expo-module.config.json +1 -1
  19. package/ios/ExpoImage.podspec +1 -1
  20. package/ios/ImageModule.swift +33 -12
  21. package/ios/ImageSource.swift +1 -2
  22. package/ios/ImageUtils.swift +4 -8
  23. package/ios/ImageView.swift +1 -1
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml +4 -4
  45. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.md5 +1 -1
  46. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha1 +1 -1
  47. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha256 +1 -1
  48. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha512 +1 -1
  49. package/package.json +4 -5
  50. package/src/ExpoImage.web.tsx +2 -1
  51. package/src/Image.tsx +15 -3
  52. package/src/Image.types.ts +9 -5
  53. package/src/web/ImageWrapper.tsx +8 -1
  54. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.md5 +0 -1
  55. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.sha1 +0 -1
  56. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.sha256 +0 -1
  57. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0-sources.jar.sha512 +0 -1
  58. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar +0 -0
  59. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.md5 +0 -1
  60. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.sha1 +0 -1
  61. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.sha256 +0 -1
  62. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.aar.sha512 +0 -1
  63. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.md5 +0 -1
  64. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.sha1 +0 -1
  65. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.sha256 +0 -1
  66. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.module.sha512 +0 -1
  67. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.pom.md5 +0 -1
  68. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.pom.sha1 +0 -1
  69. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0/expo.modules.image-2.4.0.pom.sha256 +0 -1
  70. 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
- ## 2.4.0 2025-07-17
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 2025-07-01
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 2025-07-01
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 2025-06-11
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
- ## 2.2.1 2025-06-10
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 2025-06-04
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
- - [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))
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
 
@@ -8,7 +8,7 @@ android {
8
8
  namespace "expo.modules.image"
9
9
  defaultConfig {
10
10
  versionCode 1
11
- versionName "2.4.0"
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
- val resource = activeView.drawable
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
- if (resource is Animatable) {
175
- if (setAnimating) {
176
- resource.start()
182
+ private fun setIsAnimating(resource: GifDrawable, setAnimating: Boolean) {
183
+ if (setAnimating) {
184
+ if (resource.isPaused) {
185
+ resource.resume()
177
186
  } else {
178
- resource.stop()
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 internal constructor(var nx: Int, var ny: Int) {
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
  }