expo-image 2.4.0-canary-20250713-8f814f8 → 2.5.0-canary-20250722-599a28f

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 (57) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/expo/modules/image/ExpoImageModule.kt +23 -4
  4. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashDecoder.kt +1 -146
  5. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashEncoder.kt +184 -0
  6. package/build/Image.d.ts +8 -0
  7. package/build/Image.d.ts.map +1 -1
  8. package/expo-module.config.json +1 -1
  9. package/ios/ImageModule.swift +27 -15
  10. package/ios/ImageSource.swift +1 -2
  11. package/ios/ImageUtils.swift +4 -8
  12. package/ios/ImageView.swift +4 -6
  13. package/ios/Loaders/PhotoLibraryAssetLoader.swift +1 -1
  14. package/local-maven-repo/BareExpo/expo.modules.image/{2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8-sources.jar → 2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f-sources.jar} +0 -0
  15. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f-sources.jar.md5 +1 -0
  16. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f-sources.jar.sha1 +1 -0
  17. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f-sources.jar.sha256 +1 -0
  18. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f-sources.jar.sha512 +1 -0
  19. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.aar +0 -0
  20. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.aar.md5 +1 -0
  21. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.aar.sha1 +1 -0
  22. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.aar.sha256 +1 -0
  23. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.aar.sha512 +1 -0
  24. package/local-maven-repo/BareExpo/expo.modules.image/{2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.module → 2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.module} +22 -22
  25. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.module.md5 +1 -0
  26. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.module.sha1 +1 -0
  27. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.module.sha256 +1 -0
  28. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.module.sha512 +1 -0
  29. package/local-maven-repo/BareExpo/expo.modules.image/{2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.pom → 2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.pom} +1 -1
  30. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.pom.md5 +1 -0
  31. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.pom.sha1 +1 -0
  32. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.pom.sha256 +1 -0
  33. package/local-maven-repo/BareExpo/expo.modules.image/2.5.0-canary-20250722-599a28f/expo.modules.image-2.5.0-canary-20250722-599a28f.pom.sha512 +1 -0
  34. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml +4 -4
  35. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.md5 +1 -1
  36. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha1 +1 -1
  37. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha256 +1 -1
  38. package/local-maven-repo/BareExpo/expo.modules.image/maven-metadata.xml.sha512 +1 -1
  39. package/package.json +3 -3
  40. package/src/Image.tsx +11 -0
  41. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8-sources.jar.md5 +0 -1
  42. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8-sources.jar.sha1 +0 -1
  43. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8-sources.jar.sha256 +0 -1
  44. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8-sources.jar.sha512 +0 -1
  45. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.aar +0 -0
  46. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.aar.md5 +0 -1
  47. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.aar.sha1 +0 -1
  48. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.aar.sha256 +0 -1
  49. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.aar.sha512 +0 -1
  50. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.module.md5 +0 -1
  51. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.module.sha1 +0 -1
  52. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.module.sha256 +0 -1
  53. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.module.sha512 +0 -1
  54. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.pom.md5 +0 -1
  55. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.pom.sha1 +0 -1
  56. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.pom.sha256 +0 -1
  57. package/local-maven-repo/BareExpo/expo.modules.image/2.4.0-canary-20250713-8f814f8/expo.modules.image-2.4.0-canary-20250713-8f814f8.pom.sha512 +0 -1
package/CHANGELOG.md CHANGED
@@ -6,20 +6,31 @@
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))
9
10
  - Add support for `ImageRef` source in `generateBlurhashAsync` ([#37901](https://github.com/expo/expo/pull/37901) by [@Wenszel](https://github.com/Wenszel))
10
11
  - [Android] Add generateBlurhashAsync ([#37817](https://github.com/expo/expo/pull/37817) by [@Wenszel](https://github.com/Wenszel))
11
- - [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))
12
12
 
13
13
  ### 🐛 Bug fixes
14
14
 
15
- - [iOS] Speed up displaying local assets. ([#37795](https://github.com/expo/expo/pull/37795) by [@aleqsio](https://github.com/aleqsio))
16
15
  - [Android] Fix animation resuming by casting image to GifDrawable. ([#37363](https://github.com/expo/expo/pull/37363) by [@Wenszel](https://github.com/Wenszel))
17
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
18
 
19
19
  ### 💡 Others
20
20
 
21
21
  ### 📚 3rd party library updates
22
22
 
23
+ ## 2.4.0 - 2025-07-17
24
+
25
+ ### 🎉 New features
26
+
27
+ - [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))
28
+
29
+ ### 🐛 Bug fixes
30
+
31
+ - [iOS] Speed up displaying local assets. ([#37795](https://github.com/expo/expo/pull/37795) by [@aleqsio](https://github.com/aleqsio))
32
+ - [iOS] Fix some operation were incorrectly cancelled. ([#37987](https://github.com/expo/expo/pull/37987) by [@lukmccall](https://github.com/lukmccall))
33
+
23
34
  ## 2.3.2 - 2025-07-01
24
35
 
25
36
  ### 🐛 Bug fixes
@@ -8,7 +8,7 @@ android {
8
8
  namespace "expo.modules.image"
9
9
  defaultConfig {
10
10
  versionCode 1
11
- versionName "2.4.0-canary-20250713-8f814f8"
11
+ versionName "2.5.0-canary-20250722-599a28f"
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,7 @@ 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
8
9
  import androidx.core.graphics.drawable.toBitmap
9
10
  import androidx.core.graphics.drawable.toBitmapOrNull
10
11
  import androidx.core.view.doOnDetach
@@ -30,6 +31,7 @@ import expo.modules.image.records.DecodedSource
30
31
  import expo.modules.image.records.ImageLoadOptions
31
32
  import expo.modules.image.records.ImageTransition
32
33
  import expo.modules.image.records.SourceMap
34
+ import expo.modules.image.thumbhash.ThumbhashEncoder
33
35
  import expo.modules.kotlin.Promise
34
36
  import expo.modules.kotlin.apifeatures.EitherType
35
37
  import expo.modules.kotlin.exception.Exceptions
@@ -118,7 +120,10 @@ class ExpoImageModule : Module() {
118
120
  ImageLoadTask(appContext, source, options ?: ImageLoadOptions()).load()
119
121
  }
120
122
 
121
- AsyncFunction("generateBlurhashAsync") Coroutine { source: Either<URL, Image>, numberOfComponents: Pair<Int, Int> ->
123
+ suspend fun generatePlaceholder(
124
+ source: Either<URL, Image>,
125
+ encoder: (Bitmap) -> String
126
+ ): String {
122
127
  val image = source.let {
123
128
  if (it.`is`(Image::class)) {
124
129
  it.get(Image::class)
@@ -126,10 +131,24 @@ class ExpoImageModule : Module() {
126
131
  ImageLoadTask(appContext, SourceMap(uri = it.get(URL::class).toString()), ImageLoadOptions()).load()
127
132
  }
128
133
  }
129
- val blurHash = withContext(Dispatchers.Default) {
130
- BlurhashEncoder.encode(image.ref.toBitmap(), numberOfComponents)
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
+ )
131
151
  }
132
- blurHash
133
152
  }
134
153
 
135
154
  Class(Image::class) {
@@ -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
  }
@@ -0,0 +1,184 @@
1
+ package expo.modules.image.thumbhash
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Color
5
+
6
+ // ThumbHash Java implementation (converted to kotlin) thanks to @evanw https://github.com/evanw/thumbhash
7
+ object ThumbhashEncoder {
8
+ /**
9
+ * Encodes an RGBA image to a ThumbHash. RGB should not be premultiplied by A.
10
+ * @param bitmap The bitmap to generate the ThumbHash from.
11
+ * @return The ThumbHash as a byte array.
12
+ */
13
+ fun encode(bitmap: Bitmap): ByteArray {
14
+ // Encoding an image larger than 100x100 is slow with no benefit
15
+ val resizedBitmap = resizeKeepingAspectRatio(bitmap, 100)
16
+ val w = resizedBitmap.width
17
+ val h = resizedBitmap.height
18
+
19
+ val pixels = IntArray(w * h)
20
+ resizedBitmap.getPixels(pixels, 0, w, 0, 0, w, h)
21
+
22
+ var avg_r = 0f
23
+ var avg_g = 0f
24
+ var avg_b = 0f
25
+ var avg_a = 0f
26
+ var i = 0
27
+ while (i < w * h) {
28
+ val alpha = Color.alpha(pixels[i]) / 255.0f
29
+ avg_r += alpha / 255.0f * Color.red(pixels[i])
30
+ avg_g += alpha / 255.0f * Color.green(pixels[i])
31
+ avg_b += alpha / 255.0f * Color.blue(pixels[i])
32
+ avg_a += alpha
33
+ i++
34
+ }
35
+
36
+ if (avg_a > 0) {
37
+ avg_r /= avg_a
38
+ avg_g /= avg_a
39
+ avg_b /= avg_a
40
+ }
41
+ val hasAlpha = avg_a < w * h
42
+ val l_limit = if (hasAlpha) 5 else 7 // Use fewer luminance bits if there's alpha
43
+ val lx = Math.max(1, Math.round((l_limit * w).toFloat() / Math.max(w, h).toFloat()))
44
+ val ly = Math.max(1, Math.round((l_limit * h).toFloat() / Math.max(w, h).toFloat()))
45
+ val l = FloatArray(w * h) // luminance
46
+ val p = FloatArray(w * h) // yellow - blue
47
+ val q = FloatArray(w * h) // red - green
48
+ val a = FloatArray(w * h) // alpha
49
+
50
+ // Convert the image from RGBA to LPQA (composite atop the average color)
51
+ i = 0
52
+ while (i < w * h) {
53
+ val alpha = (Color.alpha(pixels[i]) and 255) / 255.0f
54
+ val r = avg_r * (1.0f - alpha) + alpha / 255.0f * Color.red(pixels[i])
55
+ val g = avg_g * (1.0f - alpha) + alpha / 255.0f * Color.green(pixels[i])
56
+ val b = avg_b * (1.0f - alpha) + alpha / 255.0f * Color.blue(pixels[i])
57
+ l[i] = (r + g + b) / 3.0f
58
+ p[i] = (r + g) / 2.0f - b
59
+ q[i] = r - g
60
+ a[i] = alpha
61
+ i++
62
+ }
63
+
64
+ // Encode using the DCT into DC (constant) and normalized AC (varying) terms
65
+ val l_channel = Channel(Math.max(3, lx), Math.max(3, ly)).encode(w, h, l)
66
+ val p_channel = Channel(3, 3).encode(w, h, p)
67
+ val q_channel = Channel(3, 3).encode(w, h, q)
68
+ val a_channel = if (hasAlpha) Channel(5, 5).encode(w, h, a) else null
69
+
70
+ // Write the constants
71
+ val isLandscape = w > h
72
+ val header24 = (
73
+ Math.round(63.0f * l_channel.dc)
74
+ or (Math.round(31.5f + 31.5f * p_channel.dc) shl 6)
75
+ or (Math.round(31.5f + 31.5f * q_channel.dc) shl 12)
76
+ or (Math.round(31.0f * l_channel.scale) shl 18)
77
+ or if (hasAlpha) 1 shl 23 else 0
78
+ )
79
+ val header16 = (
80
+ (if (isLandscape) ly else lx)
81
+ or (Math.round(63.0f * p_channel.scale) shl 3)
82
+ or (Math.round(63.0f * q_channel.scale) shl 9)
83
+ or if (isLandscape) 1 shl 15 else 0
84
+ )
85
+ val ac_start = if (hasAlpha) 6 else 5
86
+ val ac_count = (
87
+ l_channel.ac.size + p_channel.ac.size + q_channel.ac.size +
88
+ if (hasAlpha) a_channel!!.ac.size else 0
89
+ )
90
+ val hash = ByteArray(ac_start + (ac_count + 1) / 2)
91
+ hash[0] = header24.toByte()
92
+ hash[1] = (header24 shr 8).toByte()
93
+ hash[2] = (header24 shr 16).toByte()
94
+ hash[3] = header16.toByte()
95
+ hash[4] = (header16 shr 8).toByte()
96
+ if (hasAlpha) {
97
+ hash[5] = (
98
+ Math.round(15.0f * a_channel!!.dc)
99
+ or (Math.round(15.0f * a_channel.scale) shl 4)
100
+ ).toByte()
101
+ }
102
+
103
+ // Write the varying factors
104
+ var ac_index = 0
105
+ ac_index = l_channel.writeTo(hash, ac_start, ac_index)
106
+ ac_index = p_channel.writeTo(hash, ac_start, ac_index)
107
+ ac_index = q_channel.writeTo(hash, ac_start, ac_index)
108
+ if (hasAlpha) a_channel!!.writeTo(hash, ac_start, ac_index)
109
+
110
+ return hash
111
+ }
112
+
113
+ private fun resizeKeepingAspectRatio(bitmap: Bitmap, maxSize: Int): Bitmap {
114
+ val width = bitmap.width
115
+ val height = bitmap.height
116
+ val ratio = width.toFloat() / height.toFloat()
117
+
118
+ val newWidth: Int
119
+ val newHeight: Int
120
+
121
+ if (ratio > 1) {
122
+ newWidth = maxSize
123
+ newHeight = (maxSize / ratio).toInt()
124
+ } else {
125
+ newHeight = maxSize
126
+ newWidth = (maxSize * ratio).toInt()
127
+ }
128
+
129
+ return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
130
+ }
131
+
132
+ private class Channel(var nx: Int, var ny: Int) {
133
+ var dc = 0f
134
+ var ac: FloatArray
135
+ var scale = 0f
136
+
137
+ init {
138
+ var n = 0
139
+ for (cy in 0 until ny) {
140
+ var cx = if (cy > 0) 0 else 1
141
+ while (cx * ny < nx * (ny - cy)) {
142
+ n++
143
+ cx++
144
+ }
145
+ }
146
+ ac = FloatArray(n)
147
+ }
148
+
149
+ fun encode(w: Int, h: Int, channel: FloatArray): Channel {
150
+ var n = 0
151
+ val fx = FloatArray(w)
152
+ for (cy in 0 until ny) {
153
+ var cx = 0
154
+ while (cx * ny < nx * (ny - cy)) {
155
+ var f = 0f
156
+ for (x in 0 until w) fx[x] = Math.cos(Math.PI / w * cx * (x + 0.5f)).toFloat()
157
+ for (y in 0 until h) {
158
+ val fy = Math.cos(Math.PI / h * cy * (y + 0.5f)).toFloat()
159
+ for (x in 0 until w) f += channel[x + y * w] * fx[x] * fy
160
+ }
161
+ f /= (w * h).toFloat()
162
+ if (cx > 0 || cy > 0) {
163
+ ac[n++] = f
164
+ scale = Math.max(scale, Math.abs(f))
165
+ } else {
166
+ dc = f
167
+ }
168
+ cx++
169
+ }
170
+ }
171
+ if (scale > 0) for (i in ac.indices) ac[i] = 0.5f + 0.5f / scale * ac[i]
172
+ return this
173
+ }
174
+
175
+ fun writeTo(hash: ByteArray, start: Int, index: Int): Int {
176
+ var currentIndex = index
177
+ for (v in ac) {
178
+ hash[start + (currentIndex shr 1)] = (hash[start + (currentIndex shr 1)].toInt() or (Math.round(15.0f * v) shl (currentIndex and 1 shl 2))).toByte()
179
+ currentIndex++
180
+ }
181
+ return currentIndex
182
+ }
183
+ }
184
+ }
package/build/Image.d.ts CHANGED
@@ -77,6 +77,14 @@ export declare class Image extends React.PureComponent<ImageProps> {
77
77
  width: number;
78
78
  height: number;
79
79
  }): Promise<string | null>;
80
+ /**
81
+ * Asynchronously generates a [Thumbhash](https://evanw.github.io/thumbhash/) from an image.
82
+ * @param source - The image source, either a URL (string) or an ImageRef
83
+ * @platform android
84
+ * @platform ios
85
+ * @return A promise resolving to the thumbhash string.
86
+ */
87
+ static generateThumbhashAsync(source: string | ImageRef): Promise<string>;
80
88
  /**
81
89
  * Asynchronously starts playback of the view's image if it is animated.
82
90
  * @platform android
@@ -1 +1 @@
1
- {"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../src/Image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,SAAS,MAAM,aAAa,CAAC;AACpC,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,WAAW,EACZ,MAAM,eAAe,CAAC;AAQvB,qBAAa,KAAM,SAAQ,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC;IACxD,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACjD,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;gBAEnC,KAAK,EAAE,UAAU;IAO7B,gBAAgB,2BAMd;IAEF;;OAEG;IACH,MAAM,CAAC,KAAK,kBAAqB;IAEjC;;;;;;;;;;OAUG;WACU,QAAQ,CACnB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,WAAW,CAAC,EAAE,oBAAoB,CAAC,aAAa,CAAC,GAChD,OAAO,CAAC,OAAO,CAAC;IACnB;;;;;;;;;;OAUG;WACU,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBhG;;;;;;;OAOG;WACU,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjD;;;;;;;OAOG;WACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/C;;;;;;;;;OASG;WACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIxE;;;;;;;;OAQG;WACU,qBAAqB,CAChC,MAAM,EAAE,MAAM,GAAG,QAAQ,EACzB,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GACvE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIzB;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpC;;;;OAIG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1C;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC;;;;;;OAMG;WACU,SAAS,CACpB,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,EACrC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,QAAQ,CAAC;IAKpB,MAAM;CA6CP"}
1
+ {"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../src/Image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,SAAS,MAAM,aAAa,CAAC;AACpC,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,WAAW,EACZ,MAAM,eAAe,CAAC;AAQvB,qBAAa,KAAM,SAAQ,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC;IACxD,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACjD,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;gBAEnC,KAAK,EAAE,UAAU;IAO7B,gBAAgB,2BAMd;IAEF;;OAEG;IACH,MAAM,CAAC,KAAK,kBAAqB;IAEjC;;;;;;;;;;OAUG;WACU,QAAQ,CACnB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,WAAW,CAAC,EAAE,oBAAoB,CAAC,aAAa,CAAC,GAChD,OAAO,CAAC,OAAO,CAAC;IACnB;;;;;;;;;;OAUG;WACU,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoBhG;;;;;;;OAOG;WACU,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjD;;;;;;;OAOG;WACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/C;;;;;;;;;OASG;WACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIxE;;;;;;;;OAQG;WACU,qBAAqB,CAChC,MAAM,EAAE,MAAM,GAAG,QAAQ,EACzB,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GACvE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIzB;;;;;;OAMG;WACU,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAI/E;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpC;;;;OAIG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1C;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC;;;;;;OAMG;WACU,SAAS,CACpB,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,MAAM,EACrC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,QAAQ,CAAC;IAKpB,MAAM;CA6CP"}
@@ -8,7 +8,7 @@
8
8
  "publication": {
9
9
  "groupId": "BareExpo",
10
10
  "artifactId": "expo.modules.image",
11
- "version": "2.4.0-canary-20250713-8f814f8",
11
+ "version": "2.5.0-canary-20250722-599a28f",
12
12
  "repository": "local-maven-repo"
13
13
  }
14
14
  }
@@ -171,26 +171,20 @@ public final class ImageModule: Module {
171
171
  }
172
172
 
173
173
  AsyncFunction("generateBlurhashAsync") { (source: Either<Image, URL>, numberOfComponents: CGSize, promise: Promise) in
174
- let downloader = SDWebImageDownloader()
175
174
  let parsedNumberOfComponents = (width: Int(numberOfComponents.width), height: Int(numberOfComponents.height))
176
-
177
- if let image: Image = source.get() {
178
- if let blurhashString = blurhash(fromImage: image.ref, numberOfComponents: parsedNumberOfComponents) {
175
+ generatePlaceholder(source: source) { (image: UIImage) in
176
+ if let blurhashString = blurhash(fromImage: image, numberOfComponents: parsedNumberOfComponents) {
179
177
  promise.resolve(blurhashString)
180
178
  } else {
181
179
  promise.reject(BlurhashGenerationException())
182
180
  }
183
- } else if let url: URL = source.get() {
184
- downloader.downloadImage(with: url, progress: nil, completed: { image, _, _, _ in
185
- DispatchQueue.global().async {
186
- if let downloadedImage = image {
187
- let blurhashString = blurhash(fromImage: downloadedImage, numberOfComponents: parsedNumberOfComponents)
188
- promise.resolve(blurhashString)
189
- } else {
190
- promise.reject(BlurhashGenerationException())
191
- }
192
- }
193
- })
181
+ }
182
+ }
183
+
184
+ AsyncFunction("generateThumbhashAsync") { (source: Either<Image, URL>, promise: Promise) in
185
+ generatePlaceholder(source: source) { (image: UIImage) in
186
+ let blurhashString = thumbHash(fromImage: image)
187
+ promise.resolve(blurhashString.base64EncodedString())
194
188
  }
195
189
  }
196
190
 
@@ -237,6 +231,24 @@ public final class ImageModule: Module {
237
231
  }
238
232
  }
239
233
 
234
+ func generatePlaceholder(
235
+ source: Either<Image, URL>,
236
+ generator: @escaping (UIImage) -> Void,
237
+ ) {
238
+ if let image: Image = source.get() {
239
+ generator(image.ref)
240
+ } else if let url: URL = source.get() {
241
+ let downloader = SDWebImageDownloader()
242
+ downloader.downloadImage(with: url, progress: nil, completed: { image, _, _, _ in
243
+ DispatchQueue.global().async {
244
+ if let downloadedImage = image {
245
+ generator(downloadedImage)
246
+ }
247
+ }
248
+ })
249
+ }
250
+ }
251
+
240
252
  static func registerCoders() {
241
253
  SDImageCodersManager.shared.addCoder(WebPCoder.shared)
242
254
  SDImageCodersManager.shared.addCoder(SDImageAVIFCoder.shared)
@@ -37,8 +37,7 @@ struct ImageSource: Record {
37
37
  return isPhotoLibraryAssetUrl(uri)
38
38
  }
39
39
 
40
- var isCachingAllowed: Bool {
41
- // TODO: Don't cache other non-network requests (e.g. data URIs, local files)
40
+ var cacheOriginalImage: Bool {
42
41
  return !isPhotoLibraryAsset
43
42
  }
44
43
  }
@@ -183,17 +183,13 @@ func createSDWebImageContext(forSource source: ImageSource, cachePolicy: ImageCa
183
183
  // incorrectly rendered images for resize modes that don't scale (`center` and `repeat`).
184
184
  context[.imageScaleFactor] = source.scale
185
185
 
186
- // Set which cache can be used to query and store the downloaded image.
187
- // We want to store only original images (without transformations).
188
- context[.queryCacheType] = SDImageCacheType.none.rawValue
189
- context[.storeCacheType] = SDImageCacheType.none.rawValue
186
+ let sdCacheType = cachePolicy.toSdCacheType().rawValue
187
+ context[.queryCacheType] = sdCacheType
188
+ context[.storeCacheType] = sdCacheType
190
189
 
191
- if source.isCachingAllowed {
192
- let sdCacheType = cachePolicy.toSdCacheType().rawValue
190
+ if source.cacheOriginalImage {
193
191
  context[.originalQueryCacheType] = sdCacheType
194
192
  context[.originalStoreCacheType] = sdCacheType
195
- context[.queryCacheType] = sdCacheType
196
- context[.storeCacheType] = sdCacheType
197
193
  } else {
198
194
  context[.originalQueryCacheType] = SDImageCacheType.none.rawValue
199
195
  context[.originalStoreCacheType] = SDImageCacheType.none.rawValue
@@ -121,12 +121,10 @@ public final class ImageView: ExpoView {
121
121
  if window == nil {
122
122
  // Cancel pending requests when the view is unmounted.
123
123
  cancelPendingOperation()
124
- } else if !bounds.isEmpty {
125
- // Reload the image after mounting the view with non-empty bounds.
126
- reload()
127
- } else {
128
- loadPlaceholderIfNecessary()
124
+ return
129
125
  }
126
+
127
+ loadPlaceholderIfNecessary()
130
128
  }
131
129
 
132
130
  public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@@ -165,7 +163,7 @@ public final class ImageView: ExpoView {
165
163
 
166
164
  // It seems that `UIImageView` can't tint some vector graphics. If the `tintColor` prop is specified,
167
165
  // we tell the SVG coder to decode to a bitmap instead. This will become useless when we switch to SVGNative coder.
168
- let shouldEarlyResize = imageTintColor != nil || enforceEarlyResizing
166
+ let shouldEarlyResize = imageTintColor != nil || enforceEarlyResizing || source.isPhotoLibraryAsset
169
167
  if shouldEarlyResize {
170
168
  context[.imagePreserveAspectRatio] = true
171
169
  context[.imageThumbnailPixelSize] = CGSize(
@@ -113,7 +113,7 @@ private func requestAsset(
113
113
  if let scale = context?[ImageView.screenScaleKey] as? Double,
114
114
  let containerSize = context?[ImageView.frameSizeKey] as? CGSize,
115
115
  let contentFit = context?[ImageView.contentFitKey] as? ContentFit {
116
- let targetSize = idealSize(
116
+ targetSize = idealSize(
117
117
  contentPixelSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight),
118
118
  containerSize: containerSize,
119
119
  scale: scale,
@@ -0,0 +1 @@
1
+ 0e053061d59fe36acf72811a3302c44446d4eccb71922bac5f4939a045ada73c5cb25856618ba46fb9fabeb89769e7e236f266af08858f37c70e90c4e1ce4d9f
@@ -0,0 +1 @@
1
+ fd70527a9a22d6aad8ce719ad75bc6fdc108db86d4cd8c4c8fd73d489fd96c3337ba7642f0088bf00d5e8d57a1da6c73cb6f384ca9b20c0cb27c2750457168d8
@@ -3,7 +3,7 @@
3
3
  "component": {
4
4
  "group": "BareExpo",
5
5
  "module": "expo.modules.image",
6
- "version": "2.4.0-canary-20250713-8f814f8",
6
+ "version": "2.5.0-canary-20250722-599a28f",
7
7
  "attributes": {
8
8
  "org.gradle.status": "release"
9
9
  }
@@ -54,13 +54,13 @@
54
54
  ],
55
55
  "files": [
56
56
  {
57
- "name": "expo.modules.image-2.4.0-canary-20250713-8f814f8.aar",
58
- "url": "expo.modules.image-2.4.0-canary-20250713-8f814f8.aar",
59
- "size": 293614,
60
- "sha512": "5928b97432f974641d14f99976ddd03c1933731a6dc215222f3635e8fd3a93fc807eb2068ac61b5618ade31e881c73cba403dec7737c0ad5ea15b23e146c9b87",
61
- "sha256": "28459572fbb29787a5b5eaab8cbec8d65ebcb8b1bff80d4b41020b76188714dc",
62
- "sha1": "d892986ab6d024ecfd622bf341abc5e0852b67e6",
63
- "md5": "987b16fc46ba4dfb8d53af2daf99fc7a"
57
+ "name": "expo.modules.image-2.5.0-canary-20250722-599a28f.aar",
58
+ "url": "expo.modules.image-2.5.0-canary-20250722-599a28f.aar",
59
+ "size": 302365,
60
+ "sha512": "fd70527a9a22d6aad8ce719ad75bc6fdc108db86d4cd8c4c8fd73d489fd96c3337ba7642f0088bf00d5e8d57a1da6c73cb6f384ca9b20c0cb27c2750457168d8",
61
+ "sha256": "391955de6c74abb540684536936a36dd3164d7fd3c29f8207a63b60fc3b7e715",
62
+ "sha1": "aec6c7ff43a00c6b2fec2ad77c8e0371d947cd58",
63
+ "md5": "1dfd91d5d7c26b8b455f45c08e0bc657"
64
64
  }
65
65
  ]
66
66
  },
@@ -143,13 +143,13 @@
143
143
  ],
144
144
  "files": [
145
145
  {
146
- "name": "expo.modules.image-2.4.0-canary-20250713-8f814f8.aar",
147
- "url": "expo.modules.image-2.4.0-canary-20250713-8f814f8.aar",
148
- "size": 293614,
149
- "sha512": "5928b97432f974641d14f99976ddd03c1933731a6dc215222f3635e8fd3a93fc807eb2068ac61b5618ade31e881c73cba403dec7737c0ad5ea15b23e146c9b87",
150
- "sha256": "28459572fbb29787a5b5eaab8cbec8d65ebcb8b1bff80d4b41020b76188714dc",
151
- "sha1": "d892986ab6d024ecfd622bf341abc5e0852b67e6",
152
- "md5": "987b16fc46ba4dfb8d53af2daf99fc7a"
146
+ "name": "expo.modules.image-2.5.0-canary-20250722-599a28f.aar",
147
+ "url": "expo.modules.image-2.5.0-canary-20250722-599a28f.aar",
148
+ "size": 302365,
149
+ "sha512": "fd70527a9a22d6aad8ce719ad75bc6fdc108db86d4cd8c4c8fd73d489fd96c3337ba7642f0088bf00d5e8d57a1da6c73cb6f384ca9b20c0cb27c2750457168d8",
150
+ "sha256": "391955de6c74abb540684536936a36dd3164d7fd3c29f8207a63b60fc3b7e715",
151
+ "sha1": "aec6c7ff43a00c6b2fec2ad77c8e0371d947cd58",
152
+ "md5": "1dfd91d5d7c26b8b455f45c08e0bc657"
153
153
  }
154
154
  ]
155
155
  },
@@ -163,13 +163,13 @@
163
163
  },
164
164
  "files": [
165
165
  {
166
- "name": "expo.modules.image-2.4.0-canary-20250713-8f814f8-sources.jar",
167
- "url": "expo.modules.image-2.4.0-canary-20250713-8f814f8-sources.jar",
168
- "size": 66715,
169
- "sha512": "5088775ebe8dbdd3636ae44627d45f466d3b26af289ccd64cc7f59a1db50045ec4c246dfacdfa80fdedd21b09f837215dd2f0843c8915c10afe74400ec3e626c",
170
- "sha256": "047cf4256d9f1f9d37b874fe403d46abd838a59bbdf3d5b923d25b546242d99f",
171
- "sha1": "f59c1a61e799d72cf2da93e51bdb6bfc76a1fecb",
172
- "md5": "fb1a021a3c959caf9c844732221afe77"
166
+ "name": "expo.modules.image-2.5.0-canary-20250722-599a28f-sources.jar",
167
+ "url": "expo.modules.image-2.5.0-canary-20250722-599a28f-sources.jar",
168
+ "size": 67711,
169
+ "sha512": "0e053061d59fe36acf72811a3302c44446d4eccb71922bac5f4939a045ada73c5cb25856618ba46fb9fabeb89769e7e236f266af08858f37c70e90c4e1ce4d9f",
170
+ "sha256": "a71bd4c1dc06465f11ca7b28e36ffc84d8fc28f94212400a9eda74dae84f68bc",
171
+ "sha1": "af6e947ec09e1d0bf54ef2276c9aefcf03c30e80",
172
+ "md5": "549abe213eac6144464aca6be80419a2"
173
173
  }
174
174
  ]
175
175
  }
@@ -0,0 +1 @@
1
+ 3337b860b96648aee56787aab20b670936754360bc1ae9aabd31da34f3e5f75dc9cab32556d9388d90e09dacf1f154c899fd6f05224edf9b9003b7eae3da72e6
@@ -9,7 +9,7 @@
9
9
  <modelVersion>4.0.0</modelVersion>
10
10
  <groupId>BareExpo</groupId>
11
11
  <artifactId>expo.modules.image</artifactId>
12
- <version>2.4.0-canary-20250713-8f814f8</version>
12
+ <version>2.5.0-canary-20250722-599a28f</version>
13
13
  <packaging>aar</packaging>
14
14
  <name>expo.modules.image</name>
15
15
  <url>https://github.com/expo/expo</url>
@@ -0,0 +1 @@
1
+ c1cc974bc3a6d0f1bcb7162959aac7ade03053efdac5feac4f8bff55c247964e41b405bfc2ddd3b82ecabd7ecddcb8183ce02a2eaacdb56ba67f98a46a59d121
@@ -3,11 +3,11 @@
3
3
  <groupId>BareExpo</groupId>
4
4
  <artifactId>expo.modules.image</artifactId>
5
5
  <versioning>
6
- <latest>2.4.0-canary-20250713-8f814f8</latest>
7
- <release>2.4.0-canary-20250713-8f814f8</release>
6
+ <latest>2.5.0-canary-20250722-599a28f</latest>
7
+ <release>2.5.0-canary-20250722-599a28f</release>
8
8
  <versions>
9
- <version>2.4.0-canary-20250713-8f814f8</version>
9
+ <version>2.5.0-canary-20250722-599a28f</version>
10
10
  </versions>
11
- <lastUpdated>20250713133538</lastUpdated>
11
+ <lastUpdated>20250722134543</lastUpdated>
12
12
  </versioning>
13
13
  </metadata>
@@ -1 +1 @@
1
- d6635df55af7d2ba1eff1ad1cabd94cb
1
+ 245a829128982550118c3f85cef9732a
@@ -1 +1 @@
1
- 16df7592a0fa39f97c40e1947d02cfa8c5fb8c31
1
+ d2d5de7b7516ae918e2bb68e3760ff7a9d055292
@@ -1 +1 @@
1
- d67ddea608f829dca4e700dbe6b0625c8628f00983a9b7a19c7442dbff6a10c4
1
+ 50120f99cd14e99757d38e4980efedce96bccf56b12f3a3f61d4d70a44a92d3b
@@ -1 +1 @@
1
- a8514f3b91eb7d052579211711b676e5889e0611caf3e5316355bdbe6d108236854ae7a4a5d52c90bf9e1e4d1f160fb64b832c37ff28a945156366485b3dd0cf
1
+ 1a7423133d5254c2858ef6e97964c64bfa02a225ec014c45d7cb5e46a270688386012677643ef60f34e110e5a7146e875536a1ba610262bdd0c59b9abdf4cb90
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-image",
3
3
  "title": "Expo Image",
4
- "version": "2.4.0-canary-20250713-8f814f8",
4
+ "version": "2.5.0-canary-20250722-599a28f",
5
5
  "description": "A cross-platform, performant image component for React Native and Expo with Web support",
6
6
  "main": "src/index.ts",
7
7
  "types": "build/index.d.ts",
@@ -29,10 +29,10 @@
29
29
  "license": "MIT",
30
30
  "dependencies": {},
31
31
  "devDependencies": {
32
- "expo-module-scripts": "4.1.10-canary-20250713-8f814f8"
32
+ "expo-module-scripts": "4.1.10-canary-20250722-599a28f"
33
33
  },
34
34
  "peerDependencies": {
35
- "expo": "54.0.0-canary-20250713-8f814f8",
35
+ "expo": "54.0.0-canary-20250722-599a28f",
36
36
  "react": "*",
37
37
  "react-native": "*",
38
38
  "react-native-web": "*"
package/src/Image.tsx CHANGED
@@ -143,6 +143,17 @@ export class Image extends React.PureComponent<ImageProps> {
143
143
  return ImageModule.generateBlurhashAsync(source, numberOfComponents);
144
144
  }
145
145
 
146
+ /**
147
+ * Asynchronously generates a [Thumbhash](https://evanw.github.io/thumbhash/) from an image.
148
+ * @param source - The image source, either a URL (string) or an ImageRef
149
+ * @platform android
150
+ * @platform ios
151
+ * @return A promise resolving to the thumbhash string.
152
+ */
153
+ static async generateThumbhashAsync(source: string | ImageRef): Promise<string> {
154
+ return ImageModule.generateThumbhashAsync(source);
155
+ }
156
+
146
157
  /**
147
158
  * Asynchronously starts playback of the view's image if it is animated.
148
159
  * @platform android
@@ -1 +0,0 @@
1
- 5088775ebe8dbdd3636ae44627d45f466d3b26af289ccd64cc7f59a1db50045ec4c246dfacdfa80fdedd21b09f837215dd2f0843c8915c10afe74400ec3e626c
@@ -1 +0,0 @@
1
- 28459572fbb29787a5b5eaab8cbec8d65ebcb8b1bff80d4b41020b76188714dc
@@ -1 +0,0 @@
1
- 5928b97432f974641d14f99976ddd03c1933731a6dc215222f3635e8fd3a93fc807eb2068ac61b5618ade31e881c73cba403dec7737c0ad5ea15b23e146c9b87
@@ -1 +0,0 @@
1
- 0c24d72091a2260318e8f0634b73a786a68edcc3efa686ed3a9756740e2cf897073c54071530268629ab1f28eb461941a36303ed0ba4db5f6df5fba0cbf82fec
@@ -1 +0,0 @@
1
- 19e4200b968e5ea875ddfc5e6052016998164c079fe546219ac4c1694c32dad5
@@ -1 +0,0 @@
1
- fbbb81d82cacda91b017192e2a95b7d6aae669ec1e6609d211d7ac571a74556e58e81ae5e626ea2ced1ce982dc42d9e76c797135572bcb06d282ef3e3e628a0b