expo-image 1.1.0 → 1.2.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 (80) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/android/build.gradle +1 -1
  4. package/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt +1 -1
  5. package/android/src/main/java/expo/modules/image/GlideModel.kt +14 -0
  6. package/android/src/main/java/expo/modules/image/records/SourceMap.kt +9 -0
  7. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashDecoder.kt +365 -0
  8. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashFetcher.kt +31 -0
  9. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModelLoader.kt +23 -0
  10. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModelLoaderFactory.kt +14 -0
  11. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModule.kt +17 -0
  12. package/build/ExpoImage.d.ts.map +1 -1
  13. package/build/ExpoImage.js +2 -2
  14. package/build/ExpoImage.js.map +1 -1
  15. package/build/ExpoImage.web.d.ts.map +1 -1
  16. package/build/ExpoImage.web.js +2 -2
  17. package/build/ExpoImage.web.js.map +1 -1
  18. package/build/Image.types.d.ts +17 -1
  19. package/build/Image.types.d.ts.map +1 -1
  20. package/build/Image.types.js.map +1 -1
  21. package/build/utils/AssetSourceResolver.web.d.ts +24 -0
  22. package/build/utils/AssetSourceResolver.web.d.ts.map +1 -0
  23. package/build/utils/AssetSourceResolver.web.js +67 -0
  24. package/build/utils/AssetSourceResolver.web.js.map +1 -0
  25. package/build/utils/resolveAssetSource.web.d.ts +8 -1
  26. package/build/utils/resolveAssetSource.web.d.ts.map +1 -1
  27. package/build/utils/resolveAssetSource.web.js +28 -2
  28. package/build/utils/resolveAssetSource.web.js.map +1 -1
  29. package/build/utils/resolveHashString.d.ts +14 -0
  30. package/build/utils/resolveHashString.d.ts.map +1 -0
  31. package/build/utils/resolveHashString.js +29 -0
  32. package/build/utils/resolveHashString.js.map +1 -0
  33. package/build/utils/resolveHashString.web.d.ts +20 -0
  34. package/build/utils/resolveHashString.web.d.ts.map +1 -0
  35. package/build/utils/resolveHashString.web.js +31 -0
  36. package/build/utils/resolveHashString.web.js.map +1 -0
  37. package/build/utils/resolveSources.d.ts +1 -0
  38. package/build/utils/resolveSources.d.ts.map +1 -1
  39. package/build/utils/resolveSources.js +15 -4
  40. package/build/utils/resolveSources.js.map +1 -1
  41. package/build/utils/thumbhash/thumbhash.d.ts +66 -0
  42. package/build/utils/thumbhash/thumbhash.d.ts.map +1 -0
  43. package/build/utils/thumbhash/thumbhash.js +338 -0
  44. package/build/utils/thumbhash/thumbhash.js.map +1 -0
  45. package/build/web/ImageWrapper.d.ts +3 -2
  46. package/build/web/ImageWrapper.d.ts.map +1 -1
  47. package/build/web/ImageWrapper.js +13 -7
  48. package/build/web/ImageWrapper.js.map +1 -1
  49. package/build/web/style.d.ts.map +1 -1
  50. package/build/web/style.js +5 -3
  51. package/build/web/style.js.map +1 -1
  52. package/ios/ImageModule.swift +1 -0
  53. package/ios/ImageSource.swift +4 -0
  54. package/ios/ImageView.swift +2 -2
  55. package/ios/Thumbhash.swift +550 -0
  56. package/ios/ThumbhashLoader.swift +52 -0
  57. package/package.json +2 -2
  58. package/src/ExpoImage.tsx +2 -1
  59. package/src/ExpoImage.web.tsx +5 -4
  60. package/src/Image.types.ts +19 -1
  61. package/src/utils/AssetSourceResolver.web.ts +88 -0
  62. package/src/utils/resolveAssetSource.web.ts +40 -0
  63. package/src/utils/resolveHashString.tsx +34 -0
  64. package/src/utils/resolveHashString.web.tsx +33 -0
  65. package/src/utils/resolveSources.tsx +15 -4
  66. package/src/utils/thumbhash/thumbhash.ts +387 -0
  67. package/src/web/ImageWrapper.tsx +24 -9
  68. package/src/web/style.tsx +5 -3
  69. package/android/src/main/java/expo/modules/image/enums/ImageResizeMode.kt +0 -20
  70. package/build/utils/resolveBlurhashString.d.ts +0 -3
  71. package/build/utils/resolveBlurhashString.d.ts.map +0 -1
  72. package/build/utils/resolveBlurhashString.js +0 -13
  73. package/build/utils/resolveBlurhashString.js.map +0 -1
  74. package/build/utils/resolveBlurhashString.web.d.ts +0 -3
  75. package/build/utils/resolveBlurhashString.web.d.ts.map +0 -1
  76. package/build/utils/resolveBlurhashString.web.js +0 -9
  77. package/build/utils/resolveBlurhashString.web.js.map +0 -1
  78. package/src/utils/resolveAssetSource.web.tsx +0 -4
  79. package/src/utils/resolveBlurhashString.tsx +0 -15
  80. package/src/utils/resolveBlurhashString.web.tsx +0 -10
package/CHANGELOG.md CHANGED
@@ -10,6 +10,20 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.2.0 — 2023-04-14
14
+
15
+ ### 🎉 New features
16
+
17
+ - [Web] Add support for `require()` assets. ([#21798](https://github.com/expo/expo/pull/21798) by [@aleqsio](https://github.com/aleqsio))
18
+ - Add `alt` prop as an alias to `accessibilityLabel`. ([#21884](https://github.com/expo/expo/pull/21884) by [@EvanBacon](https://github.com/EvanBacon))
19
+ - [Web] Add `accessibilityLabel` support on web. ([#21884](https://github.com/expo/expo/pull/21884) by [@EvanBacon](https://github.com/EvanBacon))
20
+ - Added `ThumbHash` support for Android, iOS and Web. ([#21952](https://github.com/expo/expo/pull/21952) by [@behenate](https://github.com/behenate))
21
+
22
+ ### 🐛 Bug fixes
23
+
24
+ - [Web] Prevent breaking in static rendering environments. ([#21883](https://github.com/expo/expo/pull/21883) by [@EvanBacon](https://github.com/EvanBacon))
25
+ - [Web] Fixed monorepo asset resolution in production for Metro web. ([#22094](https://github.com/expo/expo/pull/22094) by [@EvanBacon](https://github.com/EvanBacon))
26
+
13
27
  ## 1.1.0 — 2023-03-25
14
28
 
15
29
  ### 🎉 New features
package/README.md CHANGED
@@ -14,7 +14,7 @@ A cross-platform, performant image component for React Native and Expo.
14
14
  - Designed for speed
15
15
  - Support for many image formats (including animated ones)
16
16
  - Disk and memory caching
17
- - Supports [blurhash](https://blurha.sh), a compact representation of a placeholder for an image
17
+ - Supports [BlurHash](https://blurha.sh) and [ThumbHash](https://evanw.github.io/thumbhash/) - compact representations of a placeholder for an image
18
18
  - Transitioning between images when the source changes (no more flickering!)
19
19
  - Implements the CSS [`object-fit`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) and [`object-position`](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position) properties (see [`contentFit`](#contentfit) and [`contentPosition`](#contentposition) props)
20
20
  - Uses performant [`SDWebImage`](https://github.com/SDWebImage/SDWebImage) and [`Glide`](https://github.com/bumptech/glide) under the hood
@@ -49,7 +49,7 @@ android {
49
49
  minSdkVersion safeExtGet("minSdkVersion", 21)
50
50
  targetSdkVersion safeExtGet("targetSdkVersion", 33)
51
51
  versionCode 1
52
- versionName "1.1.0"
52
+ versionName "1.2.0"
53
53
  }
54
54
  lintOptions {
55
55
  abortOnError false
@@ -560,7 +560,7 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
560
560
  .apply {
561
561
  if (placeholder != null) {
562
562
  thumbnail(requestManager.load(placeholder.glideData))
563
- val newPlaceholderContentFit = if (bestPlaceholder.isBlurhash()) {
563
+ val newPlaceholderContentFit = if (bestPlaceholder.isBlurhash() || bestPlaceholder.isThumbhash()) {
564
564
  contentFit
565
565
  } else {
566
566
  placeholderContentFit
@@ -57,3 +57,17 @@ class GlideBlurhashModel(
57
57
  return result
58
58
  }
59
59
  }
60
+
61
+ class GlideThumbhashModel(
62
+ var uri: Uri
63
+ ) : GlideModel() {
64
+ override val glideData: GlideThumbhashModel = this
65
+
66
+ override fun equals(other: Any?): Boolean {
67
+ return (this === other) || other is GlideThumbhashModel && uri == other.uri
68
+ }
69
+
70
+ override fun hashCode(): Int {
71
+ return uri.hashCode()
72
+ }
73
+ }
@@ -10,6 +10,7 @@ import com.bumptech.glide.signature.ApplicationVersionSignature
10
10
  import expo.modules.image.GlideBlurhashModel
11
11
  import expo.modules.image.GlideModel
12
12
  import expo.modules.image.GlideRawModel
13
+ import expo.modules.image.GlideThumbhashModel
13
14
  import expo.modules.image.GlideUriModel
14
15
  import expo.modules.image.GlideUrlModel
15
16
  import expo.modules.image.ResourceIdHelper
@@ -39,6 +40,8 @@ data class SourceMap(
39
40
 
40
41
  fun isBlurhash() = parsedUri?.scheme?.startsWith("blurhash") ?: false
41
42
 
43
+ fun isThumbhash() = parsedUri?.scheme?.startsWith("thumbhash") ?: false
44
+
42
45
  internal fun createGlideModel(context: Context): GlideModel? {
43
46
  if (uri.isNullOrBlank()) {
44
47
  return null
@@ -60,6 +63,12 @@ data class SourceMap(
60
63
  )
61
64
  }
62
65
 
66
+ if (isThumbhash()) {
67
+ return GlideThumbhashModel(
68
+ parsedUri!!
69
+ )
70
+ }
71
+
63
72
  if (isResourceUri()) {
64
73
  return GlideUriModel(parsedUri!!)
65
74
  }
@@ -0,0 +1,365 @@
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 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) hash[5] = (
100
+ Math.round(15.0f * a_channel!!.dc)
101
+ or (Math.round(15.0f * a_channel.scale) shl 4)
102
+ ).toByte()
103
+
104
+ // Write the varying factors
105
+ var ac_index = 0
106
+ ac_index = l_channel.writeTo(hash, ac_start, ac_index)
107
+ ac_index = p_channel.writeTo(hash, ac_start, ac_index)
108
+ ac_index = q_channel.writeTo(hash, ac_start, ac_index)
109
+ if (hasAlpha) a_channel!!.writeTo(hash, ac_start, ac_index)
110
+ return hash
111
+ }
112
+
113
+ /**
114
+ * Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
115
+ *
116
+ * @param hash The bytes of the ThumbHash.
117
+ * @return The width, height, and pixels of the rendered placeholder image.
118
+ */
119
+ fun thumbHashToRGBA(hash: ByteArray): Image {
120
+ // Read the constants
121
+ val header24 = hash[0].toInt() and 255 or (hash[1].toInt() and 255 shl 8) or (hash[2].toInt() and 255 shl 16)
122
+ val header16 = hash[3].toInt() and 255 or (hash[4].toInt() and 255 shl 8)
123
+ val l_dc = (header24 and 63).toFloat() / 63.0f
124
+ val p_dc = (header24 shr 6 and 63).toFloat() / 31.5f - 1.0f
125
+ val q_dc = (header24 shr 12 and 63).toFloat() / 31.5f - 1.0f
126
+ val l_scale = (header24 shr 18 and 31).toFloat() / 31.0f
127
+ val hasAlpha = header24 shr 23 != 0
128
+ val p_scale = (header16 shr 3 and 63).toFloat() / 63.0f
129
+ val q_scale = (header16 shr 9 and 63).toFloat() / 63.0f
130
+ val isLandscape = header16 shr 15 != 0
131
+ val lx = Math.max(3, if (isLandscape) if (hasAlpha) 5 else 7 else header16 and 7)
132
+ val ly = Math.max(3, if (isLandscape) header16 and 7 else if (hasAlpha) 5 else 7)
133
+ val a_dc = if (hasAlpha) (hash[5].toInt() and 15).toFloat() / 15.0f else 1.0f
134
+ val a_scale = (hash[5].toInt() shr 4 and 15).toFloat() / 15.0f
135
+
136
+ // Read the varying factors (boost saturation by 1.25x to compensate for quantization)
137
+ val ac_start = if (hasAlpha) 6 else 5
138
+ var ac_index = 0
139
+ val l_channel = Channel(lx, ly)
140
+ val p_channel = Channel(3, 3)
141
+ val q_channel = Channel(3, 3)
142
+ var a_channel: Channel? = null
143
+ ac_index = l_channel.decode(hash, ac_start, ac_index, l_scale)
144
+ ac_index = p_channel.decode(hash, ac_start, ac_index, p_scale * 1.25f)
145
+ ac_index = q_channel.decode(hash, ac_start, ac_index, q_scale * 1.25f)
146
+ if (hasAlpha) {
147
+ a_channel = Channel(5, 5)
148
+ a_channel.decode(hash, ac_start, ac_index, a_scale)
149
+ }
150
+ val l_ac = l_channel.ac
151
+ val p_ac = p_channel.ac
152
+ val q_ac = q_channel.ac
153
+ val a_ac = if (hasAlpha) a_channel!!.ac else null
154
+
155
+ // Decode using the DCT into RGB
156
+ val ratio = thumbHashToApproximateAspectRatio(hash)
157
+ val w = Math.round(if (ratio > 1.0f) 32.0f else 32.0f * ratio)
158
+ val h = Math.round(if (ratio > 1.0f) 32.0f / ratio else 32.0f)
159
+ val rgba = ByteArray(w * h * 4)
160
+ val cx_stop = Math.max(lx, if (hasAlpha) 5 else 3)
161
+ val cy_stop = Math.max(ly, if (hasAlpha) 5 else 3)
162
+ val fx = FloatArray(cx_stop)
163
+ val fy = FloatArray(cy_stop)
164
+ var y = 0
165
+ var i = 0
166
+ while (y < h) {
167
+ var x = 0
168
+ while (x < w) {
169
+ var l = l_dc
170
+ var p = p_dc
171
+ var q = q_dc
172
+ var a = a_dc
173
+
174
+ // Precompute the coefficients
175
+ for (cx in 0 until cx_stop) fx[cx] = Math.cos(Math.PI / w * (x + 0.5f) * cx).toFloat()
176
+ for (cy in 0 until cy_stop) fy[cy] = Math.cos(Math.PI / h * (y + 0.5f) * cy).toFloat()
177
+
178
+ // Decode L
179
+ run {
180
+ var cy = 0
181
+ var j = 0
182
+ while (cy < ly) {
183
+ val fy2 = fy[cy] * 2.0f
184
+ var cx = if (cy > 0) 0 else 1
185
+ while (cx * ly < lx * (ly - cy)) {
186
+ l += l_ac[j] * fx[cx] * fy2
187
+ cx++
188
+ j++
189
+ }
190
+ cy++
191
+ }
192
+ }
193
+
194
+ // Decode P and Q
195
+ var cy = 0
196
+ var j = 0
197
+ while (cy < 3) {
198
+ val fy2 = fy[cy] * 2.0f
199
+ var cx = if (cy > 0) 0 else 1
200
+ while (cx < 3 - cy) {
201
+ val f = fx[cx] * fy2
202
+ p += p_ac[j] * f
203
+ q += q_ac[j] * f
204
+ cx++
205
+ j++
206
+ }
207
+ cy++
208
+ }
209
+
210
+ // Decode A
211
+ if (hasAlpha) {
212
+ var cy = 0
213
+ var j = 0
214
+ while (cy < 5) {
215
+ val fy2 = fy[cy] * 2.0f
216
+ var cx = if (cy > 0) 0 else 1
217
+ while (cx < 5 - cy) {
218
+ a += a_ac!![j] * fx[cx] * fy2
219
+ cx++
220
+ j++
221
+ }
222
+ cy++
223
+ }
224
+ }
225
+
226
+ // Convert to RGB
227
+ val b = l - 2.0f / 3.0f * p
228
+ val r = (3.0f * l - b + q) / 2.0f
229
+ val g = r - q
230
+ rgba[i] = Math.max(0, Math.round(255.0f * Math.min(1f, r))).toByte()
231
+ rgba[i + 1] = Math.max(0, Math.round(255.0f * Math.min(1f, g))).toByte()
232
+ rgba[i + 2] = Math.max(0, Math.round(255.0f * Math.min(1f, b))).toByte()
233
+ rgba[i + 3] = Math.max(0, Math.round(255.0f * Math.min(1f, a))).toByte()
234
+ x++
235
+ i += 4
236
+ }
237
+ y++
238
+ }
239
+ return Image(w, h, rgba)
240
+ }
241
+
242
+ /**
243
+ * Converts a ThumbHash into a Bitmap image
244
+ */
245
+ fun thumbHashToBitmap(hash: ByteArray): Bitmap {
246
+ val thumbhashImage = thumbHashToRGBA(hash)
247
+ // TODO: @behenate it should be possible to replace all of the code below with
248
+ // with BitmapFactory.decodeByteArray but it always returns null when using thumbhashImage.rgba
249
+ val imageArray = IntArray(thumbhashImage.width * thumbhashImage.height)
250
+ val thumbhashImageInt = thumbhashImage.rgba.map { it.toUByte().toInt() }
251
+ for (i in thumbhashImageInt.indices step 4) {
252
+ imageArray[i / 4] = Color.argb(
253
+ thumbhashImageInt[i + 3],
254
+ thumbhashImageInt[i],
255
+ thumbhashImageInt[i + 1],
256
+ thumbhashImageInt[i + 2]
257
+ )
258
+ }
259
+ return Bitmap.createBitmap(imageArray, thumbhashImage.width, thumbhashImage.height, Bitmap.Config.ARGB_8888)
260
+ }
261
+
262
+ /**
263
+ * Extracts the average color from a ThumbHash. RGB is not be premultiplied by A.
264
+ *
265
+ * @param hash The bytes of the ThumbHash.
266
+ * @return The RGBA values for the average color. Each value ranges from 0 to 1.
267
+ */
268
+ fun thumbHashToAverageRGBA(hash: ByteArray): RGBA {
269
+ val header = hash[0].toInt() and 255 or (hash[1].toInt() and 255 shl 8) or (hash[2].toInt() and 255 shl 16)
270
+ val l = (header and 63).toFloat() / 63.0f
271
+ val p = (header shr 6 and 63).toFloat() / 31.5f - 1.0f
272
+ val q = (header shr 12 and 63).toFloat() / 31.5f - 1.0f
273
+ val hasAlpha = header shr 23 != 0
274
+ val a = if (hasAlpha) (hash[5].toInt() and 15).toFloat() / 15.0f else 1.0f
275
+ val b = l - 2.0f / 3.0f * p
276
+ val r = (3.0f * l - b + q) / 2.0f
277
+ val g = r - q
278
+ return RGBA(
279
+ Math.max(0f, Math.min(1f, r)),
280
+ Math.max(0f, Math.min(1f, g)),
281
+ Math.max(0f, Math.min(1f, b)),
282
+ a
283
+ )
284
+ }
285
+
286
+ /**
287
+ * Extracts the approximate aspect ratio of the original image.
288
+ *
289
+ * @param hash The bytes of the ThumbHash.
290
+ * @return The approximate aspect ratio (i.e. width / height).
291
+ */
292
+ fun thumbHashToApproximateAspectRatio(hash: ByteArray): Float {
293
+ val header = hash[3]
294
+ val hasAlpha = hash[2].toInt() and 0x80 != 0
295
+ val isLandscape = hash[4].toInt() and 0x80 != 0
296
+ val lx = if (isLandscape) if (hasAlpha) 5 else 7 else header.toInt() and 7
297
+ val ly = if (isLandscape) header.toInt() and 7 else if (hasAlpha) 5 else 7
298
+ return lx.toFloat() / ly.toFloat()
299
+ }
300
+
301
+ class Image(var width: Int, var height: Int, var rgba: ByteArray)
302
+ class RGBA(var r: Float, var g: Float, var b: Float, var a: Float)
303
+ private class Channel internal constructor(var nx: Int, var ny: Int) {
304
+ var dc = 0f
305
+ var ac: FloatArray
306
+ var scale = 0f
307
+
308
+ init {
309
+ var n = 0
310
+ for (cy in 0 until ny) {
311
+ var cx = if (cy > 0) 0 else 1
312
+ while (cx * ny < nx * (ny - cy)) {
313
+ n++
314
+ cx++
315
+ }
316
+ }
317
+ ac = FloatArray(n)
318
+ }
319
+
320
+ fun encode(w: Int, h: Int, channel: FloatArray): Channel {
321
+ var n = 0
322
+ val fx = FloatArray(w)
323
+ for (cy in 0 until ny) {
324
+ var cx = 0
325
+ while (cx * ny < nx * (ny - cy)) {
326
+ var f = 0f
327
+ for (x in 0 until w) fx[x] = Math.cos(Math.PI / w * cx * (x + 0.5f)).toFloat()
328
+ for (y in 0 until h) {
329
+ val fy = Math.cos(Math.PI / h * cy * (y + 0.5f)).toFloat()
330
+ for (x in 0 until w) f += channel[x + y * w] * fx[x] * fy
331
+ }
332
+ f /= (w * h).toFloat()
333
+ if (cx > 0 || cy > 0) {
334
+ ac[n++] = f
335
+ scale = Math.max(scale, Math.abs(f))
336
+ } else {
337
+ dc = f
338
+ }
339
+ cx++
340
+ }
341
+ }
342
+ if (scale > 0) for (i in ac.indices) ac[i] = 0.5f + 0.5f / scale * ac[i]
343
+ return this
344
+ }
345
+
346
+ fun decode(hash: ByteArray, start: Int, index: Int, scale: Float): Int {
347
+ var index = index
348
+ for (i in ac.indices) {
349
+ val data = hash[start + (index shr 1)].toInt() shr (index and 1 shl 2)
350
+ ac[i] = ((data and 15).toFloat() / 7.5f - 1.0f) * scale
351
+ index++
352
+ }
353
+ return index
354
+ }
355
+
356
+ fun writeTo(hash: ByteArray, start: Int, index: Int): Int {
357
+ var index = index
358
+ for (v in ac) {
359
+ hash[start + (index shr 1)] = (hash[start + (index shr 1)].toInt() or (Math.round(15.0f * v) shl (index and 1 shl 2))).toByte()
360
+ index++
361
+ }
362
+ return index
363
+ }
364
+ }
365
+ }
@@ -0,0 +1,31 @@
1
+ package expo.modules.image.thumbhash
2
+
3
+ import android.graphics.Bitmap
4
+ import android.util.Base64
5
+ import com.bumptech.glide.Priority
6
+ import com.bumptech.glide.load.DataSource
7
+ import com.bumptech.glide.load.data.DataFetcher
8
+ import expo.modules.kotlin.exception.CodedException
9
+
10
+ class ThumbhashDecodingFailure(thumbhash: String?, cause: Exception?) : CodedException(
11
+ message = "Cannot decode provided thumbhash '$thumbhash' $cause"
12
+ )
13
+
14
+ class ThumbhashFetcher(
15
+ private val thumbhash: String?
16
+ ) : DataFetcher<Bitmap> {
17
+ override fun cleanup() = Unit
18
+ override fun cancel() = Unit
19
+ override fun getDataClass(): Class<Bitmap> = Bitmap::class.java
20
+ override fun getDataSource(): DataSource = DataSource.LOCAL
21
+
22
+ override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
23
+ try {
24
+ val decodedThumbhash = Base64.decode(thumbhash, Base64.DEFAULT)
25
+ val bitmap = ThumbhashDecoder.thumbHashToBitmap(decodedThumbhash)
26
+ callback.onDataReady(bitmap)
27
+ } catch (e: Exception) {
28
+ callback.onLoadFailed(ThumbhashDecodingFailure(thumbhash, e))
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,23 @@
1
+ package expo.modules.image.thumbhash
2
+
3
+ import android.graphics.Bitmap
4
+
5
+ import com.bumptech.glide.load.Options
6
+ import com.bumptech.glide.load.model.ModelLoader
7
+ import com.bumptech.glide.signature.ObjectKey
8
+
9
+ import expo.modules.image.GlideThumbhashModel
10
+
11
+ class ThumbhashModelLoader : ModelLoader<GlideThumbhashModel, Bitmap> {
12
+
13
+ override fun handles(model: GlideThumbhashModel): Boolean = true
14
+
15
+ override fun buildLoadData(model: GlideThumbhashModel, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
16
+ // Thumbhash might contain '/' characters
17
+ val thumbhash = model.uri.pathSegments.joinToString(separator = "/")
18
+ return ModelLoader.LoadData(
19
+ ObjectKey(model),
20
+ ThumbhashFetcher(thumbhash)
21
+ )
22
+ }
23
+ }
@@ -0,0 +1,14 @@
1
+ package expo.modules.image.thumbhash
2
+
3
+ import android.graphics.Bitmap
4
+ import com.bumptech.glide.load.model.ModelLoader
5
+ import com.bumptech.glide.load.model.ModelLoaderFactory
6
+ import com.bumptech.glide.load.model.MultiModelLoaderFactory
7
+ import expo.modules.image.GlideThumbhashModel
8
+
9
+ class ThumbhashModelLoaderFactory : ModelLoaderFactory<GlideThumbhashModel, Bitmap> {
10
+ override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideThumbhashModel, Bitmap> =
11
+ ThumbhashModelLoader()
12
+
13
+ override fun teardown() = Unit
14
+ }
@@ -0,0 +1,17 @@
1
+ package expo.modules.image.thumbhash
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import com.bumptech.glide.Glide
6
+ import com.bumptech.glide.Registry
7
+ import com.bumptech.glide.annotation.GlideModule
8
+ import com.bumptech.glide.module.LibraryGlideModule
9
+ import expo.modules.image.GlideThumbhashModel
10
+
11
+ @GlideModule
12
+ class ThumbhashModule : LibraryGlideModule() {
13
+ override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
14
+ super.registerComponents(context, glide, registry)
15
+ registry.prepend(GlideThumbhashModel::class.java, Bitmap::class.java, ThumbhashModelLoaderFactory())
16
+ }
17
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoImage.d.ts","sourceRoot":"","sources":["../src/ExpoImage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAsC,MAAM,cAAc,CAAC;AAExF,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,eAAe,CAAC;AAIvB,QAAA,MAAM,eAAe,KAAmC,CAAC;AAgBzD,cAAM,SAAU,SAAQ,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC;IAC3D,WAAW,aAET;IAEF,MAAM,UAAW,qBAAqB,kBAAkB,CAAC,UAGvD;IAEF,UAAU,UAAW,qBAAqB,sBAAsB,CAAC,UAE/D;IAEF,OAAO,UAAW,qBAAqB,mBAAmB,CAAC,UAGzD;IAEF,SAAS,aAEP;IAEF,MAAM;CA8DP;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ExpoImage.d.ts","sourceRoot":"","sources":["../src/ExpoImage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAsC,MAAM,cAAc,CAAC;AAExF,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACvB,MAAM,eAAe,CAAC;AAIvB,QAAA,MAAM,eAAe,KAAmC,CAAC;AAgBzD,cAAM,SAAU,SAAQ,KAAK,CAAC,aAAa,CAAC,gBAAgB,CAAC;IAC3D,WAAW,aAET;IAEF,MAAM,UAAW,qBAAqB,kBAAkB,CAAC,UAGvD;IAEF,UAAU,UAAW,qBAAqB,sBAAsB,CAAC,UAE/D;IAEF,OAAO,UAAW,qBAAqB,mBAAmB,CAAC,UAGzD;IAEF,SAAS,aAEP;IAEF,MAAM;CA+DP;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAC3B,eAAe,SAAS,CAAC"}
@@ -31,7 +31,7 @@ class ExpoImage extends React.PureComponent {
31
31
  this.props.onLoadEnd?.();
32
32
  };
33
33
  render() {
34
- const { style, ...props } = this.props;
34
+ const { style, accessibilityLabel, alt, ...props } = this.props;
35
35
  const resolvedStyle = StyleSheet.flatten(style);
36
36
  // Shadows behave different on iOS, Android & Web.
37
37
  // Android uses the `elevation` prop, whereas iOS
@@ -67,7 +67,7 @@ class ExpoImage extends React.PureComponent {
67
67
  const borderTopColor = processColor(resolvedStyle.borderTopColor);
68
68
  // @ts-ignore
69
69
  const borderBottomColor = processColor(resolvedStyle.borderBottomColor);
70
- return (React.createElement(NativeExpoImage, { ...props, ...resolvedStyle, style: resolvedStyle, onLoadStart: this.onLoadStart, onLoad: this.onLoad, onProgress: this.onProgress, onError: this.onError, tintColor: tintColor, borderColor: borderColor, borderLeftColor: borderLeftColor, borderRightColor: borderRightColor, borderTopColor: borderTopColor, borderBottomColor: borderBottomColor, borderStartColor: borderStartColor, borderEndColor: borderEndColor, backgroundColor: backgroundColor }));
70
+ return (React.createElement(NativeExpoImage, { ...props, ...resolvedStyle, accessibilityLabel: accessibilityLabel ?? alt, style: resolvedStyle, onLoadStart: this.onLoadStart, onLoad: this.onLoad, onProgress: this.onProgress, onError: this.onError, tintColor: tintColor, borderColor: borderColor, borderLeftColor: borderLeftColor, borderRightColor: borderRightColor, borderTopColor: borderTopColor, borderBottomColor: borderBottomColor, borderStartColor: borderStartColor, borderEndColor: borderEndColor, backgroundColor: backgroundColor }));
71
71
  }
72
72
  }
73
73
  export { ExpoImageModule };
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoImage.js","sourceRoot":"","sources":["../src/ExpoImage.tsx"],"names":[],"mappings":"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","sourcesContent":["import { requireNativeViewManager, requireNativeModule } from 'expo-modules-core';\nimport React from 'react';\nimport { NativeSyntheticEvent, StyleSheet, Platform, processColor } from 'react-native';\n\nimport {\n ImageErrorEventData,\n ImageLoadEventData,\n ImageNativeProps,\n ImageProgressEventData,\n} from './Image.types';\n\nconst NativeExpoImage = requireNativeViewManager('ExpoImage');\n\nconst ExpoImageModule = requireNativeModule('ExpoImage');\n\nfunction withDeprecatedNativeEvent<NativeEvent>(\n event: NativeSyntheticEvent<NativeEvent>\n): NativeEvent {\n Object.defineProperty(event.nativeEvent, 'nativeEvent', {\n get() {\n console.warn(\n '[expo-image]: Accessing event payload through \"nativeEvent\" is deprecated, it is now part of the event object itself'\n );\n return event.nativeEvent;\n },\n });\n return event.nativeEvent;\n}\n\nclass ExpoImage extends React.PureComponent<ImageNativeProps> {\n onLoadStart = () => {\n this.props.onLoadStart?.();\n };\n\n onLoad = (event: NativeSyntheticEvent<ImageLoadEventData>) => {\n this.props.onLoad?.(withDeprecatedNativeEvent(event));\n this.onLoadEnd();\n };\n\n onProgress = (event: NativeSyntheticEvent<ImageProgressEventData>) => {\n this.props.onProgress?.(withDeprecatedNativeEvent(event));\n };\n\n onError = (event: NativeSyntheticEvent<ImageErrorEventData>) => {\n this.props.onError?.(withDeprecatedNativeEvent(event));\n this.onLoadEnd();\n };\n\n onLoadEnd = () => {\n this.props.onLoadEnd?.();\n };\n\n render() {\n const { style, ...props } = this.props;\n const resolvedStyle = StyleSheet.flatten(style);\n\n // Shadows behave different on iOS, Android & Web.\n // Android uses the `elevation` prop, whereas iOS\n // and web use the regular `shadow...` props.\n if (Platform.OS === 'android') {\n delete resolvedStyle.shadowColor;\n delete resolvedStyle.shadowOffset;\n delete resolvedStyle.shadowOpacity;\n delete resolvedStyle.shadowRadius;\n } else {\n // @ts-expect-error\n delete resolvedStyle.elevation;\n }\n\n // @ts-ignore\n const backgroundColor = processColor(resolvedStyle.backgroundColor);\n // On Android, we have to set the `backgroundColor` directly on the correct component.\n // So we have to remove it from styles. Otherwise, the background color won't take into consideration the border-radius.\n if (Platform.OS === 'android') {\n delete resolvedStyle.backgroundColor;\n }\n\n const tintColor = processColor(props.tintColor || resolvedStyle.tintColor);\n\n const borderColor = processColor(resolvedStyle.borderColor);\n // @ts-ignore\n const borderStartColor = processColor(resolvedStyle.borderStartColor);\n // @ts-ignore\n const borderEndColor = processColor(resolvedStyle.borderEndColor);\n // @ts-ignore\n const borderLeftColor = processColor(resolvedStyle.borderLeftColor);\n // @ts-ignore\n const borderRightColor = processColor(resolvedStyle.borderRightColor);\n // @ts-ignore\n const borderTopColor = processColor(resolvedStyle.borderTopColor);\n // @ts-ignore\n const borderBottomColor = processColor(resolvedStyle.borderBottomColor);\n\n return (\n <NativeExpoImage\n {...props}\n {...resolvedStyle}\n style={resolvedStyle}\n onLoadStart={this.onLoadStart}\n onLoad={this.onLoad}\n onProgress={this.onProgress}\n onError={this.onError}\n tintColor={tintColor}\n borderColor={borderColor}\n borderLeftColor={borderLeftColor}\n borderRightColor={borderRightColor}\n borderTopColor={borderTopColor}\n borderBottomColor={borderBottomColor}\n borderStartColor={borderStartColor}\n borderEndColor={borderEndColor}\n backgroundColor={backgroundColor}\n />\n );\n }\n}\n\nexport { ExpoImageModule };\nexport default ExpoImage;\n"]}
1
+ {"version":3,"file":"ExpoImage.js","sourceRoot":"","sources":["../src/ExpoImage.tsx"],"names":[],"mappings":"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","sourcesContent":["import { requireNativeViewManager, requireNativeModule } from 'expo-modules-core';\nimport React from 'react';\nimport { NativeSyntheticEvent, StyleSheet, Platform, processColor } from 'react-native';\n\nimport {\n ImageErrorEventData,\n ImageLoadEventData,\n ImageNativeProps,\n ImageProgressEventData,\n} from './Image.types';\n\nconst NativeExpoImage = requireNativeViewManager('ExpoImage');\n\nconst ExpoImageModule = requireNativeModule('ExpoImage');\n\nfunction withDeprecatedNativeEvent<NativeEvent>(\n event: NativeSyntheticEvent<NativeEvent>\n): NativeEvent {\n Object.defineProperty(event.nativeEvent, 'nativeEvent', {\n get() {\n console.warn(\n '[expo-image]: Accessing event payload through \"nativeEvent\" is deprecated, it is now part of the event object itself'\n );\n return event.nativeEvent;\n },\n });\n return event.nativeEvent;\n}\n\nclass ExpoImage extends React.PureComponent<ImageNativeProps> {\n onLoadStart = () => {\n this.props.onLoadStart?.();\n };\n\n onLoad = (event: NativeSyntheticEvent<ImageLoadEventData>) => {\n this.props.onLoad?.(withDeprecatedNativeEvent(event));\n this.onLoadEnd();\n };\n\n onProgress = (event: NativeSyntheticEvent<ImageProgressEventData>) => {\n this.props.onProgress?.(withDeprecatedNativeEvent(event));\n };\n\n onError = (event: NativeSyntheticEvent<ImageErrorEventData>) => {\n this.props.onError?.(withDeprecatedNativeEvent(event));\n this.onLoadEnd();\n };\n\n onLoadEnd = () => {\n this.props.onLoadEnd?.();\n };\n\n render() {\n const { style, accessibilityLabel, alt, ...props } = this.props;\n const resolvedStyle = StyleSheet.flatten(style);\n\n // Shadows behave different on iOS, Android & Web.\n // Android uses the `elevation` prop, whereas iOS\n // and web use the regular `shadow...` props.\n if (Platform.OS === 'android') {\n delete resolvedStyle.shadowColor;\n delete resolvedStyle.shadowOffset;\n delete resolvedStyle.shadowOpacity;\n delete resolvedStyle.shadowRadius;\n } else {\n // @ts-expect-error\n delete resolvedStyle.elevation;\n }\n\n // @ts-ignore\n const backgroundColor = processColor(resolvedStyle.backgroundColor);\n // On Android, we have to set the `backgroundColor` directly on the correct component.\n // So we have to remove it from styles. Otherwise, the background color won't take into consideration the border-radius.\n if (Platform.OS === 'android') {\n delete resolvedStyle.backgroundColor;\n }\n\n const tintColor = processColor(props.tintColor || resolvedStyle.tintColor);\n\n const borderColor = processColor(resolvedStyle.borderColor);\n // @ts-ignore\n const borderStartColor = processColor(resolvedStyle.borderStartColor);\n // @ts-ignore\n const borderEndColor = processColor(resolvedStyle.borderEndColor);\n // @ts-ignore\n const borderLeftColor = processColor(resolvedStyle.borderLeftColor);\n // @ts-ignore\n const borderRightColor = processColor(resolvedStyle.borderRightColor);\n // @ts-ignore\n const borderTopColor = processColor(resolvedStyle.borderTopColor);\n // @ts-ignore\n const borderBottomColor = processColor(resolvedStyle.borderBottomColor);\n\n return (\n <NativeExpoImage\n {...props}\n {...resolvedStyle}\n accessibilityLabel={accessibilityLabel ?? alt}\n style={resolvedStyle}\n onLoadStart={this.onLoadStart}\n onLoad={this.onLoad}\n onProgress={this.onProgress}\n onError={this.onError}\n tintColor={tintColor}\n borderColor={borderColor}\n borderLeftColor={borderLeftColor}\n borderRightColor={borderRightColor}\n borderTopColor={borderTopColor}\n borderBottomColor={borderBottomColor}\n borderStartColor={borderStartColor}\n borderEndColor={borderEndColor}\n backgroundColor={backgroundColor}\n />\n );\n }\n}\n\nexport { ExpoImageModule };\nexport default ExpoImage;\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoImage.web.d.ts","sourceRoot":"","sources":["../src/ExpoImage.web.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAmC,MAAM,eAAe,CAAC;AAoClF,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,MAAM,EACN,WAAW,EACX,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,MAAM,EACN,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,GAAG,KAAK,EACT,EAAE,gBAAgB,eA0FlB"}
1
+ {"version":3,"file":"ExpoImage.web.d.ts","sourceRoot":"","sources":["../src/ExpoImage.web.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAmC,MAAM,eAAe,CAAC;AAoClF,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,MAAM,EACN,WAAW,EACX,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,MAAM,EACN,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,GAAG,KAAK,EACT,EAAE,gBAAgB,eA2FlB"}
@@ -46,7 +46,7 @@ export default function ExpoImage({ source, placeholder, contentFit, contentPosi
46
46
  ...style,
47
47
  }, className: className, events: {
48
48
  onTransitionEnd: [onAnimationFinished],
49
- }, contentPosition: { left: '50%', top: '50%' }, blurhashContentPosition: contentPosition, blurhashStyle: blurhashStyle })),
49
+ }, contentPosition: { left: '50%', top: '50%' }, hashPlaceholderContentPosition: contentPosition, hashPlaceholderStyle: blurhashStyle })),
50
50
  ]
51
51
  : null;
52
52
  const currentNodeAnimationKey = (recyclingKey
@@ -63,7 +63,7 @@ export default function ExpoImage({ source, placeholder, contentFit, contentPosi
63
63
  objectFit: selectedSource ? contentFit : imagePlaceholderContentFit,
64
64
  ...(blurRadius ? { filter: `blur(${blurRadius}px)` } : {}),
65
65
  ...style,
66
- }, className: className, priority: priority, contentPosition: selectedSource ? contentPosition : { top: '50%', left: '50%' }, blurhashContentPosition: contentPosition, blurhashStyle: blurhashStyle })),
66
+ }, className: className, priority: priority, contentPosition: selectedSource ? contentPosition : { top: '50%', left: '50%' }, hashPlaceholderContentPosition: contentPosition, hashPlaceholderStyle: blurhashStyle, accessibilityLabel: props.accessibilityLabel })),
67
67
  ];
68
68
  return (React.createElement("div", { ref: containerRef, className: "expo-image-container", style: {
69
69
  aspectRatio: String(aspectRatio),