react-native-fast-image 10.1.0 → 10.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 (35) hide show
  1. package/README.md +84 -10
  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 +9 -0
  9. package/dist/index.cjs.web.js +49 -6
  10. package/dist/index.cjs.web.js.flow +9 -0
  11. package/dist/index.d.ts +56 -13
  12. package/dist/index.js +48 -10
  13. package/dist/index.js.flow +9 -0
  14. package/dist/index.web.js +49 -6
  15. package/dist/index.web.js.flow +9 -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 +100 -46
  27. package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
  28. package/package.json +4 -2
  29. package/src/index.js.flow +9 -0
  30. package/src/index.tsx +142 -31
  31. package/src/index.web.tsx +119 -11
  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
package/src/index.web.tsx CHANGED
@@ -5,12 +5,13 @@
5
5
  //
6
6
  // The View (a div) has FastImage's style, ref and View props. The <img> fills
7
7
  // it inside its borders, with object-fit for objectFit (see Picture).
8
- // defaultSource is a second <img> under it until the image has loaded (the
8
+ // The placeholder is a second <img> under it until the image has loaded (the
9
9
  // browser draws the image over it as it arrives), and tintColor is an SVG
10
10
  // filter next to them.
11
11
  //
12
- // Works: source (uri, require(), or several sizes: see sizedSources),
13
- // defaultSource, objectFit, objectPosition, tintColor, blurRadius, style,
12
+ // Works: source (uri, require(), or several sizes: see sizedSources, and
13
+ // thumbhash and blurDataUri), placeholder (and defaultSource), objectFit,
14
+ // objectPosition, tintColor, blurRadius, style,
14
15
  // onLoadStart, onLoad, onError, onLoadEnd, and View props (testID,
15
16
  // accessibility, onLayout, pointerEvents). The native-only props (cache,
16
17
  // priority, headers, transition, downsample, loop, paused, imageRendering,
@@ -29,7 +30,8 @@ import { getAssetByID } from 'react-native-web/dist/modules/AssetRegistry'
29
30
  import { cacheControl, priority, resizeMode } from './constants'
30
31
  import { resolveObjectFit } from './objectFit'
31
32
  import { cssPosition, objectPositionOrCentered } from './objectPosition'
32
- import { warnIfChildren } from './warnings'
33
+ import { dataUri, hasSourcePlaceholder } from './placeholders'
34
+ import { warnIfChildren, warnOnce } from './warnings'
33
35
  import type {
34
36
  CachePathResult,
35
37
  CacheState,
@@ -39,6 +41,7 @@ import type {
39
41
  ObjectFit,
40
42
  PreloadResult,
41
43
  Source,
44
+ SourcePlaceholder,
42
45
  } from './index'
43
46
 
44
47
  export type * from './index'
@@ -71,6 +74,7 @@ declare const __DEV__: boolean | undefined
71
74
 
72
75
  function FastImageBase({
73
76
  source,
77
+ placeholder,
74
78
  defaultSource,
75
79
  tintColor,
76
80
  blurRadius,
@@ -121,13 +125,28 @@ function FastImageBase({
121
125
  // one doesn't stay while it loads; with several sizes, the <img> stays,
122
126
  // and the browser shows the previous image until the new one has loaded.
123
127
  const key = srcSet ?? src
124
- const defaultUri = resolveUri(defaultSource)
128
+ // The source's blurDataUri, or its ThumbHash made into one (with
129
+ // `react-native-fast-image/thumbhash`), shows blurred (see PreviewBlur)
130
+ // in place of `placeholder` (or `placeholder`'s, for a source without its
131
+ // own). With several sizes the browser picks one itself (from the srcset,
132
+ // as it loads), so they only show if all the sizes have the same one.
133
+ const preview = hasSourcePlaceholder(placeholder) ? placeholder : undefined
134
+ const blurUri =
135
+ resolveUri(dataUri(shared(source, 'blurDataUri', preview))) ??
136
+ thumbhashUri(shared(source, 'thumbhash', preview))
137
+ // A thumbhash or blurDataUri in `placeholder` isn't an image to show as it
138
+ // is: defaultSource still is.
139
+ const defaultUri =
140
+ blurUri ??
141
+ resolveUri(preview ? defaultSource : (placeholder ?? defaultSource))
125
142
  // Whether the image for `key` loaded, or failed.
126
143
  const [result, setResult] = useState<{ key: string; ok: boolean }>()
127
144
  const ok = result && result.key === key ? result.ok : undefined
128
145
  // The same on the server and in the browser. React's ids have characters
129
146
  // an SVG id can't (:r0: in React 18, «r0» in React 19).
130
- const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, '')}`
147
+ const svgId = useSvgId().replace(/[^\w-]/g, '')
148
+ const tintId = `fast-image-tint-${svgId}`
149
+ const blurId = `fast-image-blur-${svgId}`
131
150
  // objectFit (the prop, then style's).
132
151
  const fit = resolveObjectFit(objectFit, style)
133
152
  const position = cssPosition(objectPositionOrCentered(objectPosition))
@@ -138,7 +157,7 @@ function FastImageBase({
138
157
  ]
139
158
  .filter(Boolean)
140
159
  .join(' ') || undefined
141
- // defaultSource shows until the image has loaded, and if it fails.
160
+ // The placeholder shows until the image has loaded, and if it fails.
142
161
  const showDefault = !!defaultUri && ok !== true
143
162
 
144
163
  useClientLayoutEffect(() => {
@@ -160,9 +179,28 @@ function FastImageBase({
160
179
  <Picture
161
180
  key={`default ${defaultUri}`}
162
181
  src={defaultUri}
163
- fit={fit}
182
+ // A source's placeholder stands for an image whose size
183
+ // isn't known yet: as contain for scale-down and none,
184
+ // which show an image at its own size.
185
+ fit={
186
+ blurUri && (fit === 'scale-down' || fit === 'none')
187
+ ? 'contain'
188
+ : fit
189
+ }
164
190
  position={position}
165
- filter={filter}
191
+ // A source's preview gets its own blur and the tint, not
192
+ // blurRadius (it's blurred already), as on iOS and
193
+ // Android.
194
+ filter={
195
+ blurUri
196
+ ? [
197
+ `url(#${blurId})`,
198
+ tintColor ? `url(#${tintId})` : '',
199
+ ]
200
+ .filter(Boolean)
201
+ .join(' ')
202
+ : filter
203
+ }
166
204
  // The image has the label, if there is one.
167
205
  alt={key ? '' : (label ?? '')}
168
206
  />
@@ -176,7 +214,7 @@ function FastImageBase({
176
214
  fit={fit}
177
215
  position={position}
178
216
  filter={filter}
179
- // Shown as it loads, over defaultSource, and not if it
217
+ // Shown as it loads, over the placeholder, and not if it
180
218
  // failed (no broken image icon or alt text).
181
219
  hidden={ok === false}
182
220
  alt={label ?? ''}
@@ -195,11 +233,52 @@ function FastImageBase({
195
233
  />
196
234
  ) : null}
197
235
  {tintColor ? <TintFilter id={tintId} color={tintColor} /> : null}
236
+ {blurUri && showDefault ? <PreviewBlur id={blurId} /> : null}
198
237
  </View>
199
238
  )
200
239
  }
201
240
 
202
- // A source's (or defaultSource's) uri. Expo's web builds make a require()d
241
+ // A ThumbHash as a blurDataUri, with `react-native-fast-image/thumbhash`
242
+ // imported (see thumbhash.ts). Without it, ignored, with a warning in
243
+ // development: FastImage doesn't bundle the decoder on the web.
244
+ function thumbhashUri(hash: string | undefined): string | undefined {
245
+ if (!hash) return undefined
246
+ const decode = (globalThis as Record<symbol, unknown>)[
247
+ Symbol.for('react-native-fast-image/thumbhash')
248
+ ] as ((hash: string) => string | undefined) | undefined
249
+ if (decode) return decode(hash)
250
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
251
+ warnOnce(
252
+ 'thumbhash-web',
253
+ "react-native-fast-image: on the web, a `thumbhash` shows only with `import 'react-native-fast-image/thumbhash'` (it decodes them). Or make it a `blurDataUri` before it reaches the browser (e.g. on your server) with that module's thumbHashToBlurDataUri, which needs no JavaScript in the browser.",
254
+ 'warn',
255
+ )
256
+ }
257
+ return undefined
258
+ }
259
+
260
+ // The thumbhash or blurDataUri a source shows: its own, or `placeholder`'s
261
+ // (preview) if it has neither. With several sources, the one they all show.
262
+ function shared(
263
+ source: FastImageProps['source'],
264
+ key: 'thumbhash' | 'blurDataUri',
265
+ preview: SourcePlaceholder | undefined,
266
+ ): string | undefined {
267
+ const values = (Array.isArray(source) ? source : [source]).map((entry) => {
268
+ const own =
269
+ entry && typeof entry === 'object' ? (entry as Source) : undefined
270
+ // A blurDataUri that isn't a data uri is ignored (with a warning), as
271
+ // on iOS and Android.
272
+ return (own?.thumbhash || dataUri(own?.blurDataUri) ? own : preview)?.[
273
+ key
274
+ ]
275
+ })
276
+ return values.every((value) => value && value === values[0])
277
+ ? values[0]
278
+ : undefined
279
+ }
280
+
281
+ // A source's (or a placeholder's) uri. Expo's web builds make a require()d
203
282
  // image a { uri, width, height } object (or a string). Other setups can make
204
283
  // it a number in react-native-web's asset registry, resolved as its Image
205
284
  // does: the file for the scale closest to the screen's.
@@ -439,6 +518,35 @@ function TintFilter({ id, color }: { id: string; color: unknown }) {
439
518
  )
440
519
  }
441
520
 
521
+ // A source's blurDataUri, a few pixels, blurred as it's scaled up: by about
522
+ // half a pixel of an 8 px image, in units of its size, so the same filter
523
+ // works for any view, also rendered on a server (no layout needed). The blur
524
+ // fades the edges, which the alpha table restores (transparent areas stay
525
+ // transparent): Chrome ignores edgeMode, and forcing alpha to 1 would fill
526
+ // a transparent image's background.
527
+ function PreviewBlur({ id }: { id: string }) {
528
+ return (
529
+ <svg style={tintStyle}>
530
+ <defs>
531
+ <filter
532
+ id={id}
533
+ x="0"
534
+ y="0"
535
+ width="1"
536
+ height="1"
537
+ primitiveUnits="objectBoundingBox"
538
+ colorInterpolationFilters="sRGB"
539
+ >
540
+ <feGaussianBlur stdDeviation="0.06" />
541
+ <feComponentTransfer>
542
+ <feFuncA type="table" tableValues="0 1 1 1 1" />
543
+ </feComponentTransfer>
544
+ </filter>
545
+ </defs>
546
+ </svg>
547
+ )
548
+ }
549
+
442
550
  const tintStyle = {
443
551
  position: 'absolute',
444
552
  width: 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
+ }