react-native-fast-image 10.2.0 → 10.4.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 +38 -5
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +6 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.flow +3 -0
- package/dist/index.cjs.web.js +24 -11
- package/dist/index.cjs.web.js.flow +3 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1 -1
- package/dist/index.js.flow +3 -0
- package/dist/index.web.js +25 -12
- package/dist/index.web.js.flow +3 -0
- package/dist/specs/FastImageViewNativeComponent.d.ts +1 -0
- package/ios/FastImage/FFFDownsampledImage.h +10 -7
- package/ios/FastImage/FFFDownsampledImage.m +25 -3
- package/ios/FastImage/FFFastImageModule.mm +1 -1
- package/ios/FastImage/FFFastImageView.h +4 -0
- package/ios/FastImage/FFFastImageView.m +78 -2
- package/ios/FastImage/FFFastImageViewComponentView.mm +3 -0
- package/package.json +1 -1
- package/src/index.js.flow +3 -0
- package/src/index.tsx +43 -0
- package/src/index.web.tsx +58 -24
- package/src/specs/FastImageViewNativeComponent.ts +3 -0
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ npx expo install react-native-fast-image
|
|
|
71
71
|
|
|
72
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.
|
|
73
73
|
|
|
74
|
-
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).
|
|
75
75
|
|
|
76
76
|
To set the cache's size when the app starts, see [Native config](#native-config).
|
|
77
77
|
|
|
@@ -161,6 +161,8 @@ A hint for which images to start loading first when several are waiting: `'high'
|
|
|
161
161
|
- `'normal'`: the default.
|
|
162
162
|
- `'high'`: e.g. the image the screen is about.
|
|
163
163
|
|
|
164
|
+
On the web, it's the image's `fetchpriority`.
|
|
165
|
+
|
|
164
166
|
---
|
|
165
167
|
|
|
166
168
|
##### `source.cache`
|
|
@@ -365,6 +367,22 @@ How the image is filtered when it's drawn smaller or larger than its size (like
|
|
|
365
367
|
|
|
366
368
|
---
|
|
367
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
|
+
|
|
368
386
|
#### `paused`
|
|
369
387
|
|
|
370
388
|
**Type:** `boolean`
|
|
@@ -406,6 +424,20 @@ If you control the images, serve them at the size they're shown (resized on your
|
|
|
406
424
|
|
|
407
425
|
---
|
|
408
426
|
|
|
427
|
+
#### `dynamicRangeLimit`
|
|
428
|
+
|
|
429
|
+
**Type:** `'standard' | 'constrained' | 'no-limit'` · iOS, Web only
|
|
430
|
+
|
|
431
|
+
How bright HDR photos (e.g. photos taken on an iPhone) can be, on displays that support it:
|
|
432
|
+
|
|
433
|
+
- `'standard'`: in standard range, like the rest of the screen.
|
|
434
|
+
- `'constrained'`: in HDR, their highlights brighter than the rest of the screen but held below the display's peak, e.g. for images among other content (a feed).
|
|
435
|
+
- `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
|
|
436
|
+
|
|
437
|
+
Not set, iOS shows them in standard range, and the web as the browser does by default (usually HDR). On iOS and tvOS 17 and later; Android shows them in standard range. On the web it's CSS's `dynamic-range-limit`.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
409
441
|
#### `blurRadius`
|
|
410
442
|
|
|
411
443
|
**Type:** `number` · **Default:** `0`
|
|
@@ -932,14 +964,15 @@ The app needs access to the photo library, which it has if it got the url from t
|
|
|
932
964
|
|
|
933
965
|
## Web
|
|
934
966
|
|
|
935
|
-
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.
|
|
967
|
+
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.
|
|
936
968
|
|
|
937
969
|
Support is minimal:
|
|
938
970
|
|
|
939
|
-
- 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`, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
|
|
940
|
-
- Ignored: `source.headers` (a browser can't send them for an image), `source.
|
|
971
|
+
- 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`.
|
|
972
|
+
- 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`.
|
|
941
973
|
- 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).
|
|
942
|
-
-
|
|
974
|
+
- Images load lazily unless [`loading`](#loading) is `'eager'`, and `source.priority` `'high'` and `'low'` are the `<img>`'s `fetchpriority`.
|
|
975
|
+
- 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.
|
|
943
976
|
- `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' }`.
|
|
944
977
|
|
|
945
978
|
### ThumbHashes on the web
|
|
@@ -389,6 +389,12 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
|
|
|
389
389
|
view.setDownsample(downsample);
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
// iOS only: Android shows an HDR photo (a gain map) in HDR only when its
|
|
393
|
+
// whole window is in HDR color mode.
|
|
394
|
+
@Override
|
|
395
|
+
public void setDynamicRangeLimit(FastImageViewWithUrl view, @Nullable String dynamicRangeLimit) {
|
|
396
|
+
}
|
|
397
|
+
|
|
392
398
|
// Which events JS has a handler for (FastImageEvents); with onProgress,
|
|
393
399
|
// the view gets its download's progress (see onDownloadProgress).
|
|
394
400
|
@Override
|
package/dist/index.cjs.js
CHANGED
|
@@ -347,7 +347,7 @@ function builtInTransition() {
|
|
|
347
347
|
return resolveTransition(react_native.Platform.OS === "android", NO_TRANSITION);
|
|
348
348
|
}
|
|
349
349
|
let defaultTransition;
|
|
350
|
-
function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
|
|
350
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
|
|
351
351
|
var _props$recyclingKey, _defaultTransition;
|
|
352
352
|
const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
|
|
353
353
|
if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
149
149
|
recyclingKey?: ?string,
|
|
150
150
|
loop?: ?(boolean | number),
|
|
151
151
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
|
+
loading?: ?('lazy' | 'eager'),
|
|
153
|
+
sizes?: ?string,
|
|
154
|
+
dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
|
|
152
155
|
paused?: ?boolean,
|
|
153
156
|
transition?: ?(boolean | number | Transition),
|
|
154
157
|
downsample?: ?boolean,
|
package/dist/index.cjs.web.js
CHANGED
|
@@ -278,16 +278,12 @@ function dataUri(value) {
|
|
|
278
278
|
}
|
|
279
279
|
//#endregion
|
|
280
280
|
//#region src/index.web.tsx
|
|
281
|
-
var _useId;
|
|
282
281
|
const notSupported = {
|
|
283
282
|
ok: false,
|
|
284
283
|
error: "Not supported on the web"
|
|
285
284
|
};
|
|
286
|
-
let ids = 0;
|
|
287
|
-
const useCounterId = () => (0, react.useState)(() => `${++ids}`)[0];
|
|
288
|
-
const useSvgId = (_useId = react.default.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
|
|
289
285
|
const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
|
|
290
|
-
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
286
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
291
287
|
var _resolveUri;
|
|
292
288
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
|
|
293
289
|
const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
|
|
@@ -297,12 +293,13 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
297
293
|
const src = sized ? sized.src : resolveUri(source);
|
|
298
294
|
const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
|
|
299
295
|
const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
|
|
296
|
+
const priority = sourcePriority(source);
|
|
300
297
|
const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
|
|
301
298
|
const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
|
|
302
299
|
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
303
300
|
const [result, setResult] = (0, react.useState)();
|
|
304
301
|
const ok = result && result.key === key ? result.ok : void 0;
|
|
305
|
-
const svgId =
|
|
302
|
+
const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
|
|
306
303
|
const tintId = `fast-image-tint-${svgId}`;
|
|
307
304
|
const blurId = `fast-image-blur-${svgId}`;
|
|
308
305
|
const fit = resolveObjectFit(objectFit, style);
|
|
@@ -322,6 +319,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
322
319
|
}, defaultUri && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
|
|
323
320
|
key: `default ${defaultUri}`,
|
|
324
321
|
src: defaultUri,
|
|
322
|
+
loading,
|
|
323
|
+
priority,
|
|
324
|
+
dynamicRangeLimit,
|
|
325
325
|
fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
|
|
326
326
|
position,
|
|
327
327
|
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
@@ -330,7 +330,10 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
330
330
|
key: several ? "sizes" : key,
|
|
331
331
|
src,
|
|
332
332
|
srcSet,
|
|
333
|
-
|
|
333
|
+
sizes,
|
|
334
|
+
loading,
|
|
335
|
+
priority,
|
|
336
|
+
dynamicRangeLimit,
|
|
334
337
|
fit,
|
|
335
338
|
position,
|
|
336
339
|
filter,
|
|
@@ -375,6 +378,14 @@ function thumbhashUri(hash) {
|
|
|
375
378
|
if (decode) return decode(hash);
|
|
376
379
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("thumbhash-web", "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.", "warn");
|
|
377
380
|
}
|
|
381
|
+
function sourcePriority(source) {
|
|
382
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) => entry && typeof entry === "object" ? entry.priority : void 0);
|
|
383
|
+
return [
|
|
384
|
+
"high",
|
|
385
|
+
"normal",
|
|
386
|
+
"low"
|
|
387
|
+
].find((priority) => values.includes(priority));
|
|
388
|
+
}
|
|
378
389
|
function shared(source, key, preview) {
|
|
379
390
|
const values = (Array.isArray(source) ? source : [source]).map((entry) => {
|
|
380
391
|
var _ref;
|
|
@@ -429,7 +440,7 @@ const fill = {
|
|
|
429
440
|
width: "100%",
|
|
430
441
|
height: "100%"
|
|
431
442
|
};
|
|
432
|
-
function Picture({ src, srcSet,
|
|
443
|
+
function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
|
|
433
444
|
const image = (0, react.useRef)(null);
|
|
434
445
|
const mounted = (0, react.useRef)(true);
|
|
435
446
|
const handled = (0, react.useRef)(void 0);
|
|
@@ -466,8 +477,9 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
|
|
|
466
477
|
ref: image,
|
|
467
478
|
src,
|
|
468
479
|
srcSet,
|
|
469
|
-
sizes: srcSet ? "auto, 100vw" : void 0,
|
|
470
|
-
loading:
|
|
480
|
+
sizes: srcSet ? sizes !== null && sizes !== void 0 ? sizes : "auto, 100vw" : void 0,
|
|
481
|
+
loading: loading === "eager" ? void 0 : "lazy",
|
|
482
|
+
fetchPriority: priority === "high" || priority === "low" ? priority : void 0,
|
|
471
483
|
alt,
|
|
472
484
|
draggable: false,
|
|
473
485
|
style: {
|
|
@@ -475,7 +487,8 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
|
|
|
475
487
|
objectFit: fit,
|
|
476
488
|
objectPosition: position,
|
|
477
489
|
filter,
|
|
478
|
-
opacity: hidden ? 0 : void 0
|
|
490
|
+
opacity: hidden ? 0 : void 0,
|
|
491
|
+
dynamicRangeLimit
|
|
479
492
|
},
|
|
480
493
|
onLoad: (event) => loaded(event.currentTarget),
|
|
481
494
|
onError: (event) => {
|
|
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
149
149
|
recyclingKey?: ?string,
|
|
150
150
|
loop?: ?(boolean | number),
|
|
151
151
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
|
+
loading?: ?('lazy' | 'eager'),
|
|
153
|
+
sizes?: ?string,
|
|
154
|
+
dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
|
|
152
155
|
paused?: ?boolean,
|
|
153
156
|
transition?: ?(boolean | number | Transition),
|
|
154
157
|
downsample?: ?boolean,
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type Source = {
|
|
|
35
35
|
* - `'normal'`: the default.
|
|
36
36
|
* - `'high'`: e.g. the image the screen is about.
|
|
37
37
|
*
|
|
38
|
+
* On the web, it's the image's `fetchpriority`.
|
|
39
|
+
*
|
|
38
40
|
* @default 'normal'
|
|
39
41
|
*/
|
|
40
42
|
priority?: Priority;
|
|
@@ -452,6 +454,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
452
454
|
* @default 'auto'
|
|
453
455
|
*/
|
|
454
456
|
imageRendering?: 'auto' | 'smooth' | 'pixelated';
|
|
457
|
+
/**
|
|
458
|
+
* Whether the image loads as it nears the screen (`'lazy'`) or at once
|
|
459
|
+
* (`'eager'`).
|
|
460
|
+
*
|
|
461
|
+
* @default 'lazy'
|
|
462
|
+
* @platform web
|
|
463
|
+
*/
|
|
464
|
+
loading?: 'lazy' | 'eager';
|
|
465
|
+
/**
|
|
466
|
+
* With several sources, the width the image is shown at, as the image's
|
|
467
|
+
* [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes),
|
|
468
|
+
* so the browser loads the size that fits. The default is the width the
|
|
469
|
+
* image is laid out at, which needs `loading="lazy"` (otherwise, or in
|
|
470
|
+
* browsers without it, the window's width).
|
|
471
|
+
*
|
|
472
|
+
* @default 'auto, 100vw'
|
|
473
|
+
* @platform web
|
|
474
|
+
*/
|
|
475
|
+
sizes?: string;
|
|
455
476
|
/**
|
|
456
477
|
* Pauses an animated image (GIF, animated WebP, APNG, or animated AVIF) on
|
|
457
478
|
* the frame it's showing; `false` plays it again from there
|
|
@@ -512,6 +533,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
512
533
|
* @default true
|
|
513
534
|
*/
|
|
514
535
|
downsample?: boolean;
|
|
536
|
+
/**
|
|
537
|
+
* How bright HDR photos (e.g. photos taken on an iPhone) can be, on
|
|
538
|
+
* displays that support it:
|
|
539
|
+
*
|
|
540
|
+
* - `'standard'`: in standard range, like the rest of the screen.
|
|
541
|
+
* - `'constrained'`: in HDR, their highlights brighter than the rest of
|
|
542
|
+
* the screen but held below the display's peak, e.g. for images among
|
|
543
|
+
* other content (a feed).
|
|
544
|
+
* - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
|
|
545
|
+
*
|
|
546
|
+
* Not set, iOS shows them in standard range, and the web as the browser
|
|
547
|
+
* does by default (usually HDR). On iOS and tvOS 17 and later; Android
|
|
548
|
+
* shows them in standard range. On the web it's CSS's
|
|
549
|
+
* `dynamic-range-limit`.
|
|
550
|
+
*
|
|
551
|
+
* @platform ios
|
|
552
|
+
* @platform web
|
|
553
|
+
*/
|
|
554
|
+
dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit';
|
|
515
555
|
/**
|
|
516
556
|
* Blurs the image by this radius, in points (the same radius looks about
|
|
517
557
|
* the same on iOS and Android). `0` is no blur.
|
package/dist/index.js
CHANGED
|
@@ -320,7 +320,7 @@ function builtInTransition() {
|
|
|
320
320
|
return resolveTransition(Platform.OS === "android", NO_TRANSITION);
|
|
321
321
|
}
|
|
322
322
|
let defaultTransition;
|
|
323
|
-
function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, forwardedRef, ...viewProps }) {
|
|
323
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
|
|
324
324
|
var _props$recyclingKey, _defaultTransition;
|
|
325
325
|
const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
|
|
326
326
|
if (__DEV__ && fallback !== void 0) warnOnce("fallback", "react-native-fast-image: `fallback` was removed in 10.0, and does nothing: images always load natively. Remove it, or use React Native's Image for those images.", "warn");
|
package/dist/index.js.flow
CHANGED
|
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
149
149
|
recyclingKey?: ?string,
|
|
150
150
|
loop?: ?(boolean | number),
|
|
151
151
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
|
+
loading?: ?('lazy' | 'eager'),
|
|
153
|
+
sizes?: ?string,
|
|
154
|
+
dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
|
|
152
155
|
paused?: ?boolean,
|
|
153
156
|
transition?: ?(boolean | number | Transition),
|
|
154
157
|
downsample?: ?boolean,
|
package/dist/index.web.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Children, forwardRef, memo, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import React, { Children, forwardRef, memo, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { Image, PixelRatio, StyleSheet, View } from "react-native";
|
|
3
3
|
import { getAssetByID } from "react-native-web/dist/modules/AssetRegistry";
|
|
4
4
|
//#region src/constants.ts
|
|
@@ -251,16 +251,12 @@ function dataUri(value) {
|
|
|
251
251
|
}
|
|
252
252
|
//#endregion
|
|
253
253
|
//#region src/index.web.tsx
|
|
254
|
-
var _useId;
|
|
255
254
|
const notSupported = {
|
|
256
255
|
ok: false,
|
|
257
256
|
error: "Not supported on the web"
|
|
258
257
|
};
|
|
259
|
-
let ids = 0;
|
|
260
|
-
const useCounterId = () => useState(() => `${++ids}`)[0];
|
|
261
|
-
const useSvgId = (_useId = React.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
|
|
262
258
|
const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
|
|
263
|
-
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
259
|
+
function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, dynamicRangeLimit, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
264
260
|
var _resolveUri;
|
|
265
261
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
|
|
266
262
|
const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
|
|
@@ -270,12 +266,13 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
270
266
|
const src = sized ? sized.src : resolveUri(source);
|
|
271
267
|
const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
|
|
272
268
|
const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
|
|
269
|
+
const priority = sourcePriority(source);
|
|
273
270
|
const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
|
|
274
271
|
const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
|
|
275
272
|
const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
|
|
276
273
|
const [result, setResult] = useState();
|
|
277
274
|
const ok = result && result.key === key ? result.ok : void 0;
|
|
278
|
-
const svgId =
|
|
275
|
+
const svgId = useId().replace(/[^\w-]/g, "");
|
|
279
276
|
const tintId = `fast-image-tint-${svgId}`;
|
|
280
277
|
const blurId = `fast-image-blur-${svgId}`;
|
|
281
278
|
const fit = resolveObjectFit(objectFit, style);
|
|
@@ -295,6 +292,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
295
292
|
}, defaultUri && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
|
|
296
293
|
key: `default ${defaultUri}`,
|
|
297
294
|
src: defaultUri,
|
|
295
|
+
loading,
|
|
296
|
+
priority,
|
|
297
|
+
dynamicRangeLimit,
|
|
298
298
|
fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
|
|
299
299
|
position,
|
|
300
300
|
filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
|
|
@@ -303,7 +303,10 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
|
|
|
303
303
|
key: several ? "sizes" : key,
|
|
304
304
|
src,
|
|
305
305
|
srcSet,
|
|
306
|
-
|
|
306
|
+
sizes,
|
|
307
|
+
loading,
|
|
308
|
+
priority,
|
|
309
|
+
dynamicRangeLimit,
|
|
307
310
|
fit,
|
|
308
311
|
position,
|
|
309
312
|
filter,
|
|
@@ -348,6 +351,14 @@ function thumbhashUri(hash) {
|
|
|
348
351
|
if (decode) return decode(hash);
|
|
349
352
|
if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("thumbhash-web", "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.", "warn");
|
|
350
353
|
}
|
|
354
|
+
function sourcePriority(source) {
|
|
355
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) => entry && typeof entry === "object" ? entry.priority : void 0);
|
|
356
|
+
return [
|
|
357
|
+
"high",
|
|
358
|
+
"normal",
|
|
359
|
+
"low"
|
|
360
|
+
].find((priority) => values.includes(priority));
|
|
361
|
+
}
|
|
351
362
|
function shared(source, key, preview) {
|
|
352
363
|
const values = (Array.isArray(source) ? source : [source]).map((entry) => {
|
|
353
364
|
var _ref;
|
|
@@ -402,7 +413,7 @@ const fill = {
|
|
|
402
413
|
width: "100%",
|
|
403
414
|
height: "100%"
|
|
404
415
|
};
|
|
405
|
-
function Picture({ src, srcSet,
|
|
416
|
+
function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
|
|
406
417
|
const image = useRef(null);
|
|
407
418
|
const mounted = useRef(true);
|
|
408
419
|
const handled = useRef(void 0);
|
|
@@ -439,8 +450,9 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
|
|
|
439
450
|
ref: image,
|
|
440
451
|
src,
|
|
441
452
|
srcSet,
|
|
442
|
-
sizes: srcSet ? "auto, 100vw" : void 0,
|
|
443
|
-
loading:
|
|
453
|
+
sizes: srcSet ? sizes !== null && sizes !== void 0 ? sizes : "auto, 100vw" : void 0,
|
|
454
|
+
loading: loading === "eager" ? void 0 : "lazy",
|
|
455
|
+
fetchPriority: priority === "high" || priority === "low" ? priority : void 0,
|
|
444
456
|
alt,
|
|
445
457
|
draggable: false,
|
|
446
458
|
style: {
|
|
@@ -448,7 +460,8 @@ function Picture({ src, srcSet, lazy, fit, position, filter, hidden, alt, onLoad
|
|
|
448
460
|
objectFit: fit,
|
|
449
461
|
objectPosition: position,
|
|
450
462
|
filter,
|
|
451
|
-
opacity: hidden ? 0 : void 0
|
|
463
|
+
opacity: hidden ? 0 : void 0,
|
|
464
|
+
dynamicRangeLimit
|
|
452
465
|
},
|
|
453
466
|
onLoad: (event) => loaded(event.currentTarget),
|
|
454
467
|
onError: (event) => {
|
package/dist/index.web.js.flow
CHANGED
|
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
149
149
|
recyclingKey?: ?string,
|
|
150
150
|
loop?: ?(boolean | number),
|
|
151
151
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
|
+
loading?: ?('lazy' | 'eager'),
|
|
153
|
+
sizes?: ?string,
|
|
154
|
+
dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
|
|
152
155
|
paused?: ?boolean,
|
|
153
156
|
transition?: ?(boolean | number | Transition),
|
|
154
157
|
downsample?: ?boolean,
|
|
@@ -37,6 +37,7 @@ export interface NativeProps extends ViewProps {
|
|
|
37
37
|
transitionBetweenImages?: boolean;
|
|
38
38
|
transitionSkipOnCacheHit?: string;
|
|
39
39
|
downsample?: CodegenTypes.WithDefault<boolean, true>;
|
|
40
|
+
dynamicRangeLimit?: string;
|
|
40
41
|
blurRadius?: CodegenTypes.Float;
|
|
41
42
|
handledEvents?: CodegenTypes.Int32;
|
|
42
43
|
onFastImageLoadStart?: CodegenTypes.DirectEventHandler<LoadStartEvent>;
|
|
@@ -25,12 +25,15 @@
|
|
|
25
25
|
// loaded this way was decoded from, for onLoad, or zero for other images.
|
|
26
26
|
+ (CGSize) sourceSizeOfImage: (UIImage*)image;
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
28
|
+
// Drops the memory cache's image for a load (call it before the load) when
|
|
29
|
+
// the load can't use it, so it decodes the file from the disk cache:
|
|
30
|
+
// - for a full-size load (a view that doesn't downsample, or a preload), a
|
|
31
|
+
// smaller image: SDWebImage puts an image it decoded smaller from the disk
|
|
32
|
+
// cache, for a view that downsamples, under the full-size image's key too
|
|
33
|
+
// (SDWebImageManager writes what its original cache query finds back
|
|
34
|
+
// there), which would show blurry, or be a preload's size.
|
|
35
|
+
// - one decoded the other way, to HDR or SDR (dynamicRangeLimit), since
|
|
36
|
+
// SDWebImage's memory cache key is the same for both.
|
|
37
|
+
+ (void) forgetUnusableImageForURL: (NSURL*)url context: (SDWebImageContext*)context;
|
|
35
38
|
|
|
36
39
|
@end
|
|
@@ -77,7 +77,7 @@ static CGSize FFFPixelSize(NSData* data) {
|
|
|
77
77
|
return [sourceSize isKindOfClass: [FFFSourceSize class]] ? ((FFFSourceSize*) sourceSize).size : CGSizeZero;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
+ (void)
|
|
80
|
+
+ (void) forgetUnusableImageForURL: (NSURL*)url context: (SDWebImageContext*)context {
|
|
81
81
|
SDWebImageManager* manager = [SDWebImageManager sharedManager];
|
|
82
82
|
if (!url || ![manager.imageCache isKindOfClass: [SDImageCache class]] || ![manager respondsToSelector: @selector(cacheKeyForURL:context:)]) {
|
|
83
83
|
return;
|
|
@@ -85,12 +85,34 @@ static CGSize FFFPixelSize(NSData* data) {
|
|
|
85
85
|
SDImageCache* cache = (SDImageCache*) manager.imageCache;
|
|
86
86
|
NSString* key = [manager cacheKeyForURL: url context: context];
|
|
87
87
|
UIImage* image = [cache imageFromMemoryCacheForKey: key];
|
|
88
|
-
|
|
89
|
-
if (image && fullSize.width > 0 && fullSize.height > 0 && !CGSizeEqualToSize(image.size, fullSize)) {
|
|
88
|
+
if (image && ([self isSmallerFullSizeImage: image context: context] || [self isOtherDynamicRange: image context: context])) {
|
|
90
89
|
[cache removeImageFromMemoryForKey: key];
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
|
|
93
|
+
// A smaller image under a full-size load's key.
|
|
94
|
+
+ (BOOL) isSmallerFullSizeImage: (UIImage*)image context: (SDWebImageContext*)context {
|
|
95
|
+
if (context[SDWebImageContextImageThumbnailPixelSize]) {
|
|
96
|
+
return NO;
|
|
97
|
+
}
|
|
98
|
+
CGSize fullSize = [self sourceSizeOfImage: image];
|
|
99
|
+
return fullSize.width > 0 && fullSize.height > 0 && !CGSizeEqualToSize(image.size, fullSize);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// An image decoded to SDR for an HDR load, or to HDR for an SDR load. Only
|
|
103
|
+
// images SDWebImage decoded say how: not a photo library's, or an app's own
|
|
104
|
+
// loader's, which are kept. One decoded to HDR that came out SDR (most images
|
|
105
|
+
// have no HDR) is the same as an SDR decode, and an HDR file decoded without
|
|
106
|
+
// asking is HDR either way (e.g. an HDR10 HEIC on iOS 15 and 16).
|
|
107
|
+
+ (BOOL) isOtherDynamicRange: (UIImage*)image context: (SDWebImageContext*)context {
|
|
108
|
+
if (!image.sd_decodeOptions) {
|
|
109
|
+
return NO;
|
|
110
|
+
}
|
|
111
|
+
BOOL wantsHDR = [context[SDWebImageContextImageDecodeToHDR] boolValue];
|
|
112
|
+
BOOL decodedToHDR = [image.sd_decodeOptions[SDImageCoderDecodeToHDR] boolValue];
|
|
113
|
+
return wantsHDR ? !decodedToHDR : decodedToHDR && image.sd_isHighDynamicRange;
|
|
114
|
+
}
|
|
115
|
+
|
|
94
116
|
- (instancetype) initWithData: (NSData*)data scale: (CGFloat)scale options: (SDImageCoderOptions*)options {
|
|
95
117
|
// An SVG isn't decoded smaller: it's a vector image, drawn at any size.
|
|
96
118
|
// This class can't decode it, and SDWebImage would then give it to the
|
|
@@ -305,7 +305,7 @@ static NSDictionary *FFFFile(NSString *path)
|
|
|
305
305
|
SDWebImageOptions options = FFFPreloadOptions(source);
|
|
306
306
|
SDWebImageMutableContext *context = FFFPreloadContext(source);
|
|
307
307
|
// It looks in the memory cache first, for the full-size image.
|
|
308
|
-
[FFFDownsampledImage
|
|
308
|
+
[FFFDownsampledImage forgetUnusableImageForURL:source.url context:context];
|
|
309
309
|
if (!source.memoryCache) {
|
|
310
310
|
// Not decoded (only its header is read, for the size), and not
|
|
311
311
|
// kept in memory, also when it comes from the disk cache.
|
|
@@ -59,6 +59,10 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
|
|
|
59
59
|
@property (nonatomic, copy) NSString *transitionSkipOnCacheHit;
|
|
60
60
|
// Decodes images at about the view's size instead of at full size.
|
|
61
61
|
@property (nonatomic, assign) BOOL downsample;
|
|
62
|
+
// The dynamicRangeLimit prop: standard, constrained or no-limit (nil when
|
|
63
|
+
// not set). With constrained and no-limit, HDR images are decoded to HDR and
|
|
64
|
+
// shown in HDR (iOS and tvOS 17+).
|
|
65
|
+
@property (nonatomic, copy) NSString *dynamicRangeLimit;
|
|
62
66
|
// Blurs the loaded image by this radius, in points (0 is no blur).
|
|
63
67
|
@property (nonatomic, assign) CGFloat blurRadius;
|
|
64
68
|
// objectPosition, as CSS computes it: on each axis the image's left or top
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#import <React/RCTUtils.h>
|
|
5
5
|
#import <SDWebImage/SDWebImageError.h>
|
|
6
6
|
#import <SDWebImage/SDImageTransformer.h>
|
|
7
|
+
#import <SDWebImage/SDImageCodersManager.h>
|
|
7
8
|
#import "FFFDownsampledImage.h"
|
|
8
9
|
#import "FFFThumbHash.h"
|
|
9
10
|
#import "FFFBoxBlur.h"
|
|
@@ -73,6 +74,9 @@
|
|
|
73
74
|
// source's), and whether that's the source's (hashImage).
|
|
74
75
|
@property(nonatomic, assign) BOOL showsPlaceholder;
|
|
75
76
|
@property(nonatomic, assign) BOOL showsHash;
|
|
77
|
+
// dynamicRangeLimit switched between HDR and SDR: the image is decoded again,
|
|
78
|
+
// quietly (see reloadForDynamicRange).
|
|
79
|
+
@property(nonatomic, assign) BOOL needsDynamicRangeReload;
|
|
76
80
|
|
|
77
81
|
@end
|
|
78
82
|
|
|
@@ -643,6 +647,14 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
|
|
|
643
647
|
if (image.size.width <= 0 || image.size.height <= 0) {
|
|
644
648
|
return image;
|
|
645
649
|
}
|
|
650
|
+
// Blurred in standard range (the bitmap below has 8 bits per channel): an
|
|
651
|
+
// HDR image (dynamicRangeLimit) tone mapped to it, rather than its
|
|
652
|
+
// highlights clipped.
|
|
653
|
+
if (@available(iOS 17.0, tvOS 17.0, *)) {
|
|
654
|
+
if (image.isHighDynamicRange) {
|
|
655
|
+
image = [image imageRestrictedToStandardDynamicRange];
|
|
656
|
+
}
|
|
657
|
+
}
|
|
646
658
|
double fullWidth = MAX(1, round(image.size.width * image.scale * scale));
|
|
647
659
|
double fullHeight = MAX(1, round(image.size.height * image.scale * scale));
|
|
648
660
|
// At most the image's larger side (a larger box looks the same, and an
|
|
@@ -798,6 +810,33 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
798
810
|
}
|
|
799
811
|
}
|
|
800
812
|
|
|
813
|
+
- (void) setDynamicRangeLimit: (NSString*)dynamicRangeLimit {
|
|
814
|
+
if (_dynamicRangeLimit == dynamicRangeLimit || [_dynamicRangeLimit isEqualToString: dynamicRangeLimit]) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
BOOL decodedToHDR = [self decodesToHDR];
|
|
818
|
+
_dynamicRangeLimit = [dynamicRangeLimit copy];
|
|
819
|
+
if (@available(iOS 17.0, tvOS 17.0, *)) {
|
|
820
|
+
// Constrained: the highlights brighter than SDR white, but not as
|
|
821
|
+
// bright as the display can go, as Apple suggests for images among
|
|
822
|
+
// other content.
|
|
823
|
+
self.preferredImageDynamicRange = [_dynamicRangeLimit isEqualToString: @"no-limit"] ? UIImageDynamicRangeHigh
|
|
824
|
+
: [_dynamicRangeLimit isEqualToString: @"constrained"] ? UIImageDynamicRangeConstrainedHigh
|
|
825
|
+
: [_dynamicRangeLimit isEqualToString: @"standard"] ? UIImageDynamicRangeStandard
|
|
826
|
+
: UIImageDynamicRangeUnspecified;
|
|
827
|
+
}
|
|
828
|
+
// Decoded again only to switch between HDR and SDR.
|
|
829
|
+
if ([self decodesToHDR] != decodedToHDR) {
|
|
830
|
+
self.needsDynamicRangeReload = YES;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Whether HDR images are decoded to HDR (dynamicRangeLimit constrained or
|
|
835
|
+
// no-limit), rather than to SDR, their standard range version.
|
|
836
|
+
- (BOOL) decodesToHDR {
|
|
837
|
+
return [_dynamicRangeLimit isEqualToString: @"constrained"] || [_dynamicRangeLimit isEqualToString: @"no-limit"];
|
|
838
|
+
}
|
|
839
|
+
|
|
801
840
|
- (void) setPlaceholder: (UIImage*)placeholder {
|
|
802
841
|
if (_placeholder != placeholder) {
|
|
803
842
|
_placeholder = placeholder;
|
|
@@ -865,11 +904,36 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
865
904
|
return;
|
|
866
905
|
}
|
|
867
906
|
[self reloadImage];
|
|
907
|
+
} else if (self.needsDynamicRangeReload) {
|
|
908
|
+
[self reloadForDynamicRange];
|
|
868
909
|
} else {
|
|
869
910
|
[self reloadIfResized];
|
|
870
911
|
}
|
|
871
912
|
}
|
|
872
913
|
|
|
914
|
+
// The same image decoded again for a dynamicRangeLimit between HDR and SDR,
|
|
915
|
+
// as a resize is (reloadIfResized): without events once it has loaded, and
|
|
916
|
+
// the image showing stays if the load fails. A display setting shouldn't
|
|
917
|
+
// look like a new load.
|
|
918
|
+
- (void) reloadForDynamicRange {
|
|
919
|
+
self.needsDynamicRangeReload = NO;
|
|
920
|
+
// Nothing to load, it failed, or it waits for the view's size: the next
|
|
921
|
+
// load decodes as asked.
|
|
922
|
+
if (!_source || self.hasErrored || self.waitsForSize) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
// A data uri is decoded at once, with its events (see reloadImage).
|
|
926
|
+
if ([[_source.url absoluteString] hasPrefix: @"data:image"]) {
|
|
927
|
+
[self reloadImage];
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
SDWebImageOptions options = [self loadOptions];
|
|
931
|
+
if (self.showsLoadedImage) {
|
|
932
|
+
options |= SDWebImageDelayPlaceholder;
|
|
933
|
+
}
|
|
934
|
+
[self downloadImage: _source options: options context: [self loadContext] events: !self.hasCompleted];
|
|
935
|
+
}
|
|
936
|
+
|
|
873
937
|
// Whether images are decoded at about the view's size (downsample, and always
|
|
874
938
|
// for photo library images, which are large and usually shown small: Photos
|
|
875
939
|
// makes them at the size asked for). Not for `none`, which shows it at its own
|
|
@@ -938,6 +1002,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
938
1002
|
|
|
939
1003
|
- (void) reloadImage {
|
|
940
1004
|
_needsReload = NO;
|
|
1005
|
+
self.needsDynamicRangeReload = NO;
|
|
941
1006
|
self.waitsForSize = NO;
|
|
942
1007
|
self.loadsSwitchedSource = self.switchesSource;
|
|
943
1008
|
self.switchesSource = NO;
|
|
@@ -961,7 +1026,12 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
961
1026
|
self.onFastImageLoadStart(@{});
|
|
962
1027
|
}
|
|
963
1028
|
// Use SDWebImage API to support external format like WebP images
|
|
964
|
-
|
|
1029
|
+
// (as sd_imageWithData does), decoded to HDR or SDR as a
|
|
1030
|
+
// download is (see contextForSource).
|
|
1031
|
+
UIImage* image = [[SDImageCodersManager sharedManager] decodedImageWithData: [NSData dataWithContentsOfURL: _source.url] options: @{
|
|
1032
|
+
SDImageCoderDecodeScaleFactor: @1,
|
|
1033
|
+
SDImageCoderDecodeToHDR: @([self decodesToHDR]),
|
|
1034
|
+
}];
|
|
965
1035
|
if (!image) {
|
|
966
1036
|
// Not decodable: fail like a remote image, showing the
|
|
967
1037
|
// placeholder.
|
|
@@ -1070,10 +1140,15 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1070
1140
|
context[SDWebImageContextDownloadRequestModifier] = source.requestModifier;
|
|
1071
1141
|
context[SDWebImageContextImageLoader] = source.imageLoader;
|
|
1072
1142
|
context[SDWebImageContextCacheKeyFilter] = source.cacheKeyFilter;
|
|
1143
|
+
if (@available(iOS 17.0, tvOS 17.0, *)) {
|
|
1144
|
+
// An HDR image (e.g. a photo with a gain map) decoded to HDR, or to
|
|
1145
|
+
// SDR, its standard range version.
|
|
1146
|
+
context[SDWebImageContextImageDecodeToHDR] = @([self decodesToHDR]);
|
|
1147
|
+
}
|
|
1073
1148
|
if (CGSizeEqualToSize(box, CGSizeZero)) {
|
|
1074
1149
|
[FFFDownsampledImage addFullSizeToContext: context];
|
|
1075
1150
|
[self applyCacheOptions: context source: source fullSize: YES];
|
|
1076
|
-
[FFFDownsampledImage
|
|
1151
|
+
[FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
|
|
1077
1152
|
return context;
|
|
1078
1153
|
}
|
|
1079
1154
|
[FFFDownsampledImage addToContext: context box: box cover: cover];
|
|
@@ -1085,6 +1160,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
1085
1160
|
context[SDWebImageContextOriginalQueryCacheType] = @(SDImageCacheTypeNone);
|
|
1086
1161
|
}
|
|
1087
1162
|
[self applyCacheOptions: context source: source fullSize: NO];
|
|
1163
|
+
[FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
|
|
1088
1164
|
return context;
|
|
1089
1165
|
}
|
|
1090
1166
|
|
|
@@ -287,6 +287,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
|
|
|
287
287
|
if (all || oldViewProps.downsample != newViewProps.downsample) {
|
|
288
288
|
view.downsample = newViewProps.downsample;
|
|
289
289
|
}
|
|
290
|
+
if (all || oldViewProps.dynamicRangeLimit != newViewProps.dynamicRangeLimit) {
|
|
291
|
+
view.dynamicRangeLimit = FFFString(newViewProps.dynamicRangeLimit);
|
|
292
|
+
}
|
|
290
293
|
if (all || oldViewProps.blurRadius != newViewProps.blurRadius) {
|
|
291
294
|
view.blurRadius = newViewProps.blurRadius;
|
|
292
295
|
}
|
package/package.json
CHANGED
package/src/index.js.flow
CHANGED
|
@@ -149,6 +149,9 @@ export type FastImageProps = $ReadOnly<{|
|
|
|
149
149
|
recyclingKey?: ?string,
|
|
150
150
|
loop?: ?(boolean | number),
|
|
151
151
|
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
152
|
+
loading?: ?('lazy' | 'eager'),
|
|
153
|
+
sizes?: ?string,
|
|
154
|
+
dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
|
|
152
155
|
paused?: ?boolean,
|
|
153
156
|
transition?: ?(boolean | number | Transition),
|
|
154
157
|
downsample?: ?boolean,
|
package/src/index.tsx
CHANGED
|
@@ -54,6 +54,8 @@ export type Source = {
|
|
|
54
54
|
* - `'normal'`: the default.
|
|
55
55
|
* - `'high'`: e.g. the image the screen is about.
|
|
56
56
|
*
|
|
57
|
+
* On the web, it's the image's `fetchpriority`.
|
|
58
|
+
*
|
|
57
59
|
* @default 'normal'
|
|
58
60
|
*/
|
|
59
61
|
priority?: Priority
|
|
@@ -474,6 +476,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
474
476
|
* @default 'auto'
|
|
475
477
|
*/
|
|
476
478
|
imageRendering?: 'auto' | 'smooth' | 'pixelated'
|
|
479
|
+
/**
|
|
480
|
+
* Whether the image loads as it nears the screen (`'lazy'`) or at once
|
|
481
|
+
* (`'eager'`).
|
|
482
|
+
*
|
|
483
|
+
* @default 'lazy'
|
|
484
|
+
* @platform web
|
|
485
|
+
*/
|
|
486
|
+
loading?: 'lazy' | 'eager'
|
|
487
|
+
/**
|
|
488
|
+
* With several sources, the width the image is shown at, as the image's
|
|
489
|
+
* [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes),
|
|
490
|
+
* so the browser loads the size that fits. The default is the width the
|
|
491
|
+
* image is laid out at, which needs `loading="lazy"` (otherwise, or in
|
|
492
|
+
* browsers without it, the window's width).
|
|
493
|
+
*
|
|
494
|
+
* @default 'auto, 100vw'
|
|
495
|
+
* @platform web
|
|
496
|
+
*/
|
|
497
|
+
sizes?: string
|
|
477
498
|
/**
|
|
478
499
|
* Pauses an animated image (GIF, animated WebP, APNG, or animated AVIF) on
|
|
479
500
|
* the frame it's showing; `false` plays it again from there
|
|
@@ -534,6 +555,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
534
555
|
* @default true
|
|
535
556
|
*/
|
|
536
557
|
downsample?: boolean
|
|
558
|
+
/**
|
|
559
|
+
* How bright HDR photos (e.g. photos taken on an iPhone) can be, on
|
|
560
|
+
* displays that support it:
|
|
561
|
+
*
|
|
562
|
+
* - `'standard'`: in standard range, like the rest of the screen.
|
|
563
|
+
* - `'constrained'`: in HDR, their highlights brighter than the rest of
|
|
564
|
+
* the screen but held below the display's peak, e.g. for images among
|
|
565
|
+
* other content (a feed).
|
|
566
|
+
* - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
|
|
567
|
+
*
|
|
568
|
+
* Not set, iOS shows them in standard range, and the web as the browser
|
|
569
|
+
* does by default (usually HDR). On iOS and tvOS 17 and later; Android
|
|
570
|
+
* shows them in standard range. On the web it's CSS's
|
|
571
|
+
* `dynamic-range-limit`.
|
|
572
|
+
*
|
|
573
|
+
* @platform ios
|
|
574
|
+
* @platform web
|
|
575
|
+
*/
|
|
576
|
+
dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit'
|
|
537
577
|
// The radius is converted from points to the bitmap's pixels on both
|
|
538
578
|
// platforms, so it matches React Native's Image.
|
|
539
579
|
/**
|
|
@@ -811,6 +851,9 @@ function FastImageBase({
|
|
|
811
851
|
objectPosition,
|
|
812
852
|
loop,
|
|
813
853
|
transition,
|
|
854
|
+
// Web only.
|
|
855
|
+
loading: _loading,
|
|
856
|
+
sizes: _sizes,
|
|
814
857
|
forwardedRef,
|
|
815
858
|
...viewProps
|
|
816
859
|
}: FastImageProps & { forwardedRef: React.Ref<any> }) {
|
package/src/index.web.tsx
CHANGED
|
@@ -11,15 +11,17 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Works: source (uri, require(), or several sizes: see sizedSources, and
|
|
13
13
|
// thumbhash and blurDataUri), placeholder (and defaultSource), objectFit,
|
|
14
|
-
// objectPosition, tintColor, blurRadius, style,
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// recyclingKey) and onProgress
|
|
14
|
+
// objectPosition, tintColor, blurRadius, style, source.priority (as
|
|
15
|
+
// fetchpriority), the web-only loading and sizes, onLoadStart, onLoad,
|
|
16
|
+
// onError, onLoadEnd, and View props (testID, accessibility, onLayout,
|
|
17
|
+
// pointerEvents). The native-only props (cache, headers, transition,
|
|
18
|
+
// downsample, loop, paused, imageRendering, recyclingKey) and onProgress
|
|
19
|
+
// are ignored.
|
|
19
20
|
import React, {
|
|
20
21
|
forwardRef,
|
|
21
22
|
memo,
|
|
22
23
|
useEffect,
|
|
24
|
+
useId,
|
|
23
25
|
useLayoutEffect,
|
|
24
26
|
useRef,
|
|
25
27
|
useState,
|
|
@@ -40,6 +42,7 @@ import type {
|
|
|
40
42
|
FastImageStaticProperties,
|
|
41
43
|
ObjectFit,
|
|
42
44
|
PreloadResult,
|
|
45
|
+
Priority,
|
|
43
46
|
Source,
|
|
44
47
|
SourcePlaceholder,
|
|
45
48
|
} from './index'
|
|
@@ -52,15 +55,6 @@ const notSupported: CachePathResult = {
|
|
|
52
55
|
error: 'Not supported on the web',
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
// An id that's the same on the server and in the browser: React's useId,
|
|
56
|
-
// from React 18 (this package's React types are older), without the
|
|
57
|
-
// characters an SVG id can't have (:r0: in React 18, «r0» in React 19). On
|
|
58
|
-
// React 17, a counter's (which can differ from the server's).
|
|
59
|
-
let ids = 0
|
|
60
|
-
const useCounterId = () => useState(() => `${++ids}`)[0]
|
|
61
|
-
const useSvgId: () => string =
|
|
62
|
-
(React as unknown as { useId?: () => string }).useId ?? useCounterId
|
|
63
|
-
|
|
64
58
|
// A layout effect in the browser (it runs before the image can show), and an
|
|
65
59
|
// effect when rendering on a server, where layout effects warn.
|
|
66
60
|
const useClientLayoutEffect =
|
|
@@ -86,6 +80,9 @@ function FastImageBase({
|
|
|
86
80
|
children,
|
|
87
81
|
objectFit,
|
|
88
82
|
objectPosition,
|
|
83
|
+
loading,
|
|
84
|
+
sizes,
|
|
85
|
+
dynamicRangeLimit,
|
|
89
86
|
forwardedRef,
|
|
90
87
|
// On the wrapper, the view the layout and touches are for here (on
|
|
91
88
|
// native, FastImage's only view).
|
|
@@ -125,6 +122,7 @@ function FastImageBase({
|
|
|
125
122
|
// one doesn't stay while it loads; with several sizes, the <img> stays,
|
|
126
123
|
// and the browser shows the previous image until the new one has loaded.
|
|
127
124
|
const key = srcSet ?? src
|
|
125
|
+
const priority = sourcePriority(source)
|
|
128
126
|
// The source's blurDataUri, or its ThumbHash made into one (with
|
|
129
127
|
// `react-native-fast-image/thumbhash`), shows blurred (see PreviewBlur)
|
|
130
128
|
// in place of `placeholder` (or `placeholder`'s, for a source without its
|
|
@@ -144,7 +142,7 @@ function FastImageBase({
|
|
|
144
142
|
const ok = result && result.key === key ? result.ok : undefined
|
|
145
143
|
// The same on the server and in the browser. React's ids have characters
|
|
146
144
|
// an SVG id can't (:r0: in React 18, «r0» in React 19).
|
|
147
|
-
const svgId =
|
|
145
|
+
const svgId = useId().replace(/[^\w-]/g, '')
|
|
148
146
|
const tintId = `fast-image-tint-${svgId}`
|
|
149
147
|
const blurId = `fast-image-blur-${svgId}`
|
|
150
148
|
// objectFit (the prop, then style's).
|
|
@@ -179,6 +177,10 @@ function FastImageBase({
|
|
|
179
177
|
<Picture
|
|
180
178
|
key={`default ${defaultUri}`}
|
|
181
179
|
src={defaultUri}
|
|
180
|
+
// Loaded when the image is (a data uri needs no request).
|
|
181
|
+
loading={loading}
|
|
182
|
+
priority={priority}
|
|
183
|
+
dynamicRangeLimit={dynamicRangeLimit}
|
|
182
184
|
// A source's placeholder stands for an image whose size
|
|
183
185
|
// isn't known yet: as contain for scale-down and none,
|
|
184
186
|
// which show an image at its own size.
|
|
@@ -210,7 +212,10 @@ function FastImageBase({
|
|
|
210
212
|
key={several ? 'sizes' : key}
|
|
211
213
|
src={src}
|
|
212
214
|
srcSet={srcSet}
|
|
213
|
-
|
|
215
|
+
sizes={sizes}
|
|
216
|
+
loading={loading}
|
|
217
|
+
priority={priority}
|
|
218
|
+
dynamicRangeLimit={dynamicRangeLimit}
|
|
214
219
|
fit={fit}
|
|
215
220
|
position={position}
|
|
216
221
|
filter={filter}
|
|
@@ -257,6 +262,20 @@ function thumbhashUri(hash: string | undefined): string | undefined {
|
|
|
257
262
|
return undefined
|
|
258
263
|
}
|
|
259
264
|
|
|
265
|
+
// A source's priority: one source's, or the highest of several sizes'.
|
|
266
|
+
function sourcePriority(
|
|
267
|
+
source: FastImageProps['source'],
|
|
268
|
+
): Priority | undefined {
|
|
269
|
+
const values = (Array.isArray(source) ? source : [source]).map((entry) =>
|
|
270
|
+
entry && typeof entry === 'object'
|
|
271
|
+
? (entry as Source).priority
|
|
272
|
+
: undefined,
|
|
273
|
+
)
|
|
274
|
+
return (['high', 'normal', 'low'] as const).find((priority) =>
|
|
275
|
+
values.includes(priority),
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
260
279
|
// The thumbhash or blurDataUri a source shows: its own, or `placeholder`'s
|
|
261
280
|
// (preview) if it has neither. With several sources, the one they all show.
|
|
262
281
|
function shared(
|
|
@@ -321,9 +340,9 @@ type PackagerAsset = {
|
|
|
321
340
|
// Several sizes of an image, as a srcset (each size's width, times its
|
|
322
341
|
// scale), from which the browser loads the one for the image's width in
|
|
323
342
|
// device pixels, usually the smallest at least as wide, as expo-image does.
|
|
324
|
-
// sizes="auto" has the browser use the width the image
|
|
325
|
-
// only applies to lazy images, and
|
|
326
|
-
// 100vw (the viewport's width). src is the largest, for browsers without
|
|
343
|
+
// sizes="auto" (the default sizes) has the browser use the width the image
|
|
344
|
+
// is laid out at; it only applies to lazy images, and otherwise, or in
|
|
345
|
+
// browsers without it, the next value, 100vw (the viewport's width). src is the largest, for browsers without
|
|
327
346
|
// srcset.
|
|
328
347
|
function sizedSources(sources: Source[]) {
|
|
329
348
|
const widths = sources
|
|
@@ -395,7 +414,10 @@ const fill = {
|
|
|
395
414
|
function Picture({
|
|
396
415
|
src,
|
|
397
416
|
srcSet,
|
|
398
|
-
|
|
417
|
+
sizes,
|
|
418
|
+
loading,
|
|
419
|
+
priority,
|
|
420
|
+
dynamicRangeLimit,
|
|
399
421
|
fit,
|
|
400
422
|
position,
|
|
401
423
|
filter,
|
|
@@ -406,8 +428,14 @@ function Picture({
|
|
|
406
428
|
}: {
|
|
407
429
|
src: string
|
|
408
430
|
srcSet?: string
|
|
409
|
-
//
|
|
410
|
-
|
|
431
|
+
// The props (see sizedSources for sizes).
|
|
432
|
+
sizes?: string
|
|
433
|
+
loading?: 'lazy' | 'eager'
|
|
434
|
+
// source.priority, as the <img>'s fetchpriority.
|
|
435
|
+
priority?: Priority
|
|
436
|
+
// The prop, as CSS's dynamic-range-limit (the browser's default when
|
|
437
|
+
// not set).
|
|
438
|
+
dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit'
|
|
411
439
|
fit: ObjectFit
|
|
412
440
|
// objectPosition, as CSS.
|
|
413
441
|
position: string
|
|
@@ -478,8 +506,12 @@ function Picture({
|
|
|
478
506
|
ref={image}
|
|
479
507
|
src={src}
|
|
480
508
|
srcSet={srcSet}
|
|
481
|
-
sizes={srcSet ? 'auto, 100vw' : undefined}
|
|
482
|
-
loading={
|
|
509
|
+
sizes={srcSet ? (sizes ?? 'auto, 100vw') : undefined}
|
|
510
|
+
loading={loading === 'eager' ? undefined : 'lazy'}
|
|
511
|
+
// For 'high' and 'low' (the browser's own for 'normal').
|
|
512
|
+
fetchPriority={
|
|
513
|
+
priority === 'high' || priority === 'low' ? priority : undefined
|
|
514
|
+
}
|
|
483
515
|
alt={alt}
|
|
484
516
|
draggable={false}
|
|
485
517
|
style={{
|
|
@@ -488,6 +520,8 @@ function Picture({
|
|
|
488
520
|
objectPosition: position,
|
|
489
521
|
filter,
|
|
490
522
|
opacity: hidden ? 0 : undefined,
|
|
523
|
+
// Not in React's CSS types yet.
|
|
524
|
+
...({ dynamicRangeLimit } as object),
|
|
491
525
|
}}
|
|
492
526
|
onLoad={(event) =>
|
|
493
527
|
loaded(event.currentTarget as unknown as LoadedImage)
|
|
@@ -70,6 +70,9 @@ export interface NativeProps extends ViewProps {
|
|
|
70
70
|
transitionBetweenImages?: boolean
|
|
71
71
|
transitionSkipOnCacheHit?: string
|
|
72
72
|
downsample?: CodegenTypes.WithDefault<boolean, true>
|
|
73
|
+
// standard, constrained or no-limit ('' when not set). iOS and tvOS 17+
|
|
74
|
+
// (Android ignores it).
|
|
75
|
+
dynamicRangeLimit?: string
|
|
73
76
|
blurRadius?: CodegenTypes.Float
|
|
74
77
|
// The events JS has a handler for, as bits: 1 onFastImageLoadStart,
|
|
75
78
|
// 2 onFastImageProgress, 4 onFastImageLoad, 8 onFastImageError, 16
|