react-native-fast-image 10.3.0 → 10.5.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 CHANGED
@@ -26,7 +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
+ - **Placeholders**: a blurred preview from a [ThumbHash](#sourcethumbhash) or a [tiny image](#sourceblurdatauri) while an image loads, a bundled image, or a url, e.g. a thumbnail that's already cached ([`placeholder`](#placeholder)).
30
30
  - [**SVG**](docs/formats.md#svg-images) and [**photo library**](#photo-library-images-ios-and-tvos) images.
31
31
  - [**`FastImageBackground`**](#fastimagebackground) for content over an image.
32
32
  - **Expo and web support**, with a [config plugin](#expo-config-plugin) for Expo.
@@ -266,7 +266,7 @@ With several sources, the scale `width` and `height` are multiplied by.
266
266
 
267
267
  #### `placeholder`
268
268
 
269
- **Type:** `string | number | SourcePlaceholder`
269
+ **Type:** `string | number | PlaceholderSource | SourcePlaceholder`
270
270
 
271
271
  A placeholder shown while the image loads, and if it fails to load:
272
272
 
@@ -275,13 +275,15 @@ A placeholder shown while the image loads, and if it fails to load:
275
275
  - The name of an image bundled with the app (an iOS asset catalog image or an Android drawable): `placeholder="gray"`.
276
276
  - A [ThumbHash](#sourcethumbhash), shown blurred: `placeholder={{ thumbhash }}`.
277
277
  - A tiny image as a data uri, shown blurred: `placeholder={{ blurDataUri }}`.
278
+ - A url, loaded and cached like a source: `placeholder="https://example.com/placeholder.png"`, or a source with `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
279
+ - A url only shown if it's already cached, e.g. a list's thumbnail while the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
278
280
 
279
- A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one.
281
+ A source's own `thumbhash` or `blurDataUri` is shown instead, if it has one; a url placeholder shows over it once it has loaded.
280
282
 
281
283
  Notes:
282
284
 
283
- - A url isn't loaded: it's ignored, with a warning in development.
284
285
  - On Android, a `require()`d image doesn't show in debug builds, where the dev server serves it. Release builds show it.
286
+ - On the web, a url loads like any image, also with `'cacheOnly'`: a browser can't look only in its cache.
285
287
 
286
288
  ---
287
289
 
@@ -424,6 +426,20 @@ If you control the images, serve them at the size they're shown (resized on your
424
426
 
425
427
  ---
426
428
 
429
+ #### `dynamicRangeLimit`
430
+
431
+ **Type:** `'standard' | 'constrained' | 'no-limit'` · iOS, Web only
432
+
433
+ How bright HDR photos (e.g. photos taken on an iPhone) can be, on displays that support it:
434
+
435
+ - `'standard'`: in standard range, like the rest of the screen.
436
+ - `'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).
437
+ - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
438
+
439
+ 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`.
440
+
441
+ ---
442
+
427
443
  #### `blurRadius`
428
444
 
429
445
  **Type:** `number` · **Default:** `0`
@@ -830,6 +846,14 @@ Where the image is in the view: see [`objectPosition`](#objectposition).
830
846
 
831
847
  ---
832
848
 
849
+ ### `PlaceholderSource`
850
+
851
+ **Type:** `Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>`
852
+
853
+ A url as `placeholder`, loaded like a source (see `placeholder`).
854
+
855
+ ---
856
+
833
857
  ### `PositionLength`
834
858
 
835
859
  **Type:** `` number | `${number}%` ``
@@ -27,7 +27,7 @@ import java.util.Map;
27
27
  import javax.annotation.Nullable;
28
28
 
29
29
  class FastImageViewConverter {
30
- private static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
30
+ static final Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
31
31
 
32
32
  private static final Map<String, FastImageCacheControl> FAST_IMAGE_CACHE_CONTROL_MAP =
33
33
  new HashMap<String, FastImageCacheControl>() {{
@@ -315,9 +315,16 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
315
315
  view.setSources(sources.isNull() ? null : sources.asArray());
316
316
  }
317
317
 
318
- // The `placeholder` prop: the resolved asset's uri, or a data uri.
318
+ // The `placeholder` prop: a url to load apart from the image, as a source
319
+ // (a map), or the resolved asset's uri or a data uri (a string).
319
320
  @Override
320
321
  public void setPlaceholder(FastImageViewWithUrl view, Dynamic source) {
322
+ if (source.getType() == ReadableType.Map) {
323
+ view.setPlaceholder(null);
324
+ view.setPlaceholderSource(source.asMap());
325
+ return;
326
+ }
327
+ view.setPlaceholderSource(null);
321
328
  String uri = source.isNull() ? null : source.asString();
322
329
  if (uri != null && FastImageThumbHash.isDataUri(uri)) {
323
330
  Bitmap bitmap = FastImageThumbHash.dataUri(uri);
@@ -389,6 +396,12 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl>
389
396
  view.setDownsample(downsample);
390
397
  }
391
398
 
399
+ // iOS only: Android shows an HDR photo (a gain map) in HDR only when its
400
+ // whole window is in HDR color mode.
401
+ @Override
402
+ public void setDynamicRangeLimit(FastImageViewWithUrl view, @Nullable String dynamicRangeLimit) {
403
+ }
404
+
392
405
  // Which events JS has a handler for (FastImageEvents); with onProgress,
393
406
  // the view gets its download's progress (see onDownloadProgress).
394
407
  @Override
@@ -30,6 +30,7 @@ import androidx.annotation.RequiresApi;
30
30
  import androidx.appcompat.widget.AppCompatImageView;
31
31
 
32
32
  import com.bumptech.glide.GenericTransitionOptions;
33
+ import com.bumptech.glide.Priority;
33
34
  import com.bumptech.glide.RequestBuilder;
34
35
  import com.bumptech.glide.RequestManager;
35
36
  import com.bumptech.glide.load.DataSource;
@@ -39,6 +40,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
39
40
  import com.bumptech.glide.load.resource.gif.GifDrawable;
40
41
  import com.bumptech.glide.request.Request;
41
42
  import com.bumptech.glide.request.RequestOptions;
43
+ import com.bumptech.glide.request.target.CustomTarget;
42
44
  import com.bumptech.glide.request.target.DrawableImageViewTarget;
43
45
  import com.bumptech.glide.request.target.SizeReadyCallback;
44
46
  import com.bumptech.glide.request.transition.NoTransition;
@@ -309,6 +311,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
309
311
  }
310
312
 
311
313
  public void setPlaceholder(@Nullable Drawable placeholder) {
314
+ // None again (the placeholder prop is a url) doesn't reload.
315
+ if (placeholder == null && mPlaceholder == null) return;
312
316
  mNeedsReload = true;
313
317
  mPlaceholder = placeholder;
314
318
  }
@@ -338,6 +342,114 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
338
342
  updatePlaceholder();
339
343
  }
340
344
 
345
+ // `placeholder` as a url (a source): loaded apart from the image, at the
346
+ // most urgent priority, as it only helps if it comes before the image (or
347
+ // only from Glide's caches, with cache cacheOnly).
348
+ // Once it has, a copy shows instead of the other placeholders: FastImage's
349
+ // own drawable, which Glide doesn't recycle, so a fade can still draw it
350
+ // after its request is cleared. A failure shows nothing more.
351
+ @Nullable
352
+ private ReadableMap mPlaceholderSource = null;
353
+ @Nullable
354
+ private Drawable mUrlPlaceholder = null;
355
+ @Nullable
356
+ private CustomTarget<Drawable> mUrlPlaceholderTarget = null;
357
+
358
+ // Its load waits for the view's size (see startUrlPlaceholderLoad).
359
+ private boolean mUrlPlaceholderWaitsForSize = false;
360
+
361
+ public void setPlaceholderSource(@Nullable ReadableMap source) {
362
+ if (source == null && mPlaceholderSource == null) return;
363
+ mPlaceholderSource = source;
364
+ clearUrlPlaceholderTarget();
365
+ mUrlPlaceholderWaitsForSize = false;
366
+ mUrlPlaceholder = null;
367
+ updatePlaceholder();
368
+ scheduleUrlPlaceholderLoad();
369
+ }
370
+
371
+ // Starts placeholderSource's load once the current callbacks have run: an
372
+ // image from the memory cache has shown by then, and needs none.
373
+ private void scheduleUrlPlaceholderLoad() {
374
+ post(new Runnable() {
375
+ @Override
376
+ public void run() {
377
+ startUrlPlaceholderLoad();
378
+ }
379
+ });
380
+ }
381
+
382
+ // Loads placeholderSource's image while the image loads (or after it
383
+ // failed), at the view's size, once it has one: not if the image showed,
384
+ // or it's already loaded or loading.
385
+ private void startUrlPlaceholderLoad() {
386
+ if (requestManager == null || mDropped || mUrlPlaceholder != null || mUrlPlaceholderTarget != null
387
+ || !FastImageViewConverter.hasUri(mPlaceholderSource)) return;
388
+ if (mLoadEnded && mShownRequest != null) return;
389
+ if (getWidth() <= 0 || getHeight() <= 0) {
390
+ mUrlPlaceholderWaitsForSize = true;
391
+ return;
392
+ }
393
+ FastImageSource imageSource = FastImageViewConverter.getImageSource(getContext(), mPlaceholderSource);
394
+ if (imageSource == null) return;
395
+ CustomTarget<Drawable> target = new CustomTarget<Drawable>(getWidth(), getHeight()) {
396
+ @Override
397
+ public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
398
+ if (this != mUrlPlaceholderTarget) return;
399
+ mUrlPlaceholder = ownCopy(resource);
400
+ updatePlaceholder();
401
+ // With the copy, Glide's can go back to its pools (a load
402
+ // can't be cleared from its own callback).
403
+ final CustomTarget<Drawable> done = this;
404
+ post(new Runnable() {
405
+ @Override
406
+ public void run() {
407
+ if (requestManager != null) requestManager.clear(done);
408
+ }
409
+ });
410
+ }
411
+
412
+ @Override
413
+ public void onLoadCleared(@Nullable Drawable placeholder) {
414
+ }
415
+ };
416
+ mUrlPlaceholderTarget = target;
417
+ requestManager
418
+ .load(imageSource.getSourceForLoad())
419
+ .apply(FastImageViewConverter.getOptions(getContext(), imageSource, mPlaceholderSource)
420
+ .priority(Priority.IMMEDIATE)
421
+ // Copied on a software canvas (see ownCopy).
422
+ .disallowHardwareConfig())
423
+ .into(target);
424
+ }
425
+
426
+ private void clearUrlPlaceholderTarget() {
427
+ if (mUrlPlaceholderTarget != null && requestManager != null) requestManager.clear(mUrlPlaceholderTarget);
428
+ mUrlPlaceholderTarget = null;
429
+ }
430
+
431
+ // A bitmap of the drawable (an animated one's first frame), scaled up
432
+ // smoothly as the other placeholders are, or null for one with no size.
433
+ @Nullable
434
+ private Drawable ownCopy(Drawable drawable) {
435
+ int width = drawable.getIntrinsicWidth();
436
+ int height = drawable.getIntrinsicHeight();
437
+ if (width <= 0 || height <= 0) return null;
438
+ try {
439
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
440
+ Canvas canvas = new Canvas(bitmap);
441
+ Drawable copy = drawable.getConstantState() != null ? drawable.getConstantState().newDrawable().mutate() : drawable;
442
+ copy.setBounds(0, 0, width, height);
443
+ copy.draw(canvas);
444
+ BitmapDrawable own = new BitmapDrawable(getResources(), bitmap);
445
+ own.setFilterBitmap(true);
446
+ return own;
447
+ } catch (OutOfMemoryError | IllegalStateException e) {
448
+ // As startFade: out of memory, or a hardware bitmap after all.
449
+ return null;
450
+ }
451
+ }
452
+
341
453
  // The picked source's ThumbHash or blurDataUri, or null.
342
454
  @Nullable
343
455
  private String pickedSourcePlaceholder() {
@@ -372,10 +484,16 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
372
484
  mInvalidPlaceholder = key;
373
485
  }
374
486
  }
487
+ // placeholderSource's image, once it has loaded, shows instead.
488
+ if (mUrlPlaceholder != null) placeholder = mUrlPlaceholder;
375
489
  if (placeholder == mShownPlaceholder) return;
376
490
  boolean showing = mShowsPlaceholder;
377
491
  mShownPlaceholder = placeholder;
378
- if (showing) setImageDrawable(placeholder);
492
+ // Also one that arrives while the view shows nothing (no placeholder
493
+ // while the image loads, or after it failed): never over an image.
494
+ Drawable shown = getDrawable();
495
+ boolean showsNothing = shown == null || shown == FastImageViewConverter.TRANSPARENT_DRAWABLE;
496
+ if (showing || (placeholder != null && showsNothing)) setImageDrawable(placeholder);
379
497
  }
380
498
 
381
499
  // The events JS has a handler for (the handledEvents prop): the view only
@@ -449,6 +567,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
449
567
  void drop() {
450
568
  mDropped = true;
451
569
  clearView(requestManager);
570
+ clearUrlPlaceholderTarget();
452
571
  endProgress();
453
572
  }
454
573
 
@@ -1183,10 +1302,11 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1183
1302
  setImageMatrix(matrix);
1184
1303
  return;
1185
1304
  }
1186
- // A source's placeholder (a ThumbHash or a blurDataUri) stands for an
1187
- // image whose size isn't known yet: with scale-down and none (which
1188
- // show an image at its own size), it's shown as with contain.
1189
- boolean hashFits = drawable == mShownPlaceholder && mPlaceholderKey != null;
1305
+ // A source's placeholder (a ThumbHash or a blurDataUri), or
1306
+ // placeholderSource's image, stands for an image whose size isn't
1307
+ // known yet: with scale-down and none (which show an image at its own
1308
+ // size), it's shown as with contain.
1309
+ boolean hashFits = drawable == mShownPlaceholder && (mPlaceholderKey != null || drawable == mUrlPlaceholder);
1190
1310
  if (mLayoutScaleType == ScaleType.CENTER_CROP || mLayoutScaleType == ScaleType.FIT_CENTER || hashFits) {
1191
1311
  float x = viewWidth / drawableWidth;
1192
1312
  float y = viewHeight / drawableHeight;
@@ -1352,6 +1472,17 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1352
1472
  // The loading image loaded (FastImageRequestListener).
1353
1473
  void onImageLoaded() {
1354
1474
  mLoadEnded = true;
1475
+ // The image came first: its placeholder isn't needed (not from
1476
+ // within Glide's callback).
1477
+ if (mUrlPlaceholder == null) {
1478
+ mUrlPlaceholderWaitsForSize = false;
1479
+ post(new Runnable() {
1480
+ @Override
1481
+ public void run() {
1482
+ if (mUrlPlaceholder == null && mLoadEnded && mShownRequest != null) clearUrlPlaceholderTarget();
1483
+ }
1484
+ });
1485
+ }
1355
1486
  endProgress();
1356
1487
  // Before the view shows it (the target does once this returns).
1357
1488
  applyObjectFit();
@@ -1380,6 +1511,10 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1380
1511
  invalidate();
1381
1512
  }
1382
1513
  if (!switchSourceIfResized()) reloadIfResized();
1514
+ if (mUrlPlaceholderWaitsForSize && w > 0 && h > 0) {
1515
+ mUrlPlaceholderWaitsForSize = false;
1516
+ scheduleUrlPlaceholderLoad();
1517
+ }
1383
1518
  }
1384
1519
 
1385
1520
  private void reloadIfResized() {
@@ -1508,6 +1643,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1508
1643
  }
1509
1644
  });
1510
1645
  builder.into(target);
1646
+ if (!quiet) scheduleUrlPlaceholderLoad();
1511
1647
  }
1512
1648
 
1513
1649
  // Loading the image that's showing again (at a new size, or with another
@@ -1524,6 +1660,8 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1524
1660
  endProgress();
1525
1661
  applyObjectFit();
1526
1662
  mShownRequest = null;
1663
+ // Shows instead of the other placeholders once it loads.
1664
+ scheduleUrlPlaceholderLoad();
1527
1665
  final int load = mLoadCount;
1528
1666
  // Not from within Glide's callback. Without a thumbnail Glide shows the
1529
1667
  // placeholder the request started with: a source placeholder that changed since
@@ -1605,6 +1743,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1605
1743
 
1606
1744
  // Clear the image.
1607
1745
  setImageDrawable(null);
1746
+ scheduleUrlPlaceholderLoad();
1608
1747
  return;
1609
1748
  }
1610
1749
 
@@ -1632,6 +1771,7 @@ class FastImageViewWithUrl extends AppCompatImageView implements ReactPointerEve
1632
1771
 
1633
1772
  endProgress();
1634
1773
  setImageDrawable(mShownPlaceholder);
1774
+ scheduleUrlPlaceholderLoad();
1635
1775
  return;
1636
1776
  }
1637
1777
 
package/dist/index.cjs.js CHANGED
@@ -261,8 +261,14 @@ var FastImageViewNativeComponent_default = (0, react_native.codegenNativeCompone
261
261
  var NativeFastImageModule_default = react_native.TurboModuleRegistry.get("FastImageModule");
262
262
  //#endregion
263
263
  //#region src/placeholders.ts
264
+ const SCHEME = /^[a-z][a-z\d+.-]*:/i;
264
265
  function isImageName(value) {
265
- return !/^[a-z][a-z\d+.-]*:/i.test(value) && !value.startsWith("/");
266
+ return !SCHEME.test(value) && !value.startsWith("/");
267
+ }
268
+ function placeholderSource(value) {
269
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
270
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
271
+ return typeof value === "string" ? { uri } : value;
266
272
  }
267
273
  function hasSourcePlaceholder(value) {
268
274
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
@@ -270,7 +276,7 @@ function hasSourcePlaceholder(value) {
270
276
  function dataUri(value) {
271
277
  if (typeof value !== "string" || value === "") return void 0;
272
278
  if (/^data:/i.test(value)) return value;
273
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `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.`, "warn");
279
+ if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
274
280
  }
275
281
  //#endregion
276
282
  //#region src/index.tsx
@@ -348,7 +354,7 @@ function builtInTransition() {
348
354
  }
349
355
  let defaultTransition;
350
356
  function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadStart, onProgress, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loop, transition, loading: _loading, sizes: _sizes, forwardedRef, ...viewProps }) {
351
- var _props$recyclingKey, _defaultTransition;
357
+ var _placeholderSource, _props$recyclingKey, _defaultTransition;
352
358
  const { fallback, resizeMode: _resizeMode, ...props } = viewProps;
353
359
  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");
354
360
  if (__DEV__) warnIfChildren(children);
@@ -366,7 +372,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, onLoadSt
366
372
  sourcePlaceholders: placeholders.some(Boolean) ? placeholders : void 0
367
373
  };
368
374
  }, [source, shared]);
369
- const resolvedPlaceholder = resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
375
+ const resolvedPlaceholder = (_placeholderSource = placeholderSource(placeholder)) !== null && _placeholderSource !== void 0 ? _placeholderSource : resolvePlaceholder(hasSourcePlaceholder(placeholder) ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
370
376
  return /* @__PURE__ */ react.default.createElement(FastImageViewNativeComponent_default, {
371
377
  ...props,
372
378
  ref: forwardedRef,
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
148
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
@@ -151,6 +158,7 @@ export type FastImageProps = $ReadOnly<{|
151
158
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
159
  loading?: ?('lazy' | 'eager'),
153
160
  sizes?: ?string,
161
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
154
162
  paused?: ?boolean,
155
163
  transition?: ?(boolean | number | Transition),
156
164
  downsample?: ?boolean,
@@ -268,13 +268,18 @@ function cssPosition({ x, y }) {
268
268
  }
269
269
  //#endregion
270
270
  //#region src/placeholders.ts
271
+ function placeholderSource(value) {
272
+ const uri = typeof value === "string" ? value : value && typeof value === "object" ? value.uri : void 0;
273
+ if (typeof uri !== "string" || !/^https?:/i.test(uri)) return void 0;
274
+ return typeof value === "string" ? { uri } : value;
275
+ }
271
276
  function hasSourcePlaceholder(value) {
272
277
  return !!value && typeof value === "object" && ("thumbhash" in value || "blurDataUri" in value);
273
278
  }
274
279
  function dataUri(value) {
275
280
  if (typeof value !== "string" || value === "") return void 0;
276
281
  if (/^data:/i.test(value)) return value;
277
- if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `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.`, "warn");
282
+ if (typeof __DEV__ !== "undefined" && __DEV__) warnOnce("placeholder-url", `react-native-fast-image: \`blurDataUri\` takes a data uri (data:image/…): "${value.slice(0, 60)}" is ignored. For a placeholder loaded from a url, use \`placeholder\`.`, "warn");
278
283
  }
279
284
  //#endregion
280
285
  //#region src/index.web.tsx
@@ -283,8 +288,8 @@ const notSupported = {
283
288
  error: "Not supported on the web"
284
289
  };
285
290
  const useClientLayoutEffect = globalThis.document === void 0 ? react.useEffect : react.useLayoutEffect;
286
- function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, objectPosition, loading, sizes, forwardedRef, onLayout, pointerEvents, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
287
- var _resolveUri;
291
+ 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 }) {
292
+ var _resolveUri, _placeholderSource;
288
293
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
289
294
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
290
295
  const label = ariaLabel || accessibilityLabel || void 0;
@@ -296,7 +301,9 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
296
301
  const priority = sourcePriority(source);
297
302
  const preview = hasSourcePlaceholder(placeholder) ? placeholder : void 0;
298
303
  const blurUri = (_resolveUri = resolveUri(dataUri(shared(source, "blurDataUri", preview)))) !== null && _resolveUri !== void 0 ? _resolveUri : thumbhashUri(shared(source, "thumbhash", preview));
299
- const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
304
+ const urlPlaceholder = (_placeholderSource = placeholderSource(placeholder)) === null || _placeholderSource === void 0 ? void 0 : _placeholderSource.uri;
305
+ const [failedPlaceholder, setFailedPlaceholder] = (0, react.useState)();
306
+ const defaultUri = blurUri !== null && blurUri !== void 0 ? blurUri : resolveUri(preview || urlPlaceholder ? defaultSource : placeholder !== null && placeholder !== void 0 ? placeholder : defaultSource);
300
307
  const [result, setResult] = (0, react.useState)();
301
308
  const ok = result && result.key === key ? result.ok : void 0;
302
309
  const svgId = (0, react.useId)().replace(/[^\w-]/g, "");
@@ -305,7 +312,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
305
312
  const fit = resolveObjectFit(objectFit, style);
306
313
  const position = cssPosition(objectPositionOrCentered(objectPosition));
307
314
  const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
308
- const showDefault = !!defaultUri && ok !== true;
315
+ const showDefault = ok !== true;
309
316
  useClientLayoutEffect(() => {
310
317
  if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
311
318
  }, [key]);
@@ -321,10 +328,23 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
321
328
  src: defaultUri,
322
329
  loading,
323
330
  priority,
331
+ dynamicRangeLimit,
324
332
  fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
325
333
  position,
326
334
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
327
335
  alt: key ? "" : label !== null && label !== void 0 ? label : ""
336
+ }) : null, urlPlaceholder && showDefault ? /* @__PURE__ */ react.default.createElement(Picture, {
337
+ key: `placeholder ${urlPlaceholder}`,
338
+ src: urlPlaceholder,
339
+ loading,
340
+ priority: "high",
341
+ dynamicRangeLimit,
342
+ fit: fit === "scale-down" || fit === "none" ? "contain" : fit,
343
+ position,
344
+ filter: tintColor ? `url(#${tintId})` : void 0,
345
+ hidden: failedPlaceholder === urlPlaceholder,
346
+ onError: () => setFailedPlaceholder(urlPlaceholder),
347
+ alt: ""
328
348
  }) : null, key && src ? /* @__PURE__ */ react.default.createElement(Picture, {
329
349
  key: several ? "sizes" : key,
330
350
  src,
@@ -332,6 +352,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
332
352
  sizes,
333
353
  loading,
334
354
  priority,
355
+ dynamicRangeLimit,
335
356
  fit,
336
357
  position,
337
358
  filter,
@@ -438,7 +459,7 @@ const fill = {
438
459
  width: "100%",
439
460
  height: "100%"
440
461
  };
441
- function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter, hidden, alt, onLoad, onError }) {
462
+ function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
442
463
  const image = (0, react.useRef)(null);
443
464
  const mounted = (0, react.useRef)(true);
444
465
  const handled = (0, react.useRef)(void 0);
@@ -485,7 +506,8 @@ function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter,
485
506
  objectFit: fit,
486
507
  objectPosition: position,
487
508
  filter,
488
- opacity: hidden ? 0 : void 0
509
+ opacity: hidden ? 0 : void 0,
510
+ dynamicRangeLimit
489
511
  },
490
512
  onLoad: (event) => loaded(event.currentTarget),
491
513
  onError: (event) => {
@@ -110,6 +110,13 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type PlaceholderSource = {
114
+ uri?: string,
115
+ headers?: Object,
116
+ cacheKey?: string,
117
+ cache?: CacheControls,
118
+ }
119
+
113
120
  export type SourcePlaceholder = {
114
121
  thumbhash?: string,
115
122
  blurDataUri?: string,
@@ -139,7 +146,7 @@ export type FastImageProps = $ReadOnly<{|
139
146
  onProgress?: ?(event: OnProgressEvent) => void,
140
147
 
141
148
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
- placeholder?: ?(number | string | SourcePlaceholder),
149
+ placeholder?: ?(number | string | SourcePlaceholder | PlaceholderSource),
143
150
  // Deprecated: use placeholder, which is the same.
144
151
  defaultSource?: ?number,
145
152
 
@@ -151,6 +158,7 @@ export type FastImageProps = $ReadOnly<{|
151
158
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
159
  loading?: ?('lazy' | 'eager'),
153
160
  sizes?: ?string,
161
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
154
162
  paused?: ?boolean,
155
163
  transition?: ?(boolean | number | Transition),
156
164
  downsample?: ?boolean,
package/dist/index.d.ts CHANGED
@@ -154,6 +154,8 @@ export type Source = {
154
154
  };
155
155
  /** A `thumbhash` or `blurDataUri` as `placeholder`. */
156
156
  export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>;
157
+ /** A url as `placeholder`, loaded like a source (see `placeholder`). */
158
+ export type PlaceholderSource = Pick<Source, 'uri' | 'headers' | 'cacheKey' | 'cache'>;
157
159
  /** How the image fades in: see [`transition`](#transition). */
158
160
  export type Transition = {
159
161
  /**
@@ -343,17 +345,23 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
343
345
  * `placeholder={{ thumbhash }}`.
344
346
  * - A tiny image as a data uri, shown blurred:
345
347
  * `placeholder={{ blurDataUri }}`.
348
+ * - A url, loaded and cached like a source:
349
+ * `placeholder="https://example.com/placeholder.png"`, or a source with
350
+ * `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
351
+ * - A url only shown if it's already cached, e.g. a list's thumbnail while
352
+ * the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
346
353
  *
347
354
  * A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
348
- * one.
355
+ * one; a url placeholder shows over it once it has loaded.
349
356
  *
350
357
  * Notes:
351
358
  *
352
- * - A url isn't loaded: it's ignored, with a warning in development.
353
359
  * - On Android, a `require()`d image doesn't show in debug builds, where
354
360
  * the dev server serves it. Release builds show it.
361
+ * - On the web, a url loads like any image, also with `'cacheOnly'`: a
362
+ * browser can't look only in its cache.
355
363
  */
356
- placeholder?: ImageRequireSource | string | SourcePlaceholder;
364
+ placeholder?: ImageRequireSource | string | SourcePlaceholder | PlaceholderSource;
357
365
  /**
358
366
  * @deprecated Use `placeholder`, which does the same.
359
367
  */
@@ -533,6 +541,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
533
541
  * @default true
534
542
  */
535
543
  downsample?: boolean;
544
+ /**
545
+ * How bright HDR photos (e.g. photos taken on an iPhone) can be, on
546
+ * displays that support it:
547
+ *
548
+ * - `'standard'`: in standard range, like the rest of the screen.
549
+ * - `'constrained'`: in HDR, their highlights brighter than the rest of
550
+ * the screen but held below the display's peak, e.g. for images among
551
+ * other content (a feed).
552
+ * - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
553
+ *
554
+ * Not set, iOS shows them in standard range, and the web as the browser
555
+ * does by default (usually HDR). On iOS and tvOS 17 and later; Android
556
+ * shows them in standard range. On the web it's CSS's
557
+ * `dynamic-range-limit`.
558
+ *
559
+ * @platform ios
560
+ * @platform web
561
+ */
562
+ dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit';
536
563
  /**
537
564
  * Blurs the image by this radius, in points (the same radius looks about
538
565
  * the same on iOS and Android). `0` is no blur.