react-native-fast-image 10.1.1 → 10.3.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 (35) hide show
  1. package/README.md +107 -14
  2. package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
  3. package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
  4. package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
  5. package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
  6. package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
  7. package/dist/index.cjs.js +47 -9
  8. package/dist/index.cjs.js.flow +11 -0
  9. package/dist/index.cjs.web.js +67 -14
  10. package/dist/index.cjs.web.js.flow +11 -0
  11. package/dist/index.d.ts +77 -13
  12. package/dist/index.js +48 -10
  13. package/dist/index.js.flow +11 -0
  14. package/dist/index.web.js +68 -15
  15. package/dist/index.web.js.flow +11 -0
  16. package/dist/placeholders.d.ts +4 -0
  17. package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
  18. package/dist/thumbhash.cjs.js +241 -0
  19. package/dist/thumbhash.d.ts +12 -0
  20. package/dist/thumbhash.js +240 -0
  21. package/ios/FastImage/FFFBoxBlur.h +7 -0
  22. package/ios/FastImage/FFFBoxBlur.m +29 -0
  23. package/ios/FastImage/FFFThumbHash.h +24 -0
  24. package/ios/FastImage/FFFThumbHash.m +269 -0
  25. package/ios/FastImage/FFFastImageView.h +7 -1
  26. package/ios/FastImage/FFFastImageView.m +93 -46
  27. package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
  28. package/package.json +4 -2
  29. package/src/index.js.flow +11 -0
  30. package/src/index.tsx +166 -31
  31. package/src/index.web.tsx +166 -33
  32. package/src/placeholders.ts +45 -0
  33. package/src/specs/FastImageViewNativeComponent.ts +8 -2
  34. package/src/thumbhash.ts +363 -0
  35. package/thumbhash/package.json +8 -0
@@ -0,0 +1,45 @@
1
+ // Placeholders given as strings (`placeholder`, and a source's `blurDataUri`)
2
+ // are data uris, shared by the native (index.tsx) and web (index.web.tsx)
3
+ // versions, or for `placeholder`, the name of an image bundled with the app.
4
+ // They're decoded at once, so they show in the view's first frame; a url
5
+ // would load like an image (and on iOS and Android, not at all), so it's
6
+ // ignored, with a warning in development.
7
+ import type { SourcePlaceholder } from './index'
8
+ import { warnOnce } from './warnings'
9
+
10
+ // Metro's (and React Native's Jest preset's) development flag; other web
11
+ // bundlers may not define it.
12
+ declare const __DEV__: boolean | undefined
13
+
14
+ // Whether `placeholder` is the name of an image bundled with the app (an iOS
15
+ // asset catalog image, an Android drawable): no scheme, not a path.
16
+ export function isImageName(value: string): boolean {
17
+ return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith('/')
18
+ }
19
+
20
+ // Whether a source has a thumbhash or blurDataUri, or `placeholder` is one
21
+ // (for every size of the source) rather than an image.
22
+ export function hasSourcePlaceholder(
23
+ value: unknown,
24
+ ): value is SourcePlaceholder {
25
+ return (
26
+ !!value &&
27
+ typeof value === 'object' &&
28
+ ('thumbhash' in value || 'blurDataUri' in value)
29
+ )
30
+ }
31
+
32
+ // The value if it's a data uri, otherwise undefined (with a warning for
33
+ // another string).
34
+ export function dataUri(value: unknown): string | undefined {
35
+ if (typeof value !== 'string' || value === '') return undefined
36
+ if (/^data:/i.test(value)) return value
37
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
38
+ warnOnce(
39
+ 'placeholder-url',
40
+ `react-native-fast-image: \`placeholder\` and \`blurDataUri\` don't load urls: "${value.slice(0, 60)}" is ignored. Use a data uri (data:image/…), or for \`placeholder\`, a require()d image or the name of an image bundled with the app.`,
41
+ 'warn',
42
+ )
43
+ }
44
+ return undefined
45
+ }
@@ -45,8 +45,14 @@ export interface NativeProps extends ViewProps {
45
45
  source?: CodegenTypes.UnsafeMixed
46
46
  // Several sources (sizes of one image), picked from for the view's size.
47
47
  sources?: CodegenTypes.UnsafeMixed
48
- // A resolved asset source ({ uri, width, height, scale }).
49
- defaultSource?: CodegenTypes.UnsafeMixed
48
+ // The `placeholder` prop: a resolved asset source ({ uri, width, height,
49
+ // scale }) or a data uri ({ uri } on iOS; the uri on Android).
50
+ placeholder?: CodegenTypes.UnsafeMixed
51
+ // What shows in place of placeholder while a source loads: its ThumbHash,
52
+ // or its blurDataUri (blurred natively), by the source's index ('' for
53
+ // none; one for a single source). Apart from the sources, so a new one
54
+ // for the same image doesn't load it again.
55
+ sourcePlaceholders?: ReadonlyArray<string>
50
56
  // objectFit, resolved (fill, contain, cover, none or scale-down).
51
57
  objectFit?: CodegenTypes.WithDefault<string, 'cover'>
52
58
  // objectPosition, as CSS computes it: on each axis the image's left or top
@@ -0,0 +1,363 @@
1
+ // ThumbHashes (https://evanw.github.io/thumbhash/) on the web, as
2
+ // `react-native-fast-image/thumbhash`: importing it makes the web version
3
+ // show sources' thumbhashes (iOS and Android decode them natively), and
4
+ // thumbHashToBlurDataUri makes a source's `blurDataUri` from one, e.g. on a
5
+ // server, so a page shows it without any JavaScript. FastImage doesn't import
6
+ // it, so web bundles only have it if the app does.
7
+ //
8
+ // The data uri is a PNG of at most 8 × 8 pixels: a ThumbHash holds at most 7
9
+ // frequencies on its longer side, so more pixels add bytes, not detail. The
10
+ // web version blurs it as it scales it up, as it does every blurDataUri.
11
+ //
12
+ // thumbHashToRGBA and rgbaToDataURL are Evan Wallace's reference code
13
+ // (https://github.com/evanw/thumbhash, js/thumbhash.js), typed, with a check
14
+ // that the hash is long enough for what its header says it holds:
15
+ //
16
+ // Copyright (c) 2023 Evan Wallace
17
+ //
18
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ // of this software and associated documentation files (the "Software"), to
20
+ // deal in the Software without restriction, including without limitation the
21
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
22
+ // sell copies of the Software, and to permit persons to whom the Software is
23
+ // furnished to do so, subject to the following conditions:
24
+ //
25
+ // The above copyright notice and this permission notice shall be included in
26
+ // all copies or substantial portions of the Software.
27
+ //
28
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33
+ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
34
+ // IN THE SOFTWARE.
35
+
36
+ // Recently made data uris, by their hash, the least recently used dropped.
37
+ const cache = new Map<string, string | undefined>()
38
+ const CACHE_SIZE = 256
39
+
40
+ /**
41
+ * Makes a source's `blurDataUri` from a ThumbHash, e.g. on your server, so
42
+ * the page shows the preview without any JavaScript. Undefined for an invalid
43
+ * hash. Works in browsers, Node and React Native.
44
+ *
45
+ * @example
46
+ * ```js
47
+ * // e.g. on your server, so the page shows the preview without any JavaScript
48
+ * const source = { uri: photo.url, blurDataUri: thumbHashToBlurDataUri(photo.thumbhash) }
49
+ * ```
50
+ */
51
+ export function thumbHashToBlurDataUri(
52
+ thumbhash: string | undefined,
53
+ ): string | undefined {
54
+ if (!thumbhash) return undefined
55
+ if (cache.has(thumbhash)) {
56
+ const uri = cache.get(thumbhash)
57
+ cache.delete(thumbhash)
58
+ cache.set(thumbhash, uri)
59
+ return uri
60
+ }
61
+ const hash = base64Bytes(thumbhash)
62
+ const image = hash ? thumbHashToRGBA(hash) : undefined
63
+ const small = image ? shrink(image.w, image.h, image.rgba, 8) : undefined
64
+ const uri = small ? rgbaToDataURL(small.w, small.h, small.rgba) : undefined
65
+ if (cache.size >= CACHE_SIZE) {
66
+ cache.delete(cache.keys().next().value as string)
67
+ }
68
+ cache.set(thumbhash, uri)
69
+ return uri
70
+ }
71
+
72
+ // The web version's decoder, which index.web.tsx finds here without importing
73
+ // this module (the same key from any copy of the package).
74
+ ;(globalThis as Record<symbol, unknown>)[
75
+ Symbol.for('react-native-fast-image/thumbhash')
76
+ ] = thumbHashToBlurDataUri
77
+
78
+ // The pixels averaged down so the longer side is at most max.
79
+ function shrink(w: number, h: number, rgba: Uint8Array, max: number) {
80
+ const scale = Math.max(w, h) / max
81
+ if (scale <= 1) return { w, h, rgba }
82
+ const sw = Math.max(1, Math.round(w / scale))
83
+ const sh = Math.max(1, Math.round(h / scale))
84
+ const out = new Uint8Array(sw * sh * 4)
85
+ for (let y = 0; y < sh; y++) {
86
+ const y0 = Math.floor((y * h) / sh)
87
+ const y1 = Math.floor(((y + 1) * h) / sh)
88
+ for (let x = 0; x < sw; x++) {
89
+ const x0 = Math.floor((x * w) / sw)
90
+ const x1 = Math.floor(((x + 1) * w) / sw)
91
+ for (let c = 0; c < 4; c++) {
92
+ let sum = 0
93
+ for (let yy = y0; yy < y1; yy++)
94
+ for (let xx = x0; xx < x1; xx++)
95
+ sum += rgba[(yy * w + xx) * 4 + c]
96
+ out[(y * sw + x) * 4 + c] = Math.round(
97
+ sum / ((y1 - y0) * (x1 - x0)),
98
+ )
99
+ }
100
+ }
101
+ }
102
+ return { w: sw, h: sh, rgba: out }
103
+ }
104
+
105
+ // A ThumbHash's bytes: base64, standard or url-safe, with or without
106
+ // padding.
107
+ function base64Bytes(base64: string): Uint8Array | undefined {
108
+ const standard = base64.trim().replace(/-/g, '+').replace(/_/g, '/')
109
+ if (!/^[A-Za-z0-9+/]*=*$/.test(standard)) return undefined
110
+ try {
111
+ const binary = atob(
112
+ standard.replace(/=+$/, '') +
113
+ '='.repeat((4 - (standard.replace(/=+$/, '').length % 4)) % 4),
114
+ )
115
+ return Uint8Array.from(binary, (c) => c.charCodeAt(0))
116
+ } catch {
117
+ return undefined
118
+ }
119
+ }
120
+
121
+ // How many AC factors a channel of nx × ny has.
122
+ function count(nx: number, ny: number) {
123
+ let n = 0
124
+ for (let cy = 0; cy < ny; cy++)
125
+ for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) n++
126
+ return n
127
+ }
128
+
129
+ function thumbHashToRGBA(
130
+ hash: Uint8Array,
131
+ ): { w: number; h: number; rgba: Uint8Array } | undefined {
132
+ const { PI, min, max, cos, round } = Math
133
+ if (hash.length < 5) return undefined
134
+
135
+ // Read the constants
136
+ const header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16)
137
+ const header16 = hash[3] | (hash[4] << 8)
138
+ const l_dc = (header24 & 63) / 63
139
+ const p_dc = ((header24 >> 6) & 63) / 31.5 - 1
140
+ const q_dc = ((header24 >> 12) & 63) / 31.5 - 1
141
+ const l_scale = ((header24 >> 18) & 31) / 31
142
+ const hasAlpha = header24 >> 23
143
+ const p_scale = ((header16 >> 3) & 63) / 63
144
+ const q_scale = ((header16 >> 9) & 63) / 63
145
+ const isLandscape = header16 >> 15
146
+ const lx = max(3, isLandscape ? (hasAlpha ? 5 : 7) : header16 & 7)
147
+ const ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7)
148
+ const ac_start = hasAlpha ? 6 : 5
149
+ const ac_count =
150
+ count(lx, ly) + 2 * count(3, 3) + (hasAlpha ? count(5, 5) : 0)
151
+ if (hash.length < ac_start + Math.ceil(ac_count / 2)) return undefined
152
+ const a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1
153
+ const a_scale = (hash[5] >> 4) / 15
154
+
155
+ // Read the varying factors (boost saturation by 1.25x to compensate for quantization)
156
+ let ac_index = 0
157
+ const decodeChannel = (nx: number, ny: number, scale: number) => {
158
+ const ac: number[] = []
159
+ for (let cy = 0; cy < ny; cy++)
160
+ for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++)
161
+ ac.push(
162
+ (((hash[ac_start + (ac_index >> 1)] >>
163
+ ((ac_index++ & 1) << 2)) &
164
+ 15) /
165
+ 7.5 -
166
+ 1) *
167
+ scale,
168
+ )
169
+ return ac
170
+ }
171
+ const l_ac = decodeChannel(lx, ly, l_scale)
172
+ const p_ac = decodeChannel(3, 3, p_scale * 1.25)
173
+ const q_ac = decodeChannel(3, 3, q_scale * 1.25)
174
+ const a_ac = hasAlpha ? decodeChannel(5, 5, a_scale) : []
175
+
176
+ // Decode using the DCT into RGB
177
+ const ratio = thumbHashToApproximateAspectRatio(hash)
178
+ const w = max(1, round(ratio > 1 ? 32 : 32 * ratio))
179
+ const h = max(1, round(ratio > 1 ? 32 / ratio : 32))
180
+ const rgba = new Uint8Array(w * h * 4)
181
+ const fx: number[] = []
182
+ const fy: number[] = []
183
+ for (let y = 0, i = 0; y < h; y++) {
184
+ for (let x = 0; x < w; x++, i += 4) {
185
+ let l = l_dc,
186
+ p = p_dc,
187
+ q = q_dc,
188
+ a = a_dc
189
+
190
+ // Precompute the coefficients
191
+ for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++)
192
+ fx[cx] = cos((PI / w) * (x + 0.5) * cx)
193
+ for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++)
194
+ fy[cy] = cos((PI / h) * (y + 0.5) * cy)
195
+
196
+ // Decode L
197
+ for (let cy = 0, j = 0; cy < ly; cy++)
198
+ for (
199
+ let cx = cy ? 0 : 1, fy2 = fy[cy] * 2;
200
+ cx * ly < lx * (ly - cy);
201
+ cx++, j++
202
+ )
203
+ l += l_ac[j] * fx[cx] * fy2
204
+
205
+ // Decode P and Q
206
+ for (let cy = 0, j = 0; cy < 3; cy++) {
207
+ for (
208
+ let cx = cy ? 0 : 1, fy2 = fy[cy] * 2;
209
+ cx < 3 - cy;
210
+ cx++, j++
211
+ ) {
212
+ const f = fx[cx] * fy2
213
+ p += p_ac[j] * f
214
+ q += q_ac[j] * f
215
+ }
216
+ }
217
+
218
+ // Decode A
219
+ if (hasAlpha)
220
+ for (let cy = 0, j = 0; cy < 5; cy++)
221
+ for (
222
+ let cx = cy ? 0 : 1, fy2 = fy[cy] * 2;
223
+ cx < 5 - cy;
224
+ cx++, j++
225
+ )
226
+ a += a_ac[j] * fx[cx] * fy2
227
+
228
+ // Convert to RGB
229
+ const b = l - (2 / 3) * p
230
+ const r = (3 * l - b + q) / 2
231
+ const g = r - q
232
+ rgba[i] = max(0, 255 * min(1, r))
233
+ rgba[i + 1] = max(0, 255 * min(1, g))
234
+ rgba[i + 2] = max(0, 255 * min(1, b))
235
+ rgba[i + 3] = max(0, 255 * min(1, a))
236
+ }
237
+ }
238
+ return { w, h, rgba }
239
+ }
240
+
241
+ function thumbHashToApproximateAspectRatio(hash: Uint8Array) {
242
+ const header = hash[3]
243
+ const hasAlpha = hash[2] & 0x80
244
+ const isLandscape = hash[4] & 0x80
245
+ const lx = isLandscape ? (hasAlpha ? 5 : 7) : header & 7
246
+ const ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7
247
+ return ly ? lx / ly : 1
248
+ }
249
+
250
+ // A PNG (uncompressed) of the pixels, as a data uri.
251
+ function rgbaToDataURL(w: number, h: number, rgba: Uint8Array) {
252
+ const row = w * 4 + 1
253
+ const idat = 6 + h * (5 + row)
254
+ const bytes = [
255
+ 137,
256
+ 80,
257
+ 78,
258
+ 71,
259
+ 13,
260
+ 10,
261
+ 26,
262
+ 10,
263
+ 0,
264
+ 0,
265
+ 0,
266
+ 13,
267
+ 73,
268
+ 72,
269
+ 68,
270
+ 82,
271
+ 0,
272
+ 0,
273
+ w >> 8,
274
+ w & 255,
275
+ 0,
276
+ 0,
277
+ h >> 8,
278
+ h & 255,
279
+ 8,
280
+ 6,
281
+ 0,
282
+ 0,
283
+ 0,
284
+ 0,
285
+ 0,
286
+ 0,
287
+ 0,
288
+ idat >>> 24,
289
+ (idat >> 16) & 255,
290
+ (idat >> 8) & 255,
291
+ idat & 255,
292
+ 73,
293
+ 68,
294
+ 65,
295
+ 84,
296
+ 120,
297
+ 1,
298
+ ]
299
+ const table = [
300
+ 0, 498536548, 997073096, 651767980, 1994146192, 1802195444, 1303535960,
301
+ 1342533948, -306674912, -267414716, -690576408, -882789492, -1687895376,
302
+ -2032938284, -1609899400, -1111625188,
303
+ ]
304
+ let a = 1,
305
+ b = 0
306
+ for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) {
307
+ bytes.push(
308
+ y + 1 < h ? 0 : 1,
309
+ row & 255,
310
+ row >> 8,
311
+ ~row & 255,
312
+ (row >> 8) ^ 255,
313
+ 0,
314
+ )
315
+ for (b = (b + a) % 65521; i < end; i++) {
316
+ const u = rgba[i] & 255
317
+ bytes.push(u)
318
+ a = (a + u) % 65521
319
+ b = (b + a) % 65521
320
+ }
321
+ }
322
+ bytes.push(
323
+ b >> 8,
324
+ b & 255,
325
+ a >> 8,
326
+ a & 255,
327
+ 0,
328
+ 0,
329
+ 0,
330
+ 0,
331
+ 0,
332
+ 0,
333
+ 0,
334
+ 0,
335
+ 73,
336
+ 69,
337
+ 78,
338
+ 68,
339
+ 174,
340
+ 66,
341
+ 96,
342
+ 130,
343
+ )
344
+ for (let [start, end] of [
345
+ [12, 29],
346
+ [37, 41 + idat],
347
+ ]) {
348
+ let c = ~0
349
+ for (let i = start; i < end; i++) {
350
+ c ^= bytes[i]
351
+ c = (c >>> 4) ^ table[c & 15]
352
+ c = (c >>> 4) ^ table[c & 15]
353
+ }
354
+ c = ~c
355
+ bytes[end++] = c >>> 24
356
+ bytes[end++] = (c >> 16) & 255
357
+ bytes[end++] = (c >> 8) & 255
358
+ bytes[end++] = c & 255
359
+ }
360
+ let binary = ''
361
+ for (const byte of bytes) binary += String.fromCharCode(byte)
362
+ return 'data:image/png;base64,' + btoa(binary)
363
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "react-native-fast-image-thumbhash",
3
+ "private": true,
4
+ "main": "../dist/thumbhash.cjs.js",
5
+ "module": "../dist/thumbhash.js",
6
+ "react-native": "../src/thumbhash.ts",
7
+ "types": "../dist/thumbhash.d.ts"
8
+ }