expo-image 3.0.0-canary-20250404-3c3b5fd → 3.0.0

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 (91) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/expo/modules/image/ExpoImageModule.kt +52 -0
  4. package/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt +32 -8
  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.d.ts +5 -0
  12. package/build/ExpoImage.d.ts.map +1 -1
  13. package/build/ExpoImage.web.d.ts +1 -1
  14. package/build/ExpoImage.web.d.ts.map +1 -1
  15. package/build/Image.d.ts +36 -7
  16. package/build/Image.d.ts.map +1 -1
  17. package/build/Image.types.d.ts +34 -22
  18. package/build/Image.types.d.ts.map +1 -1
  19. package/build/ImageModule.web.d.ts +1 -2
  20. package/build/ImageModule.web.d.ts.map +1 -1
  21. package/build/index.d.ts +2 -2
  22. package/build/index.d.ts.map +1 -1
  23. package/build/useImage.d.ts +1 -1
  24. package/build/useImage.d.ts.map +1 -1
  25. package/build/utils/blurhash/base83.d.ts.map +1 -1
  26. package/build/utils/blurhash/decode.d.ts +1 -1
  27. package/build/utils/blurhash/decode.d.ts.map +1 -1
  28. package/build/utils/blurhash/useBlurhash.d.ts +2 -2
  29. package/build/utils/blurhash/useBlurhash.d.ts.map +1 -1
  30. package/build/utils/blurhash/utils.d.ts.map +1 -1
  31. package/build/utils/thumbhash/thumbhash.d.ts +2 -2
  32. package/build/utils/thumbhash/thumbhash.d.ts.map +1 -1
  33. package/build/web/AnimationManager.d.ts.map +1 -1
  34. package/build/web/ImageWrapper.d.ts.map +1 -1
  35. package/build/web/getImageWrapperEventHandler.d.ts +4 -3
  36. package/build/web/getImageWrapperEventHandler.d.ts.map +1 -1
  37. package/build/web/useSourceSelection.d.ts +2 -2
  38. package/build/web/useSourceSelection.d.ts.map +1 -1
  39. package/expo-module.config.json +7 -1
  40. package/ios/ExpoImage.podspec +1 -1
  41. package/ios/ImageModule.swift +53 -12
  42. package/ios/ImageSource.swift +1 -5
  43. package/ios/ImageUtils.swift +6 -8
  44. package/ios/ImageView.swift +48 -19
  45. package/ios/Loaders/PhotoLibraryAssetLoader.swift +13 -2
  46. package/jest-rsc.config.js +1 -0
  47. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0-sources.jar +0 -0
  48. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0-sources.jar.md5 +1 -0
  49. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0-sources.jar.sha1 +1 -0
  50. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0-sources.jar.sha256 +1 -0
  51. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0-sources.jar.sha512 +1 -0
  52. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.aar +0 -0
  53. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.aar.md5 +1 -0
  54. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.aar.sha1 +1 -0
  55. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.aar.sha256 +1 -0
  56. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.aar.sha512 +1 -0
  57. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.module +177 -0
  58. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.module.md5 +1 -0
  59. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.module.sha1 +1 -0
  60. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.module.sha256 +1 -0
  61. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.module.sha512 +1 -0
  62. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.pom +88 -0
  63. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.pom.md5 +1 -0
  64. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.pom.sha1 +1 -0
  65. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.pom.sha256 +1 -0
  66. package/local-maven-repo/BareExpo/expo.modules.image/3.0.0/expo.modules.image-3.0.0.pom.sha512 +1 -0
  67. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml +13 -0
  68. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.md5 +1 -0
  69. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha1 +1 -0
  70. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha256 +1 -0
  71. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha512 +1 -0
  72. package/package.json +9 -4
  73. package/src/ExpoImage.tsx +7 -0
  74. package/src/ExpoImage.web.tsx +14 -7
  75. package/src/Image.tsx +50 -10
  76. package/src/Image.types.ts +36 -23
  77. package/src/ImageModule.web.ts +2 -5
  78. package/src/index.ts +2 -2
  79. package/src/ts-declarations/react-native-assets.d.ts +1 -0
  80. package/src/ts-declarations/react-native-web.d.ts +3 -0
  81. package/src/useImage.ts +1 -1
  82. package/src/utils/AssetSourceResolver.web.ts +1 -1
  83. package/src/utils/blurhash/decode.ts +1 -1
  84. package/src/utils/blurhash/useBlurhash.tsx +1 -1
  85. package/src/utils/resolveAssetSource.web.ts +1 -1
  86. package/src/utils/thumbhash/thumbhash.ts +4 -4
  87. package/src/web/AnimationManager.tsx +12 -5
  88. package/src/web/ImageWrapper.tsx +8 -1
  89. package/src/web/getImageWrapperEventHandler.ts +5 -3
  90. package/src/web/positioning.ts +1 -1
  91. package/tsconfig.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,120 @@
4
4
 
5
5
  ### 🛠 Breaking changes
6
6
 
7
+ ### 🎉 New features
8
+
9
+ ### 🐛 Bug fixes
10
+
11
+ ### 💡 Others
12
+
13
+ ## 3.0.0 — 2025-08-13
14
+
15
+ ### 🎉 New features
16
+
17
+ - Add `generateThumbhashAsync` ([#38090](https://github.com/expo/expo/pull/38090) by [@Wenszel](https://github.com/Wenszel))
18
+ - Add support for `ImageRef` source in `generateBlurhashAsync` ([#37901](https://github.com/expo/expo/pull/37901) by [@Wenszel](https://github.com/Wenszel))
19
+ - [Android] Add generateBlurhashAsync ([#37817](https://github.com/expo/expo/pull/37817) by [@Wenszel](https://github.com/Wenszel))
20
+
21
+ ### 🐛 Bug fixes
22
+
23
+ - [Android] Fix animation resuming by casting image to GifDrawable. ([#37363](https://github.com/expo/expo/pull/37363) by [@Wenszel](https://github.com/Wenszel))
24
+ - [Web] Fix `alt` as an alias for `accessibilityLabel` ([#37682](https://github.com/expo/expo/pull/37682) by [@huextrat](https://github.com/huextrat))
25
+ - [iOS] Fix caching resized images from Photo Library. ([#38105](https://github.com/expo/expo/pull/38105) by [@jakex7](https://github.com/jakex7))
26
+ - [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))
27
+
28
+ ## 2.4.0 - 2025-07-17
29
+
30
+ ### 🎉 New features
31
+
32
+ - [iOS] Add a new prop - `enforceEarlyResizing` to reduce the memory usage of the image view. ([#37909](https://github.com/expo/expo/pull/37909) by [@lukmccall](https://github.com/lukmccall))
33
+
34
+ ### 🐛 Bug fixes
35
+
36
+ - [iOS] Speed up displaying local assets. ([#37795](https://github.com/expo/expo/pull/37795) by [@aleqsio](https://github.com/aleqsio))
37
+ - [iOS] Fix some operation were incorrectly cancelled. ([#37987](https://github.com/expo/expo/pull/37987) by [@lukmccall](https://github.com/lukmccall))
38
+
39
+ ## 2.3.2 - 2025-07-01
40
+
41
+ ### 🐛 Bug fixes
42
+
43
+ - [iOS] Use specified cache type when no transformation is applied ([#37777](https://github.com/expo/expo/pull/37777) by [@jakex7](https://github.com/jakex7))
44
+
45
+ ## 2.3.1 - 2025-07-01
46
+
47
+ ### 🐛 Bug fixes
48
+
49
+ - [iOS] Fixed contentPosition is not correct after switching theme. ([#37374](https://github.com/expo/expo/pull/37374) by [@kudo](https://github.com/kudo))
50
+
51
+ ### 📚 3rd party library updates
52
+
53
+ - [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))
54
+
55
+ ## 2.3.0 - 2025-06-11
56
+
57
+ ### 🛠 Breaking changes
58
+
59
+ - [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))
60
+
61
+ ### 🐛 Bug fixes
62
+
63
+ - [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))
64
+
65
+ ## 2.2.1 - 2025-06-10
66
+
67
+ _This version does not introduce any user-facing changes._
68
+
69
+ ## 2.2.0 - 2025-06-04
70
+
71
+ ### 🎉 New features
72
+
73
+ - Add imperative api to lock/unlock/reload resource. ([#36912](https://github.com/expo/expo/pull/36912) by [@jakex7](https://github.com/jakex7))
74
+
75
+ ### 🐛 Bug fixes
76
+
77
+ - Fix React Server Components support. ([#36801](https://github.com/expo/expo/pull/36801) by [@EvanBacon](https://github.com/EvanBacon))
78
+ - [iOS] Fix PhotoLibrary assets being scaled twice. ([#36776](https://github.com/expo/expo/pull/36776) by [@alanjhughes](https://github.com/alanjhughes))
79
+ - [iOS] Don't add transformers when unnecessary. ([#36884](https://github.com/expo/expo/pull/36884) by [@jakex7](https://github.com/jakex7))
80
+ - [Web] Fix `tintColor` in React 19. ([#37133](https://github.com/expo/expo/pull/37133) by [@bradleyayers](https://github.com/bradleyayers))
81
+
82
+ ## 2.1.7 — 2025-05-06
83
+
84
+ _This version does not introduce any user-facing changes._
85
+
86
+ ## 2.1.6 — 2025-04-30
87
+
88
+ _This version does not introduce any user-facing changes._
89
+
90
+ ## 2.1.5 — 2025-04-25
91
+
92
+ ### 🐛 Bug fixes
93
+
94
+ - Fixed `CUICatalog: Invalid asset name supplied: ''` error on iOS when the path is empty. ([#36294](https://github.com/expo/expo/pull/36294) by [@Innei](https://github.com/Innei))
95
+
96
+ ## 2.1.4 — 2025-04-14
97
+
98
+ ### 🐛 Bug fixes
99
+
100
+ - Fixed SVG image tinting on iOS. ([#35927](https://github.com/expo/expo/pull/35927) by [@kudo](https://github.com/kudo))
101
+ - [Android] Fixed OutOfMemoryError crash when displaying some gif images ([#36097](https://github.com/expo/expo/pull/36097) by [@rahimrahman](https://github.com/rahimrahman))
102
+
103
+ ## 2.1.3 — 2025-04-11
104
+
105
+ _This version does not introduce any user-facing changes._
106
+
107
+ ## 2.1.2 — 2025-04-09
108
+
109
+ _This version does not introduce any user-facing changes._
110
+
111
+ ## 2.1.1 — 2025-04-08
112
+
113
+ ### 🐛 Bug fixes
114
+
115
+ - Fixed SVG image tinting on iOS. ([#35927](https://github.com/expo/expo/pull/35927) by [@kudo](https://github.com/kudo))
116
+
117
+ ## 2.1.0 — 2025-04-04
118
+
119
+ ### 🛠 Breaking changes
120
+
7
121
  - upgrade RN to 0.78 ([#35050](https://github.com/expo/expo/pull/35050) by [@vonovak](https://github.com/vonovak))
8
122
 
9
123
  ### 🎉 New features
@@ -8,7 +8,7 @@ android {
8
8
  namespace "expo.modules.image"
9
9
  defaultConfig {
10
10
  versionCode 1
11
- versionName "2.0.2"
11
+ versionName "3.0.0"
12
12
  consumerProguardFiles("proguard-rules.pro")
13
13
 
14
14
  buildConfigField("boolean", "ALLOW_GLIDE_LOGS", project.properties.get("EXPO_ALLOW_GLIDE_LOGS", "false"))
@@ -34,7 +34,7 @@ dependencies {
34
34
  kapt "com.github.bumptech.glide:compiler:${GLIDE_VERSION}"
35
35
  api 'com.caverock:androidsvg-aar:1.4'
36
36
 
37
- implementation "com.github.penfeizhou.android.animation:glide-plugin:3.0.2"
37
+ implementation "com.github.penfeizhou.android.animation:glide-plugin:3.0.5"
38
38
  implementation "com.github.bumptech.glide:avif-integration:${GLIDE_VERSION}"
39
39
 
40
40
  api 'com.github.bumptech.glide:okhttp3-integration:4.11.0'
@@ -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
@@ -275,6 +314,19 @@ class ExpoImageModule : Module() {
275
314
  view.setIsAnimating(false)
276
315
  }
277
316
 
317
+ AsyncFunction("lockResourceAsync") { view: ExpoImageViewWrapper ->
318
+ view.lockResource = true
319
+ }
320
+
321
+ AsyncFunction("unlockResourceAsync") { view: ExpoImageViewWrapper ->
322
+ view.lockResource = false
323
+ }
324
+
325
+ AsyncFunction("reloadAsync") { view: ExpoImageViewWrapper ->
326
+ view.shouldRerender = true
327
+ view.rerenderIfNeeded(force = true)
328
+ }
329
+
278
330
  OnViewDidUpdateProps { view: ExpoImageViewWrapper ->
279
331
  view.rerenderIfNeeded()
280
332
  }
@@ -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
@@ -163,25 +164,45 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
163
164
 
164
165
  internal var autoplay: Boolean = true
165
166
 
167
+ internal var lockResource: Boolean = false
168
+
166
169
  internal var priority: Priority = Priority.NORMAL
167
170
  internal var cachePolicy: CachePolicy = CachePolicy.DISK
168
171
 
169
172
  fun setIsAnimating(setAnimating: Boolean) {
170
- 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
+ }
171
181
 
172
- if (resource is Animatable) {
173
- if (setAnimating) {
174
- resource.start()
182
+ private fun setIsAnimating(resource: GifDrawable, setAnimating: Boolean) {
183
+ if (setAnimating) {
184
+ if (resource.isPaused) {
185
+ resource.resume()
175
186
  } else {
176
- resource.stop()
187
+ resource.start()
177
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()
178
199
  }
179
200
  }
180
201
 
181
202
  /**
182
203
  * Whether the image should be loaded again
183
204
  */
184
- private var shouldRerender = false
205
+ internal var shouldRerender = false
185
206
 
186
207
  /**
187
208
  * Currently loaded source
@@ -477,8 +498,11 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
477
498
  }
478
499
  }
479
500
 
480
- internal fun rerenderIfNeeded(shouldRerenderBecauseOfResize: Boolean = false) =
481
- trace(Trace.tag, "rerenderIfNeeded(shouldRerenderBecauseOfResize=$shouldRerenderBecauseOfResize)") {
501
+ internal fun rerenderIfNeeded(shouldRerenderBecauseOfResize: Boolean = false, force: Boolean = false) =
502
+ trace(Trace.tag, "rerenderIfNeeded(shouldRerenderBecauseOfResize=$shouldRerenderBecauseOfResize,force=$force)") {
503
+ if (lockResource && !force) {
504
+ return@trace
505
+ }
482
506
  val bestSource = bestSource
483
507
  val bestPlaceholder = bestPlaceholder
484
508
 
@@ -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
  }