react-native-fast-image 10.3.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 CHANGED
@@ -424,6 +424,20 @@ If you control the images, serve them at the size they're shown (resized on your
424
424
 
425
425
  ---
426
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
+
427
441
  #### `blurRadius`
428
442
 
429
443
  **Type:** `number` · **Default:** `0`
@@ -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
@@ -151,6 +151,7 @@ export type FastImageProps = $ReadOnly<{|
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
152
  loading?: ?('lazy' | 'eager'),
153
153
  sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
154
155
  paused?: ?boolean,
155
156
  transition?: ?(boolean | number | Transition),
156
157
  downsample?: ?boolean,
@@ -283,7 +283,7 @@ const notSupported = {
283
283
  error: "Not supported on the web"
284
284
  };
285
285
  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 }) {
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 }) {
287
287
  var _resolveUri;
288
288
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
289
289
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
@@ -321,6 +321,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
321
321
  src: defaultUri,
322
322
  loading,
323
323
  priority,
324
+ dynamicRangeLimit,
324
325
  fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
325
326
  position,
326
327
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
@@ -332,6 +333,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
332
333
  sizes,
333
334
  loading,
334
335
  priority,
336
+ dynamicRangeLimit,
335
337
  fit,
336
338
  position,
337
339
  filter,
@@ -438,7 +440,7 @@ const fill = {
438
440
  width: "100%",
439
441
  height: "100%"
440
442
  };
441
- function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter, hidden, alt, onLoad, onError }) {
443
+ function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
442
444
  const image = (0, react.useRef)(null);
443
445
  const mounted = (0, react.useRef)(true);
444
446
  const handled = (0, react.useRef)(void 0);
@@ -485,7 +487,8 @@ function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter,
485
487
  objectFit: fit,
486
488
  objectPosition: position,
487
489
  filter,
488
- opacity: hidden ? 0 : void 0
490
+ opacity: hidden ? 0 : void 0,
491
+ dynamicRangeLimit
489
492
  },
490
493
  onLoad: (event) => loaded(event.currentTarget),
491
494
  onError: (event) => {
@@ -151,6 +151,7 @@ export type FastImageProps = $ReadOnly<{|
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
152
  loading?: ?('lazy' | 'eager'),
153
153
  sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
154
155
  paused?: ?boolean,
155
156
  transition?: ?(boolean | number | Transition),
156
157
  downsample?: ?boolean,
package/dist/index.d.ts CHANGED
@@ -533,6 +533,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
533
533
  * @default true
534
534
  */
535
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';
536
555
  /**
537
556
  * Blurs the image by this radius, in points (the same radius looks about
538
557
  * the same on iOS and Android). `0` is no blur.
@@ -151,6 +151,7 @@ export type FastImageProps = $ReadOnly<{|
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
152
  loading?: ?('lazy' | 'eager'),
153
153
  sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
154
155
  paused?: ?boolean,
155
156
  transition?: ?(boolean | number | Transition),
156
157
  downsample?: ?boolean,
package/dist/index.web.js CHANGED
@@ -256,7 +256,7 @@ const notSupported = {
256
256
  error: "Not supported on the web"
257
257
  };
258
258
  const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
259
- 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 }) {
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 }) {
260
260
  var _resolveUri;
261
261
  if (typeof __DEV__ !== "undefined" && __DEV__) warnIfChildren(children);
262
262
  const { accessibilityLabel, "aria-label": ariaLabel, resizeMode: _resizeMode, ...viewProps } = props;
@@ -294,6 +294,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
294
294
  src: defaultUri,
295
295
  loading,
296
296
  priority,
297
+ dynamicRangeLimit,
297
298
  fit: blurUri && (fit === "scale-down" || fit === "none") ? "contain" : fit,
298
299
  position,
299
300
  filter: blurUri ? [`url(#${blurId})`, tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") : filter,
@@ -305,6 +306,7 @@ function FastImageBase({ source, placeholder, defaultSource, tintColor, blurRadi
305
306
  sizes,
306
307
  loading,
307
308
  priority,
309
+ dynamicRangeLimit,
308
310
  fit,
309
311
  position,
310
312
  filter,
@@ -411,7 +413,7 @@ const fill = {
411
413
  width: "100%",
412
414
  height: "100%"
413
415
  };
414
- function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter, hidden, alt, onLoad, onError }) {
416
+ function Picture({ src, srcSet, sizes, loading, priority, dynamicRangeLimit, fit, position, filter, hidden, alt, onLoad, onError }) {
415
417
  const image = useRef(null);
416
418
  const mounted = useRef(true);
417
419
  const handled = useRef(void 0);
@@ -458,7 +460,8 @@ function Picture({ src, srcSet, sizes, loading, priority, fit, position, filter,
458
460
  objectFit: fit,
459
461
  objectPosition: position,
460
462
  filter,
461
- opacity: hidden ? 0 : void 0
463
+ opacity: hidden ? 0 : void 0,
464
+ dynamicRangeLimit
462
465
  },
463
466
  onLoad: (event) => loaded(event.currentTarget),
464
467
  onError: (event) => {
@@ -151,6 +151,7 @@ export type FastImageProps = $ReadOnly<{|
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
152
  loading?: ?('lazy' | 'eager'),
153
153
  sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
154
155
  paused?: ?boolean,
155
156
  transition?: ?(boolean | number | Transition),
156
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
- // SDWebImage puts an image it decoded smaller from the disk cache, for a view
29
- // that downsamples, into the memory cache under the full-size image's key too
30
- // (SDWebImageManager writes what its original cache query finds back there),
31
- // where a full-size load (a view that doesn't downsample, or a preload) would
32
- // find it: a blurry image, or a preload's size. Drops it there, so that load
33
- // decodes the file from the disk cache. Call it before a full-size load.
34
- + (void) forgetSmallerFullSizeImageForURL: (NSURL*)url context: (SDWebImageContext*)context;
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) forgetSmallerFullSizeImageForURL: (NSURL*)url context: (SDWebImageContext*)context {
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
- CGSize fullSize = [self sourceSizeOfImage: image];
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 forgetSmallerFullSizeImageForURL:source.url context:context];
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
- UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]];
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 forgetSmallerFullSizeImageForURL: source.url context: context];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "10.3.0",
3
+ "version": "10.4.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
package/src/index.js.flow CHANGED
@@ -151,6 +151,7 @@ export type FastImageProps = $ReadOnly<{|
151
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
152
  loading?: ?('lazy' | 'eager'),
153
153
  sizes?: ?string,
154
+ dynamicRangeLimit?: ?('standard' | 'constrained' | 'no-limit'),
154
155
  paused?: ?boolean,
155
156
  transition?: ?(boolean | number | Transition),
156
157
  downsample?: ?boolean,
package/src/index.tsx CHANGED
@@ -555,6 +555,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
555
555
  * @default true
556
556
  */
557
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'
558
577
  // The radius is converted from points to the bitmap's pixels on both
559
578
  // platforms, so it matches React Native's Image.
560
579
  /**
package/src/index.web.tsx CHANGED
@@ -82,6 +82,7 @@ function FastImageBase({
82
82
  objectPosition,
83
83
  loading,
84
84
  sizes,
85
+ dynamicRangeLimit,
85
86
  forwardedRef,
86
87
  // On the wrapper, the view the layout and touches are for here (on
87
88
  // native, FastImage's only view).
@@ -179,6 +180,7 @@ function FastImageBase({
179
180
  // Loaded when the image is (a data uri needs no request).
180
181
  loading={loading}
181
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.
@@ -213,6 +215,7 @@ function FastImageBase({
213
215
  sizes={sizes}
214
216
  loading={loading}
215
217
  priority={priority}
218
+ dynamicRangeLimit={dynamicRangeLimit}
216
219
  fit={fit}
217
220
  position={position}
218
221
  filter={filter}
@@ -414,6 +417,7 @@ function Picture({
414
417
  sizes,
415
418
  loading,
416
419
  priority,
420
+ dynamicRangeLimit,
417
421
  fit,
418
422
  position,
419
423
  filter,
@@ -429,6 +433,9 @@ function Picture({
429
433
  loading?: 'lazy' | 'eager'
430
434
  // source.priority, as the <img>'s fetchpriority.
431
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'
432
439
  fit: ObjectFit
433
440
  // objectPosition, as CSS.
434
441
  position: string
@@ -513,6 +520,8 @@ function Picture({
513
520
  objectPosition: position,
514
521
  filter,
515
522
  opacity: hidden ? 0 : undefined,
523
+ // Not in React's CSS types yet.
524
+ ...({ dynamicRangeLimit } as object),
516
525
  }}
517
526
  onLoad={(event) =>
518
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