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.
- package/README.md +107 -14
- package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
- package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
- package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
- package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
- package/dist/index.cjs.js +47 -9
- package/dist/index.cjs.js.flow +11 -0
- package/dist/index.cjs.web.js +67 -14
- package/dist/index.cjs.web.js.flow +11 -0
- package/dist/index.d.ts +77 -13
- package/dist/index.js +48 -10
- package/dist/index.js.flow +11 -0
- package/dist/index.web.js +68 -15
- package/dist/index.web.js.flow +11 -0
- package/dist/placeholders.d.ts +4 -0
- package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
- package/dist/thumbhash.cjs.js +241 -0
- package/dist/thumbhash.d.ts +12 -0
- package/dist/thumbhash.js +240 -0
- package/ios/FastImage/FFFBoxBlur.h +7 -0
- package/ios/FastImage/FFFBoxBlur.m +29 -0
- package/ios/FastImage/FFFThumbHash.h +24 -0
- package/ios/FastImage/FFFThumbHash.m +269 -0
- package/ios/FastImage/FFFastImageView.h +7 -1
- package/ios/FastImage/FFFastImageView.m +93 -46
- package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
- package/package.json +4 -2
- package/src/index.js.flow +11 -0
- package/src/index.tsx +166 -31
- package/src/index.web.tsx +166 -33
- package/src/placeholders.ts +45 -0
- package/src/specs/FastImageViewNativeComponent.ts +8 -2
- package/src/thumbhash.ts +363 -0
- package/thumbhash/package.json +8 -0
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ React Native's `Image` component handles image caching like browsers for the mos
|
|
|
26
26
|
- **Responsive images**: give several sizes, and the one that fits the view loads ([`source`](#source)).
|
|
27
27
|
- **Animated images** (GIF, WebP, APNG and AVIF) that can [loop](#loop) and [pause](#paused).
|
|
28
28
|
- **Fades** in and between images ([`transition`](#transition)).
|
|
29
|
+
- **Placeholders**: a blurred preview from a [ThumbHash](#sourcethumbhash) or a [tiny image](#sourceblurdatauri) while an image loads, or a bundled image ([`placeholder`](#placeholder)).
|
|
29
30
|
- [**SVG**](docs/formats.md#svg-images) and [**photo library**](#photo-library-images-ios-and-tvos) images.
|
|
30
31
|
- [**`FastImageBackground`**](#fastimagebackground) for content over an image.
|
|
31
32
|
- **Expo and web support**, with a [config plugin](#expo-config-plugin) for Expo.
|
|
@@ -70,7 +71,7 @@ npx expo install react-native-fast-image
|
|
|
70
71
|
|
|
71
72
|
It has native code, so it needs a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (e.g. `npx expo run:ios`), not Expo Go.
|
|
72
73
|
|
|
73
|
-
Needs React Native 0.83 or later with the New Architecture (Expo SDK 55 or later), iOS/tvOS 15.1 or later, and Android 7.0 (API 24) or later; on tvOS, it works with [React Native tvOS](https://github.com/react-native-tvos/react-native-tvos). Apps on the legacy architecture or older React Native can use FastImage 9 (`npm install react-native-fast-image@9`). Upgrading from FastImage 9: see [Upgrading from 9.x](docs/upgrading.md).
|
|
74
|
+
Needs React Native 0.83 or later with the New Architecture (Expo SDK 55 or later) and React 19, iOS/tvOS 15.1 or later, and Android 7.0 (API 24) or later; on tvOS, it works with [React Native tvOS](https://github.com/react-native-tvos/react-native-tvos). Apps on the legacy architecture or older React Native can use FastImage 9 (`npm install react-native-fast-image@9`). Upgrading from FastImage 9: see [Upgrading from 9.x](docs/upgrading.md).
|
|
74
75
|
|
|
75
76
|
To set the cache's size when the app starts, see [Native config](#native-config).
|
|
76
77
|
|
|
@@ -160,6 +161,8 @@ A hint for which images to start loading first when several are waiting: `'high'
|
|
|
160
161
|
- `'normal'`: the default.
|
|
161
162
|
- `'high'`: e.g. the image the screen is about.
|
|
162
163
|
|
|
164
|
+
On the web, it's the image's `fetchpriority`.
|
|
165
|
+
|
|
163
166
|
---
|
|
164
167
|
|
|
165
168
|
##### `source.cache`
|
|
@@ -211,6 +214,32 @@ Whether the image is kept on disk. With `false` it isn't kept in the disk cache
|
|
|
211
214
|
|
|
212
215
|
---
|
|
213
216
|
|
|
217
|
+
##### `source.thumbhash`
|
|
218
|
+
|
|
219
|
+
**Type:** `string`
|
|
220
|
+
|
|
221
|
+
A [ThumbHash](https://evanw.github.io/thumbhash/) of the image, shown blurred while the image loads, and if it fails to load. It's a short string you make when the image is uploaded (e.g. with the `thumbhash` package) and send with the image's url.
|
|
222
|
+
|
|
223
|
+
On the web, it needs `import 'react-native-fast-image/thumbhash'` (see [ThumbHashes on the web](#thumbhashes-on-the-web)).
|
|
224
|
+
|
|
225
|
+
```jsx
|
|
226
|
+
<FastImage source={{ uri: photo.url, thumbhash: photo.thumbhash }} />
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
##### `source.blurDataUri`
|
|
232
|
+
|
|
233
|
+
**Type:** `string`
|
|
234
|
+
|
|
235
|
+
A tiny version of the image (e.g. 8 to 20 px) as a data uri, shown blurred while the image loads, and if it fails to load. On the web it shows without any JavaScript, e.g. in a server-rendered page.
|
|
236
|
+
|
|
237
|
+
```jsx
|
|
238
|
+
<FastImage source={{ uri: photo.url, blurDataUri: photo.lqip }} />
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
214
243
|
##### `source.width`
|
|
215
244
|
|
|
216
245
|
**Type:** `number`
|
|
@@ -235,13 +264,32 @@ With several sources, the scale `width` and `height` are multiplied by.
|
|
|
235
264
|
|
|
236
265
|
---
|
|
237
266
|
|
|
267
|
+
#### `placeholder`
|
|
268
|
+
|
|
269
|
+
**Type:** `string | number | SourcePlaceholder`
|
|
270
|
+
|
|
271
|
+
A placeholder shown while the image loads, and if it fails to load:
|
|
272
|
+
|
|
273
|
+
- An image loaded with `require()`: `placeholder={require('./gray.png')}`.
|
|
274
|
+
- A data uri: `placeholder="data:image/png;base64,…"`.
|
|
275
|
+
- The name of an image bundled with the app (an iOS asset catalog image or an Android drawable): `placeholder="gray"`.
|
|
276
|
+
- A [ThumbHash](#sourcethumbhash), shown blurred: `placeholder={{ thumbhash }}`.
|
|
277
|
+
- A tiny image as a data uri, shown blurred: `placeholder={{ blurDataUri }}`.
|
|
278
|
+
|
|
279
|
+
A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one.
|
|
280
|
+
|
|
281
|
+
Notes:
|
|
282
|
+
|
|
283
|
+
- A url isn't loaded: it's ignored, with a warning in development.
|
|
284
|
+
- On Android, a `require()`d image doesn't show in debug builds, where the dev server serves it. Release builds show it.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
238
288
|
#### `defaultSource`
|
|
239
289
|
|
|
240
290
|
**Type:** `number`
|
|
241
291
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
On Android, `defaultSource` doesn't show in debug builds: there the dev server serves `require()`d images, and `defaultSource` is only loaded from the app's resources.
|
|
292
|
+
**Deprecated:** Use `placeholder`, which does the same.
|
|
245
293
|
|
|
246
294
|
---
|
|
247
295
|
|
|
@@ -272,7 +320,7 @@ Where the image is in the view, as CSS's `object-position` places it: where its
|
|
|
272
320
|
- A string in CSS's syntax: one or two keywords (`'top'`, `'left bottom'`), lengths in points (`'10px 20px'`, with `px`) or percentages (`'25% 75%'`), or offsets from an edge (`'right 10px bottom 20%'`). A percentage puts that point of the image at that point of the view: `'0% 0%'` is the top-left corner, `'100% 100%'` the bottom-right.
|
|
273
321
|
- An object with one of `top` and `bottom` and one of `left` and `right`, each in points (a number) or a percentage (`'25%'`), e.g. `{ right: 10, bottom: '20%' }`. A missing axis is centered.
|
|
274
322
|
|
|
275
|
-
Points are dp on Android. An invalid value is ignored (the image is centered), with a warning in development.
|
|
323
|
+
Points are dp on Android. An invalid value is ignored (the image is centered), with a warning in development. Placeholders are placed the same way.
|
|
276
324
|
|
|
277
325
|
```jsx
|
|
278
326
|
<FastImage source={{ uri }} objectFit="cover" objectPosition="top" />
|
|
@@ -284,7 +332,7 @@ Points are dp on Android. An invalid value is ignored (the image is centered), w
|
|
|
284
332
|
|
|
285
333
|
**Type:** `string | null`
|
|
286
334
|
|
|
287
|
-
For views that get reused for other content, such as rows in FlashList or recyclerlistview. Set it to something that identifies the content, e.g. the item's id. When it changes, the image is cleared right away (to `
|
|
335
|
+
For views that get reused for other content, such as rows in FlashList or recyclerlistview. Set it to something that identifies the content, e.g. the item's id. When it changes, the image is cleared right away (to `placeholder`, or blank) instead of staying until the new one has loaded. Unlike changing `key`, this keeps the view, which is what list recycling is for.
|
|
288
336
|
|
|
289
337
|
```jsx
|
|
290
338
|
<FastImage recyclingKey={item.id} source={{ uri: item.imageUrl }} />
|
|
@@ -319,6 +367,22 @@ How the image is filtered when it's drawn smaller or larger than its size (like
|
|
|
319
367
|
|
|
320
368
|
---
|
|
321
369
|
|
|
370
|
+
#### `loading`
|
|
371
|
+
|
|
372
|
+
**Type:** `'lazy' | 'eager'` · **Default:** `'lazy'` · Web only
|
|
373
|
+
|
|
374
|
+
Whether the image loads as it nears the screen (`'lazy'`) or at once (`'eager'`).
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
#### `sizes`
|
|
379
|
+
|
|
380
|
+
**Type:** `string` · **Default:** `'auto, 100vw'` · Web only
|
|
381
|
+
|
|
382
|
+
With several sources, the width the image is shown at, as the image's [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes), so the browser loads the size that fits. The default is the width the image is laid out at, which needs `loading="lazy"` (otherwise, or in browsers without it, the window's width).
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
322
386
|
#### `paused`
|
|
323
387
|
|
|
324
388
|
**Type:** `boolean`
|
|
@@ -368,7 +432,7 @@ Blurs the image by this radius, in points (the same radius looks about the same
|
|
|
368
432
|
|
|
369
433
|
It's for still images, or a radius that changes now and then (e.g. blurring a photo behind a sheet). Each change blurs the image again on the CPU, so don't animate it.
|
|
370
434
|
|
|
371
|
-
- Only the loaded image is blurred, not
|
|
435
|
+
- Only the loaded image is blurred, not a placeholder.
|
|
372
436
|
- An animated image (GIF, animated WebP, APNG or AVIF) shows its first frame, blurred, and doesn't animate.
|
|
373
437
|
- With `tintColor`, the blurred image is tinted.
|
|
374
438
|
- The image is blurred at about the size it's shown at, off the main thread. The cached file stays the original image, so `getCachePath` and other views of it aren't affected.
|
|
@@ -452,7 +516,7 @@ The image's style: View's style props (`borderRadius` clips the image, and borde
|
|
|
452
516
|
|
|
453
517
|
**Type:** `ColorValue`
|
|
454
518
|
|
|
455
|
-
If supplied, changes the color of all the non-transparent pixels to the given color.
|
|
519
|
+
If supplied, changes the color of all the non-transparent pixels to the given color, including the placeholder.
|
|
456
520
|
|
|
457
521
|
<!-- api:props end -->
|
|
458
522
|
|
|
@@ -811,12 +875,20 @@ An image's load priority: see [`source.priority`](#sourcepriority).
|
|
|
811
875
|
|
|
812
876
|
---
|
|
813
877
|
|
|
878
|
+
### `SourcePlaceholder`
|
|
879
|
+
|
|
880
|
+
**Type:** `Pick<Source, 'thumbhash' | 'blurDataUri'>`
|
|
881
|
+
|
|
882
|
+
A `thumbhash` or `blurDataUri` as `placeholder`.
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
814
886
|
### `Transition`
|
|
815
887
|
|
|
816
888
|
How the image fades in: see [`transition`](#transition).
|
|
817
889
|
|
|
818
890
|
- `duration?` (`number`): How long the fade takes, in milliseconds; 0 means no fade. Defaults to the default transition's (see `FastImage.setDefaults`) if it fades, otherwise the platform's usual length: 300 ms on Android, 250 ms on iOS.
|
|
819
|
-
- `betweenImages?` (`boolean`, default `false`): Also fades between images: a new `source` replacing an image that's showing cross-dissolves from it. Otherwise only an image that appears over nothing (or over
|
|
891
|
+
- `betweenImages?` (`boolean`, default `false`): Also fades between images: a new `source` replacing an image that's showing cross-dissolves from it. Otherwise only an image that appears over nothing (or over a placeholder) fades in, and a new source replaces the image showing at once, once it has loaded.
|
|
820
892
|
- `skipOnCacheHit?` (`'none' | 'memory' | 'all' | null`, default `'memory'`): Skips the fade for an image from a cache, so images already loaded show at once, e.g. in a list scrolled back up or a reused row.
|
|
821
893
|
|
|
822
894
|
- `'memory'`: skips it for images from the memory cache.
|
|
@@ -878,16 +950,37 @@ The app needs access to the photo library, which it has if it got the url from t
|
|
|
878
950
|
|
|
879
951
|
## Web
|
|
880
952
|
|
|
881
|
-
FastImage works on the web with [react-native-web](https://necolas.github.io/react-native-web/). Bundlers pick the web version through the package's `browser` field, or the `.web.js` files next to the native ones.
|
|
953
|
+
FastImage works on the web with [react-native-web](https://necolas.github.io/react-native-web/) and React 19 or later. Bundlers pick the web version through the package's `browser` field, or the `.web.js` files next to the native ones.
|
|
882
954
|
|
|
883
955
|
Support is minimal:
|
|
884
956
|
|
|
885
|
-
- Props that work: `source` (a `uri`, a `require()`d image, or several sizes), `defaultSource
|
|
886
|
-
- Ignored: `source.headers` (a browser can't send them for an image), `source.
|
|
887
|
-
- The image is an `<img>` element that fills the view inside its borders, with `objectFit` and `objectPosition` as its CSS `object-fit` and `object-position`. `tintColor` is an SVG filter and `blurRadius` a CSS `blur()`, and
|
|
888
|
-
-
|
|
957
|
+
- Props that work: `source` (a `uri`, a `require()`d image, or several sizes, with `thumbhash` and `blurDataUri`), `placeholder` (and `defaultSource`), `objectFit`, `objectPosition`, `tintColor`, `blurRadius`, `style`, `source.priority`, the web-only `loading` and `sizes`, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
|
|
958
|
+
- Ignored: `source.headers` (a browser can't send them for an image), `source.cache`, `source.cacheKey`, `source.memoryCache`, `source.diskCache`, `recyclingKey`, `loop`, `imageRendering`, `paused`, `transition`, `downsample` and `onProgress`.
|
|
959
|
+
- The image is an `<img>` element that fills the view inside its borders, with `objectFit` and `objectPosition` as its CSS `object-fit` and `object-position`. `tintColor` is an SVG filter and `blurRadius` a CSS `blur()`, and the placeholder is another `<img>`, under the image until it has loaded. A new `source` gets a new `<img>`, so the previous image doesn't stay while it loads (with several sizes, the browser shows it until the new one has loaded).
|
|
960
|
+
- Images load lazily unless [`loading`](#loading) is `'eager'`, and `source.priority` `'high'` and `'low'` are the `<img>`'s `fetchpriority`.
|
|
961
|
+
- Several sizes are shown with an `<img>` whose `srcset` lists them, with `sizes="auto, 100vw"` (or [`sizes`](#sizes)): the browser loads the one for the width the image is shown at, in device pixels (usually the smallest that's at least as wide). Browsers that don't support `sizes="auto"`, or an image with `loading="eager"`, use the viewport's width.
|
|
889
962
|
- `FastImage.preload` loads the images into the browser's cache and resolves with a result per source. `clearMemoryCache`, `clearDiskCache` and `configureCache` resolve without doing anything (the browser manages its cache), and `getCachePath` and `writeToCache` resolve with `{ ok: false, error: 'Not supported on the web' }`.
|
|
890
963
|
|
|
964
|
+
### ThumbHashes on the web
|
|
965
|
+
|
|
966
|
+
On the web, a [`thumbhash`](#sourcethumbhash) needs a decoder, which isn't included by default. Import it once:
|
|
967
|
+
|
|
968
|
+
```js
|
|
969
|
+
import 'react-native-fast-image/thumbhash'
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
Or, to show the preview without any JavaScript (e.g. in a server-rendered page), make the hash a [`blurDataUri`](#sourceblurdatauri) on your server:
|
|
973
|
+
|
|
974
|
+
```js
|
|
975
|
+
import { thumbHashToBlurDataUri } from 'react-native-fast-image/thumbhash'
|
|
976
|
+
|
|
977
|
+
const source = {
|
|
978
|
+
uri: photo.url,
|
|
979
|
+
thumbhash: photo.thumbhash, // iOS and Android
|
|
980
|
+
blurDataUri: thumbHashToBlurDataUri(photo.thumbhash), // the web
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
891
984
|
## Jest
|
|
892
985
|
|
|
893
986
|
With React Native's Jest preset, FastImage renders without any setup, as its native view (`FastImageView`, in snapshots). Its functions need the native module, which Jest doesn't have, so they throw: mock the ones a test calls, e.g.
|
|
@@ -136,8 +136,9 @@ final class FastImageBlur extends BitmapTransformation {
|
|
|
136
136
|
pool.put(copy);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Three box blurs, each across then down, with the edges extended.
|
|
140
|
-
|
|
139
|
+
// Three box blurs, each across then down, with the edges extended. Also
|
|
140
|
+
// for a source's blurDataUri (FastImageThumbHash).
|
|
141
|
+
static void blur(int[] pixels, int width, int height, int boxSize) {
|
|
141
142
|
int[] other = new int[width * height];
|
|
142
143
|
int radius = boxSize / 2;
|
|
143
144
|
for (int i = 0; i < 3; i++) {
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
package com.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.BitmapFactory;
|
|
5
|
+
import android.util.Base64;
|
|
6
|
+
import android.util.LruCache;
|
|
7
|
+
|
|
8
|
+
import androidx.annotation.Nullable;
|
|
9
|
+
|
|
10
|
+
import java.nio.IntBuffer;
|
|
11
|
+
import java.util.Locale;
|
|
12
|
+
|
|
13
|
+
// A source's `thumbhash` (https://evanw.github.io/thumbhash/) decoded into the
|
|
14
|
+
// placeholder it shows while its image loads: at most 32 × 32 pixels, which
|
|
15
|
+
// the view scales up smoothly. Decoding takes well under a millisecond, so it
|
|
16
|
+
// happens as the view picks its source; recent ones are cached by their hash,
|
|
17
|
+
// for list rows that show the same images again. Also a source's
|
|
18
|
+
// `blurDataUri`: a data uri's few pixels, scaled up to the same size and
|
|
19
|
+
// blurred (blurDataUri).
|
|
20
|
+
//
|
|
21
|
+
// thumbHashToRGBA is Evan Wallace's reference decoder
|
|
22
|
+
// (https://github.com/evanw/thumbhash, java/com/madebyevan/thumbhash), with a
|
|
23
|
+
// check that the hash is long enough for what its header says it holds:
|
|
24
|
+
//
|
|
25
|
+
// Copyright (c) 2023 Evan Wallace
|
|
26
|
+
//
|
|
27
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
// of this software and associated documentation files (the "Software"), to
|
|
29
|
+
// deal in the Software without restriction, including without limitation the
|
|
30
|
+
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
31
|
+
// sell copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
// furnished to do so, subject to the following conditions:
|
|
33
|
+
//
|
|
34
|
+
// The above copyright notice and this permission notice shall be included in
|
|
35
|
+
// all copies or substantial portions of the Software.
|
|
36
|
+
//
|
|
37
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
42
|
+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
43
|
+
// IN THE SOFTWARE.
|
|
44
|
+
final class FastImageThumbHash {
|
|
45
|
+
private FastImageThumbHash() {
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Recently decoded placeholders, by their hash or blurDataUri: at most
|
|
49
|
+
// 4 KB each.
|
|
50
|
+
private static final LruCache<String, Bitmap> cache = new LruCache<>(256);
|
|
51
|
+
|
|
52
|
+
// A source's placeholder: its blurDataUri's, or its ThumbHash's.
|
|
53
|
+
@Nullable
|
|
54
|
+
static Bitmap placeholder(@Nullable String value) {
|
|
55
|
+
return value != null && isDataUri(value) ? blurDataUri(value) : bitmap(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A source's blurDataUri, decoded, scaled so its longer side is 32 px
|
|
59
|
+
// (a ThumbHash's size), and blurred by about half of one of its pixels, so
|
|
60
|
+
// that a few pixels scale up smoothly instead of in steps.
|
|
61
|
+
@Nullable
|
|
62
|
+
static Bitmap blurDataUri(String uri) {
|
|
63
|
+
Bitmap cached = cache.get(uri);
|
|
64
|
+
if (cached != null) return cached;
|
|
65
|
+
Bitmap decoded = dataUri(uri);
|
|
66
|
+
if (decoded == null) return null;
|
|
67
|
+
int width = decoded.getWidth();
|
|
68
|
+
int height = decoded.getHeight();
|
|
69
|
+
float scale = 32f / Math.max(width, height);
|
|
70
|
+
int w = Math.max(1, Math.round(width * scale));
|
|
71
|
+
int h = Math.max(1, Math.round(height * scale));
|
|
72
|
+
// Blurred on its premultiplied pixels (as stored), so transparent
|
|
73
|
+
// pixels don't darken their neighbors.
|
|
74
|
+
Bitmap bitmap = Bitmap.createScaledBitmap(decoded, w, h, true).copy(Bitmap.Config.ARGB_8888, true);
|
|
75
|
+
if (bitmap == null) return null;
|
|
76
|
+
int[] pixels = new int[w * h];
|
|
77
|
+
IntBuffer buffer = IntBuffer.wrap(pixels);
|
|
78
|
+
bitmap.copyPixelsToBuffer(buffer);
|
|
79
|
+
FastImageBlur.blur(pixels, w, h, 2 * Math.max(1, Math.round(scale / 2)) + 1);
|
|
80
|
+
buffer.rewind();
|
|
81
|
+
bitmap.copyPixelsFromBuffer(buffer);
|
|
82
|
+
cache.put(uri, bitmap);
|
|
83
|
+
return bitmap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static boolean isDataUri(String value) {
|
|
87
|
+
return value.regionMatches(true, 0, "data:", 0, 5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A base64 data uri's image (PNG, JPEG, WebP, a GIF's first frame), or
|
|
91
|
+
// null if it isn't one Android decodes (e.g. an SVG).
|
|
92
|
+
@Nullable
|
|
93
|
+
static Bitmap dataUri(String uri) {
|
|
94
|
+
int comma = uri.indexOf(',');
|
|
95
|
+
if (comma < 0 || !uri.substring(0, comma).toLowerCase(Locale.ROOT).endsWith(";base64")) return null;
|
|
96
|
+
byte[] data;
|
|
97
|
+
try {
|
|
98
|
+
data = Base64.decode(uri.substring(comma + 1), Base64.DEFAULT);
|
|
99
|
+
} catch (IllegalArgumentException e) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return BitmapFactory.decodeByteArray(data, 0, data.length);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The placeholder for a base64 ThumbHash (standard or url-safe, with or
|
|
106
|
+
// without padding), or null for an empty or invalid one.
|
|
107
|
+
@Nullable
|
|
108
|
+
static Bitmap bitmap(@Nullable String thumbhash) {
|
|
109
|
+
if (thumbhash == null || thumbhash.isEmpty()) return null;
|
|
110
|
+
Bitmap cached = cache.get(thumbhash);
|
|
111
|
+
if (cached != null) return cached;
|
|
112
|
+
StringBuilder base64 = new StringBuilder(thumbhash.trim().replace('-', '+').replace('_', '/'));
|
|
113
|
+
while (base64.length() % 4 != 0) base64.append('=');
|
|
114
|
+
byte[] hash;
|
|
115
|
+
try {
|
|
116
|
+
hash = Base64.decode(base64.toString(), Base64.NO_WRAP);
|
|
117
|
+
} catch (IllegalArgumentException e) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
int[] size = new int[2];
|
|
121
|
+
int[] colors = thumbHashToColors(hash, size);
|
|
122
|
+
if (colors == null) return null;
|
|
123
|
+
Bitmap bitmap = Bitmap.createBitmap(colors, size[0], size[1], Bitmap.Config.ARGB_8888);
|
|
124
|
+
cache.put(thumbhash, bitmap);
|
|
125
|
+
return bitmap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The hash decoded as unpremultiplied ARGB colors (Bitmap.createBitmap's),
|
|
129
|
+
// its width and height in size, or null if it's too short.
|
|
130
|
+
@Nullable
|
|
131
|
+
private static int[] thumbHashToColors(byte[] hash, int[] size) {
|
|
132
|
+
if (hash.length < 5) return null;
|
|
133
|
+
// Read the constants
|
|
134
|
+
int header24 = (hash[0] & 255) | ((hash[1] & 255) << 8) | ((hash[2] & 255) << 16);
|
|
135
|
+
int header16 = (hash[3] & 255) | ((hash[4] & 255) << 8);
|
|
136
|
+
float l_dc = (float) (header24 & 63) / 63.0f;
|
|
137
|
+
float p_dc = (float) ((header24 >> 6) & 63) / 31.5f - 1.0f;
|
|
138
|
+
float q_dc = (float) ((header24 >> 12) & 63) / 31.5f - 1.0f;
|
|
139
|
+
float l_scale = (float) ((header24 >> 18) & 31) / 31.0f;
|
|
140
|
+
boolean hasAlpha = (header24 >> 23) != 0;
|
|
141
|
+
float p_scale = (float) ((header16 >> 3) & 63) / 63.0f;
|
|
142
|
+
float q_scale = (float) ((header16 >> 9) & 63) / 63.0f;
|
|
143
|
+
boolean isLandscape = (header16 >> 15) != 0;
|
|
144
|
+
int lx = Math.max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7);
|
|
145
|
+
int ly = Math.max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7);
|
|
146
|
+
int ac_start = hasAlpha ? 6 : 5;
|
|
147
|
+
int ac_count = count(lx, ly) + 2 * count(3, 3) + (hasAlpha ? count(5, 5) : 0);
|
|
148
|
+
if (hash.length < ac_start + (ac_count + 1) / 2) return null;
|
|
149
|
+
float a_dc = hasAlpha ? (float) (hash[5] & 15) / 15.0f : 1.0f;
|
|
150
|
+
float a_scale = hasAlpha ? (float) ((hash[5] >> 4) & 15) / 15.0f : 0;
|
|
151
|
+
|
|
152
|
+
// Read the varying factors (boost saturation by 1.25x to compensate for quantization)
|
|
153
|
+
int[] ac_index = {0};
|
|
154
|
+
float[] l_ac = decode(hash, ac_start, ac_index, lx, ly, l_scale);
|
|
155
|
+
float[] p_ac = decode(hash, ac_start, ac_index, 3, 3, p_scale * 1.25f);
|
|
156
|
+
float[] q_ac = decode(hash, ac_start, ac_index, 3, 3, q_scale * 1.25f);
|
|
157
|
+
float[] a_ac = hasAlpha ? decode(hash, ac_start, ac_index, 5, 5, a_scale) : null;
|
|
158
|
+
|
|
159
|
+
// Decode using the DCT into RGB
|
|
160
|
+
float ratio = approximateAspectRatio(hash);
|
|
161
|
+
int w = Math.max(1, Math.round(ratio > 1.0f ? 32.0f : 32.0f * ratio));
|
|
162
|
+
int h = Math.max(1, Math.round(ratio > 1.0f ? 32.0f / ratio : 32.0f));
|
|
163
|
+
int[] colors = new int[w * h];
|
|
164
|
+
int cx_stop = Math.max(lx, hasAlpha ? 5 : 3);
|
|
165
|
+
int cy_stop = Math.max(ly, hasAlpha ? 5 : 3);
|
|
166
|
+
float[] fx = new float[cx_stop];
|
|
167
|
+
float[] fy = new float[cy_stop];
|
|
168
|
+
for (int y = 0, i = 0; y < h; y++) {
|
|
169
|
+
for (int x = 0; x < w; x++, i++) {
|
|
170
|
+
float l = l_dc, p = p_dc, q = q_dc, a = a_dc;
|
|
171
|
+
|
|
172
|
+
// Precompute the coefficients
|
|
173
|
+
for (int cx = 0; cx < cx_stop; cx++)
|
|
174
|
+
fx[cx] = (float) Math.cos(Math.PI / w * (x + 0.5f) * cx);
|
|
175
|
+
for (int cy = 0; cy < cy_stop; cy++)
|
|
176
|
+
fy[cy] = (float) Math.cos(Math.PI / h * (y + 0.5f) * cy);
|
|
177
|
+
|
|
178
|
+
// Decode L
|
|
179
|
+
for (int cy = 0, j = 0; cy < ly; cy++) {
|
|
180
|
+
float fy2 = fy[cy] * 2.0f;
|
|
181
|
+
for (int cx = cy > 0 ? 0 : 1; cx * ly < lx * (ly - cy); cx++, j++)
|
|
182
|
+
l += l_ac[j] * fx[cx] * fy2;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Decode P and Q
|
|
186
|
+
for (int cy = 0, j = 0; cy < 3; cy++) {
|
|
187
|
+
float fy2 = fy[cy] * 2.0f;
|
|
188
|
+
for (int cx = cy > 0 ? 0 : 1; cx < 3 - cy; cx++, j++) {
|
|
189
|
+
float f = fx[cx] * fy2;
|
|
190
|
+
p += p_ac[j] * f;
|
|
191
|
+
q += q_ac[j] * f;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Decode A
|
|
196
|
+
if (a_ac != null)
|
|
197
|
+
for (int cy = 0, j = 0; cy < 5; cy++) {
|
|
198
|
+
float fy2 = fy[cy] * 2.0f;
|
|
199
|
+
for (int cx = cy > 0 ? 0 : 1; cx < 5 - cy; cx++, j++)
|
|
200
|
+
a += a_ac[j] * fx[cx] * fy2;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Convert to RGB
|
|
204
|
+
float b = l - 2.0f / 3.0f * p;
|
|
205
|
+
float r = (3.0f * l - b + q) / 2.0f;
|
|
206
|
+
float g = r - q;
|
|
207
|
+
colors[i] = (channel(a) << 24) | (channel(r) << 16) | (channel(g) << 8) | channel(b);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
size[0] = w;
|
|
211
|
+
size[1] = h;
|
|
212
|
+
return colors;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private static int channel(float value) {
|
|
216
|
+
return Math.max(0, Math.round(255.0f * Math.min(1, value)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// How many AC factors a channel of nx × ny has.
|
|
220
|
+
private static int count(int nx, int ny) {
|
|
221
|
+
int n = 0;
|
|
222
|
+
for (int cy = 0; cy < ny; cy++)
|
|
223
|
+
for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++)
|
|
224
|
+
n++;
|
|
225
|
+
return n;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private static float[] decode(byte[] hash, int start, int[] index, int nx, int ny, float scale) {
|
|
229
|
+
float[] ac = new float[count(nx, ny)];
|
|
230
|
+
for (int i = 0; i < ac.length; i++) {
|
|
231
|
+
int data = hash[start + (index[0] >> 1)] >> ((index[0] & 1) << 2);
|
|
232
|
+
ac[i] = ((float) (data & 15) / 7.5f - 1.0f) * scale;
|
|
233
|
+
index[0]++;
|
|
234
|
+
}
|
|
235
|
+
return ac;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private static float approximateAspectRatio(byte[] hash) {
|
|
239
|
+
byte header = hash[3];
|
|
240
|
+
boolean hasAlpha = (hash[2] & 0x80) != 0;
|
|
241
|
+
boolean isLandscape = (hash[4] & 0x80) != 0;
|
|
242
|
+
int lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7;
|
|
243
|
+
int ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7;
|
|
244
|
+
return ly == 0 ? 1 : (float) lx / (float) ly;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -145,7 +145,7 @@ class FastImageViewConverter {
|
|
|
145
145
|
.set(FastImageGlide.REQUEST, true)
|
|
146
146
|
.set(FastImageGlide.NO_STORE, noStore);
|
|
147
147
|
|
|
148
|
-
// imageSource is null when only
|
|
148
|
+
// imageSource is null when only the placeholder prop is shown.
|
|
149
149
|
if (imageSource != null && imageSource.isResource()) {
|
|
150
150
|
// Every local resource (drawable) in Android has its own unique numeric id, which are
|
|
151
151
|
// generated at build time. Although these ids are unique, they are not guaranteed unique
|
|
@@ -7,9 +7,12 @@ import static com.fastimage.FastImageRequestListener.REACT_ON_LOAD_EVENT;
|
|
|
7
7
|
import android.app.Activity;
|
|
8
8
|
import android.content.Context;
|
|
9
9
|
import android.content.ContextWrapper;
|
|
10
|
+
import android.graphics.Bitmap;
|
|
10
11
|
import android.graphics.PorterDuff;
|
|
11
12
|
import android.util.Log;
|
|
12
13
|
import android.graphics.Rect;
|
|
14
|
+
import android.graphics.drawable.BitmapDrawable;
|
|
15
|
+
import android.util.DisplayMetrics;
|
|
13
16
|
|
|
14
17
|
import androidx.annotation.NonNull;
|
|
15
18
|
|
|
@@ -312,12 +315,19 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
|
|
|
312
315
|
view.setSources(sources.isNull() ? null : sources.asArray());
|
|
313
316
|
}
|
|
314
317
|
|
|
315
|
-
// The resolved asset's uri.
|
|
318
|
+
// The `placeholder` prop: the resolved asset's uri, or a data uri.
|
|
316
319
|
@Override
|
|
317
|
-
public void
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
320
|
+
public void setPlaceholder(FastImageViewWithUrl view, Dynamic source) {
|
|
321
|
+
String uri = source.isNull() ? null : source.asString();
|
|
322
|
+
if (uri != null && FastImageThumbHash.isDataUri(uri)) {
|
|
323
|
+
Bitmap bitmap = FastImageThumbHash.dataUri(uri);
|
|
324
|
+
// One pixel per dp, as on iOS and the web (decoded, it's at the
|
|
325
|
+
// screen's density: one pixel per pixel).
|
|
326
|
+
if (bitmap != null) bitmap.setDensity(DisplayMetrics.DENSITY_DEFAULT);
|
|
327
|
+
view.setPlaceholder(bitmap == null ? null : new BitmapDrawable(view.getResources(), bitmap));
|
|
328
|
+
} else {
|
|
329
|
+
view.setPlaceholder(ResourceDrawableIdHelper.getResourceDrawable(view.getContext(), uri));
|
|
330
|
+
}
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
@Override
|
|
@@ -334,6 +344,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
|
|
|
334
344
|
view.setRecyclingKey(recyclingKey);
|
|
335
345
|
}
|
|
336
346
|
|
|
347
|
+
@Override
|
|
348
|
+
public void setSourcePlaceholders(FastImageViewWithUrl view, @Nullable ReadableArray placeholders) {
|
|
349
|
+
view.setSourcePlaceholders(placeholders);
|
|
350
|
+
}
|
|
351
|
+
|
|
337
352
|
@Override
|
|
338
353
|
public void setLoopCount(FastImageViewWithUrl view, int loopCount) {
|
|
339
354
|
view.setLoopCount(loopCount);
|