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.
@@ -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"
@@ -69,10 +70,23 @@
69
70
  // shownPlaceholder).
70
71
  @property(nonatomic, strong) UIImage* hashImage;
71
72
  @property(nonatomic, copy) NSString* hashImageKey;
72
- // Whether the view shows its placeholder (the placeholder prop or the
73
- // source's), and whether that's the source's (hashImage).
73
+ // placeholderSource's image once it has loaded, or nil: it shows instead of
74
+ // the other placeholders (see shownPlaceholder). Its load, and which it is.
75
+ @property(nonatomic, strong) UIImage* urlPlaceholderImage;
76
+ @property(nonatomic, strong) id<SDWebImageOperation> urlPlaceholderOperation;
77
+ @property(nonatomic, assign) NSUInteger urlPlaceholderLoad;
78
+ // placeholderSource changed: its load starts with the image's, or after the
79
+ // props if the image is still loading (see startUrlPlaceholderLoad).
80
+ @property(nonatomic, assign) BOOL needsUrlPlaceholderLoad;
81
+ // Whether the view shows a placeholder (the placeholder prop, the source's,
82
+ // or placeholderSource's), and whether it stands for the image (the source's
83
+ // or placeholderSource's): drawn as with contain for scale-down and none, as
84
+ // the image's size isn't known yet, scaled smoothly and not blurred.
74
85
  @property(nonatomic, assign) BOOL showsPlaceholder;
75
86
  @property(nonatomic, assign) BOOL showsHash;
87
+ // dynamicRangeLimit switched between HDR and SDR: the image is decoded again,
88
+ // quietly (see reloadForDynamicRange).
89
+ @property(nonatomic, assign) BOOL needsDynamicRangeReload;
76
90
 
77
91
  @end
78
92
 
@@ -440,7 +454,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
440
454
 
441
455
  - (void) setImage: (UIImage*)image {
442
456
  self.imageCount++;
443
- self.showsHash = image != nil && image == self.hashImage;
457
+ self.showsHash = image != nil && (image == self.hashImage || image == self.urlPlaceholderImage);
444
458
  self.showsPlaceholder = self.showsHash || (image != nil && image == _placeholder);
445
459
  if ([_imageRendering isEqualToString: @"pixelated"]) {
446
460
  // A source's placeholder is a blurred preview, scaled up smoothly
@@ -536,7 +550,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
536
550
  // Whether the image is blurred: a loaded image, not the placeholder prop (as
537
551
  // with React Native's Image, and on Android).
538
552
  - (BOOL) blurs: (UIImage*)image {
539
- return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage;
553
+ return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage && image != self.urlPlaceholderImage;
540
554
  }
541
555
 
542
556
  // Blurs the image off the main thread, and shows it (tinted as any image) unless
@@ -643,6 +657,14 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
643
657
  if (image.size.width <= 0 || image.size.height <= 0) {
644
658
  return image;
645
659
  }
660
+ // Blurred in standard range (the bitmap below has 8 bits per channel): an
661
+ // HDR image (dynamicRangeLimit) tone mapped to it, rather than its
662
+ // highlights clipped.
663
+ if (@available(iOS 17.0, tvOS 17.0, *)) {
664
+ if (image.isHighDynamicRange) {
665
+ image = [image imageRestrictedToStandardDynamicRange];
666
+ }
667
+ }
646
668
  double fullWidth = MAX(1, round(image.size.width * image.scale * scale));
647
669
  double fullHeight = MAX(1, round(image.size.height * image.scale * scale));
648
670
  // At most the image's larger side (a larger box looks the same, and an
@@ -798,6 +820,33 @@ NSString *FFFErrorMessage(NSError *error)
798
820
  }
799
821
  }
800
822
 
823
+ - (void) setDynamicRangeLimit: (NSString*)dynamicRangeLimit {
824
+ if (_dynamicRangeLimit == dynamicRangeLimit || [_dynamicRangeLimit isEqualToString: dynamicRangeLimit]) {
825
+ return;
826
+ }
827
+ BOOL decodedToHDR = [self decodesToHDR];
828
+ _dynamicRangeLimit = [dynamicRangeLimit copy];
829
+ if (@available(iOS 17.0, tvOS 17.0, *)) {
830
+ // Constrained: the highlights brighter than SDR white, but not as
831
+ // bright as the display can go, as Apple suggests for images among
832
+ // other content.
833
+ self.preferredImageDynamicRange = [_dynamicRangeLimit isEqualToString: @"no-limit"] ? UIImageDynamicRangeHigh
834
+ : [_dynamicRangeLimit isEqualToString: @"constrained"] ? UIImageDynamicRangeConstrainedHigh
835
+ : [_dynamicRangeLimit isEqualToString: @"standard"] ? UIImageDynamicRangeStandard
836
+ : UIImageDynamicRangeUnspecified;
837
+ }
838
+ // Decoded again only to switch between HDR and SDR.
839
+ if ([self decodesToHDR] != decodedToHDR) {
840
+ self.needsDynamicRangeReload = YES;
841
+ }
842
+ }
843
+
844
+ // Whether HDR images are decoded to HDR (dynamicRangeLimit constrained or
845
+ // no-limit), rather than to SDR, their standard range version.
846
+ - (BOOL) decodesToHDR {
847
+ return [_dynamicRangeLimit isEqualToString: @"constrained"] || [_dynamicRangeLimit isEqualToString: @"no-limit"];
848
+ }
849
+
801
850
  - (void) setPlaceholder: (UIImage*)placeholder {
802
851
  if (_placeholder != placeholder) {
803
852
  _placeholder = placeholder;
@@ -810,10 +859,124 @@ NSString *FFFErrorMessage(NSError *error)
810
859
  [self updateHashImage];
811
860
  }
812
861
 
813
- // What shows while the image loads and if it fails: the picked source's
814
- // placeholder (its ThumbHash or blurDataUri), or the placeholder prop.
862
+ // What shows while the image loads and if it fails: placeholderSource's
863
+ // image once it has loaded, the picked source's placeholder (its ThumbHash or
864
+ // blurDataUri), or the placeholder prop.
815
865
  - (UIImage*) shownPlaceholder {
816
- return self.hashImage ?: _placeholder;
866
+ return self.urlPlaceholderImage ?: self.hashImage ?: _placeholder;
867
+ }
868
+
869
+ // Shows the placeholder that shows now (shownPlaceholder) in place of
870
+ // `previous` if the view showed it, or if it showed nothing (one that arrived
871
+ // while nothing showed): never over a loaded image, or the image kept while
872
+ // a new source loads.
873
+ - (void) showChangedPlaceholder: (UIImage*)previous showing: (BOOL)showing {
874
+ UIImage* placeholder = [self shownPlaceholder];
875
+ if (placeholder == previous) {
876
+ return;
877
+ }
878
+ BOOL showsNothing = super.image == nil && !self.showsLoadedImage && !self.blursOverNothing;
879
+ if (showing || (placeholder && showsNothing)) {
880
+ [self setImage: placeholder];
881
+ }
882
+ }
883
+
884
+ // Images that views showed, by their source's cacheKey or url, while they're
885
+ // still in memory (held by a view or SDWebImage's memory cache), so
886
+ // placeholderSource finds one at once whatever size it was decoded at (the
887
+ // memory cache's key includes the size).
888
+ + (NSMapTable<NSString*, UIImage*>*) shownImages {
889
+ static NSMapTable* images;
890
+ static dispatch_once_t once;
891
+ dispatch_once(&once, ^{
892
+ images = [NSMapTable strongToWeakObjectsMapTable];
893
+ });
894
+ return images;
895
+ }
896
+
897
+ // The key a remote image (http or https) is recorded under: its cacheKey or
898
+ // url. nil for others (data uris, files), which aren't recorded.
899
+ + (NSString*) shownImageKey: (FFFastImageSource*)source {
900
+ NSString* scheme = source.url.scheme.lowercaseString;
901
+ if (![scheme isEqualToString: @"http"] && ![scheme isEqualToString: @"https"]) {
902
+ return nil;
903
+ }
904
+ return source.cacheKey.length > 0 ? source.cacheKey : source.url.absoluteString;
905
+ }
906
+
907
+ + (void) rememberShownImage: (UIImage*)image source: (FFFastImageSource*)source {
908
+ NSString* key = [self shownImageKey: source];
909
+ if (image && key) {
910
+ [[self shownImages] setObject: image forKey: key];
911
+ }
912
+ }
913
+
914
+ - (void) setPlaceholderSource: (FFFastImageSource*)placeholderSource {
915
+ _placeholderSource = placeholderSource;
916
+ [self cancelUrlPlaceholderLoad];
917
+ [self showUrlPlaceholderImage: nil];
918
+ self.needsUrlPlaceholderLoad = YES;
919
+ }
920
+
921
+ - (void) cancelUrlPlaceholderLoad {
922
+ self.urlPlaceholderLoad++;
923
+ [self.urlPlaceholderOperation cancel];
924
+ self.urlPlaceholderOperation = nil;
925
+ }
926
+
927
+ // Loads placeholderSource's image with the image's load (after layout, so at
928
+ // the view's size, with the props applied), or after the image failed: not
929
+ // if the image shows at once (from the memory cache, or a data uri), or the
930
+ // placeholder is already loaded or loading.
931
+ - (void) startUrlPlaceholderLoad {
932
+ self.needsUrlPlaceholderLoad = NO;
933
+ if (!_placeholderSource.url || self.urlPlaceholderImage || self.urlPlaceholderOperation) {
934
+ return;
935
+ }
936
+ if (_source && !self.hasErrored && [self loadsDuringLayout]) {
937
+ return;
938
+ }
939
+ [self loadUrlPlaceholder];
940
+ }
941
+
942
+ // Loads placeholderSource's image, as a source would load (its cache,
943
+ // headers and cacheKey), at a high priority, as it only helps if it comes
944
+ // before the image. One a view still shows (or that's still in memory) shows
945
+ // at once. A failure shows nothing more.
946
+ - (void) loadUrlPlaceholder {
947
+ [self cancelUrlPlaceholderLoad];
948
+ NSUInteger load = self.urlPlaceholderLoad;
949
+ FFFastImageSource* source = _placeholderSource;
950
+ NSString* key = [FFFastImageView shownImageKey: source];
951
+ UIImage* shown = key ? [[FFFastImageView shownImages] objectForKey: key] : nil;
952
+ if (shown) {
953
+ [self showUrlPlaceholderImage: shown];
954
+ return;
955
+ }
956
+ SDWebImageOptions options = [source cacheOptions] | SDWebImageHighPriority | SDWebImageRetryFailed | SDWebImageHandleCookies;
957
+ SDWebImageContext* context = [self contextForSource: source box: [self decodeBox] cover: NO];
958
+ __weak typeof(self) weakSelf = self;
959
+ self.urlPlaceholderOperation = [[SDWebImageManager sharedManager] loadImageWithURL: source.url options: options context: context progress: nil completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
960
+ if (!finished || weakSelf.urlPlaceholderLoad != load) {
961
+ return;
962
+ }
963
+ weakSelf.urlPlaceholderOperation = nil;
964
+ if (!image) {
965
+ return;
966
+ }
967
+ [FFFastImageView rememberShownImage: image source: source];
968
+ [weakSelf showUrlPlaceholderImage: image];
969
+ }];
970
+ }
971
+
972
+ - (void) showUrlPlaceholderImage: (UIImage*)image {
973
+ if (self.urlPlaceholderImage == image) {
974
+ return;
975
+ }
976
+ UIImage* previous = [self shownPlaceholder];
977
+ BOOL showing = self.showsPlaceholder;
978
+ self.urlPlaceholderImage = image;
979
+ [self showChangedPlaceholder: previous showing: showing];
817
980
  }
818
981
 
819
982
  // The picked source's ThumbHash or blurDataUri, or nil.
@@ -837,10 +1000,7 @@ NSString *FFFErrorMessage(NSError *error)
837
1000
  BOOL showing = self.showsPlaceholder;
838
1001
  self.hashImageKey = hash;
839
1002
  self.hashImage = [FFFThumbHash imageForPlaceholder: hash];
840
- UIImage* placeholder = [self shownPlaceholder];
841
- if (showing && placeholder != previous) {
842
- [self setImage: placeholder];
843
- }
1003
+ [self showChangedPlaceholder: previous showing: showing];
844
1004
  }
845
1005
 
846
1006
  - (void) didSetProps {
@@ -865,9 +1025,42 @@ NSString *FFFErrorMessage(NSError *error)
865
1025
  return;
866
1026
  }
867
1027
  [self reloadImage];
1028
+ } else if (self.needsDynamicRangeReload) {
1029
+ [self reloadForDynamicRange];
868
1030
  } else {
869
1031
  [self reloadIfResized];
870
1032
  }
1033
+ // A new placeholderSource while the image loads, or after it failed (a
1034
+ // load waiting for the view's size starts it with the image's).
1035
+ if (self.needsUrlPlaceholderLoad && !_needsReload && !self.waitsForSize) {
1036
+ if (!self.hasCompleted || self.hasErrored) {
1037
+ [self startUrlPlaceholderLoad];
1038
+ }
1039
+ self.needsUrlPlaceholderLoad = NO;
1040
+ }
1041
+ }
1042
+
1043
+ // The same image decoded again for a dynamicRangeLimit between HDR and SDR,
1044
+ // as a resize is (reloadIfResized): without events once it has loaded, and
1045
+ // the image showing stays if the load fails. A display setting shouldn't
1046
+ // look like a new load.
1047
+ - (void) reloadForDynamicRange {
1048
+ self.needsDynamicRangeReload = NO;
1049
+ // Nothing to load, it failed, or it waits for the view's size: the next
1050
+ // load decodes as asked.
1051
+ if (!_source || self.hasErrored || self.waitsForSize) {
1052
+ return;
1053
+ }
1054
+ // A data uri is decoded at once, with its events (see reloadImage).
1055
+ if ([[_source.url absoluteString] hasPrefix: @"data:image"]) {
1056
+ [self reloadImage];
1057
+ return;
1058
+ }
1059
+ SDWebImageOptions options = [self loadOptions];
1060
+ if (self.showsLoadedImage) {
1061
+ options |= SDWebImageDelayPlaceholder;
1062
+ }
1063
+ [self downloadImage: _source options: options context: [self loadContext] events: !self.hasCompleted];
871
1064
  }
872
1065
 
873
1066
  // Whether images are decoded at about the view's size (downsample, and always
@@ -938,6 +1131,8 @@ NSString *FFFErrorMessage(NSError *error)
938
1131
 
939
1132
  - (void) reloadImage {
940
1133
  _needsReload = NO;
1134
+ self.needsDynamicRangeReload = NO;
1135
+ self.needsUrlPlaceholderLoad = NO;
941
1136
  self.waitsForSize = NO;
942
1137
  self.loadsSwitchedSource = self.switchesSource;
943
1138
  self.switchesSource = NO;
@@ -961,13 +1156,19 @@ NSString *FFFErrorMessage(NSError *error)
961
1156
  self.onFastImageLoadStart(@{});
962
1157
  }
963
1158
  // Use SDWebImage API to support external format like WebP images
964
- UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]];
1159
+ // (as sd_imageWithData does), decoded to HDR or SDR as a
1160
+ // download is (see contextForSource).
1161
+ UIImage* image = [[SDImageCodersManager sharedManager] decodedImageWithData: [NSData dataWithContentsOfURL: _source.url] options: @{
1162
+ SDImageCoderDecodeScaleFactor: @1,
1163
+ SDImageCoderDecodeToHDR: @([self decodesToHDR]),
1164
+ }];
965
1165
  if (!image) {
966
1166
  // Not decodable: fail like a remote image, showing the
967
1167
  // placeholder.
968
1168
  [self setImage: [self shownPlaceholder]];
969
1169
  self.showsLoadedImage = NO;
970
1170
  [self sendOnError: @"Failed to decode the image"];
1171
+ [self startUrlPlaceholderLoad];
971
1172
  if (self.onFastImageLoadEnd) {
972
1173
  self.onFastImageLoadEnd([self loadEndEvent: NO]);
973
1174
  }
@@ -1010,10 +1211,15 @@ NSString *FFFErrorMessage(NSError *error)
1010
1211
  self.retriedAfterBackground = NO;
1011
1212
  [self stopWaitingForActive];
1012
1213
 
1214
+ [self startUrlPlaceholderLoad];
1013
1215
  [self downloadImage: _source options: options context: [self loadContext] events: YES];
1014
- } else if ([self shownPlaceholder]) {
1015
- [self setImage: [self shownPlaceholder]];
1016
- self.showsLoadedImage = NO;
1216
+ } else {
1217
+ if ([self shownPlaceholder]) {
1218
+ [self setImage: [self shownPlaceholder]];
1219
+ self.showsLoadedImage = NO;
1220
+ }
1221
+ // No image: only placeholders show.
1222
+ [self startUrlPlaceholderLoad];
1017
1223
  }
1018
1224
  }
1019
1225
 
@@ -1070,10 +1276,15 @@ NSString *FFFErrorMessage(NSError *error)
1070
1276
  context[SDWebImageContextDownloadRequestModifier] = source.requestModifier;
1071
1277
  context[SDWebImageContextImageLoader] = source.imageLoader;
1072
1278
  context[SDWebImageContextCacheKeyFilter] = source.cacheKeyFilter;
1279
+ if (@available(iOS 17.0, tvOS 17.0, *)) {
1280
+ // An HDR image (e.g. a photo with a gain map) decoded to HDR, or to
1281
+ // SDR, its standard range version.
1282
+ context[SDWebImageContextImageDecodeToHDR] = @([self decodesToHDR]);
1283
+ }
1073
1284
  if (CGSizeEqualToSize(box, CGSizeZero)) {
1074
1285
  [FFFDownsampledImage addFullSizeToContext: context];
1075
1286
  [self applyCacheOptions: context source: source fullSize: YES];
1076
- [FFFDownsampledImage forgetSmallerFullSizeImageForURL: source.url context: context];
1287
+ [FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
1077
1288
  return context;
1078
1289
  }
1079
1290
  [FFFDownsampledImage addToContext: context box: box cover: cover];
@@ -1085,6 +1296,7 @@ NSString *FFFErrorMessage(NSError *error)
1085
1296
  context[SDWebImageContextOriginalQueryCacheType] = @(SDImageCacheTypeNone);
1086
1297
  }
1087
1298
  [self applyCacheOptions: context source: source fullSize: NO];
1299
+ [FFFDownsampledImage forgetUnusableImageForURL: source.url context: context];
1088
1300
  return context;
1089
1301
  }
1090
1302
 
@@ -1243,6 +1455,8 @@ NSString *FFFErrorMessage(NSError *error)
1243
1455
  [weakSelf setImage: current];
1244
1456
  }
1245
1457
  weakSelf.showsLoadedImage = NO;
1458
+ // Shows instead of the other placeholders once it loads.
1459
+ [weakSelf startUrlPlaceholderLoad];
1246
1460
  [weakSelf sendOnError: [source errorMessage: error]];
1247
1461
  if (weakSelf.onFastImageLoadEnd) {
1248
1462
  weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: NO]);
@@ -1250,6 +1464,12 @@ NSString *FFFErrorMessage(NSError *error)
1250
1464
  } else {
1251
1465
  weakSelf.hasCompleted = YES;
1252
1466
  weakSelf.showsLoadedImage = YES;
1467
+ // For other views' placeholderSource.
1468
+ [FFFastImageView rememberShownImage: image source: source];
1469
+ // The image came first: its placeholder isn't needed.
1470
+ if (!weakSelf.urlPlaceholderImage) {
1471
+ [weakSelf cancelUrlPlaceholderLoad];
1472
+ }
1253
1473
  [weakSelf sendOnLoad: image];
1254
1474
  if (weakSelf.onFastImageLoadEnd) {
1255
1475
  weakSelf.onFastImageLoadEnd([weakSelf loadEndEvent: YES]);
@@ -1299,11 +1519,14 @@ NSString *FFFErrorMessage(NSError *error)
1299
1519
  self.waitsForSize = NO;
1300
1520
  [self stopWaitingForActive];
1301
1521
  [self sd_cancelLatestImageLoad];
1522
+ [self cancelUrlPlaceholderLoad];
1302
1523
  }
1303
1524
 
1304
1525
  - (void) dealloc {
1305
1526
  [self stopWaitingForActive];
1306
1527
  [self sd_cancelLatestImageLoad];
1528
+ // Fabric frees views it doesn't recycle without stop.
1529
+ [_urlPlaceholderOperation cancel];
1307
1530
  }
1308
1531
 
1309
1532
  @end
@@ -102,6 +102,22 @@ static id _Nullable FFFJSON(const folly::dynamic &value)
102
102
  return value.isNull() ? nil : FFFObjectFromDynamic(value);
103
103
  }
104
104
 
105
+ // Whether the placeholder prop is a url to load apart from the image (a
106
+ // source with an http or https uri), rather than an image to show at once: a
107
+ // bundled image (served by the packager in development), a file or a data uri.
108
+ static BOOL FFFIsUrlPlaceholder(id _Nullable placeholder)
109
+ {
110
+ if (![placeholder isKindOfClass:[NSDictionary class]] || placeholder[@"__packager_asset"]) {
111
+ return NO;
112
+ }
113
+ id uri = placeholder[@"uri"];
114
+ if (![uri isKindOfClass:[NSString class]]) {
115
+ return NO;
116
+ }
117
+ NSString *scheme = [NSURL URLWithString:uri].scheme.lowercaseString;
118
+ return [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"];
119
+ }
120
+
105
121
  // The events JS has a handler for: the handledEvents prop's bits (see
106
122
  // src/specs/FastImageViewNativeComponent.ts).
107
123
  typedef NS_OPTIONS(int32_t, FFFEvent) {
@@ -234,7 +250,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
234
250
  }
235
251
  if (all || oldViewProps.placeholder != newViewProps.placeholder) {
236
252
  id placeholder = FFFJSON(newViewProps.placeholder);
237
- view.placeholder = placeholder ? [RCTConvert UIImage:placeholder] : nil;
253
+ BOOL loads = FFFIsUrlPlaceholder(placeholder);
254
+ view.placeholder = placeholder && !loads ? [RCTConvert UIImage:placeholder] : nil;
255
+ view.placeholderSource = loads ? [RCTConvert FFFastImageSource:placeholder] : nil;
238
256
  }
239
257
  if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
240
258
  NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
@@ -287,6 +305,9 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
287
305
  if (all || oldViewProps.downsample != newViewProps.downsample) {
288
306
  view.downsample = newViewProps.downsample;
289
307
  }
308
+ if (all || oldViewProps.dynamicRangeLimit != newViewProps.dynamicRangeLimit) {
309
+ view.dynamicRangeLimit = FFFString(newViewProps.dynamicRangeLimit);
310
+ }
290
311
  if (all || oldViewProps.blurRadius != newViewProps.blurRadius) {
291
312
  view.blurRadius = newViewProps.blurRadius;
292
313
  }
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.5.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
package/src/index.js.flow CHANGED
@@ -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/src/index.tsx CHANGED
@@ -16,7 +16,12 @@ import { fromStyle, resolveObjectFit } from './objectFit'
16
16
  import { objectPositionOrCentered, type ObjectPosition } from './objectPosition'
17
17
  import FastImageView from './specs/FastImageViewNativeComponent'
18
18
  import NativeFastImageModule from './specs/NativeFastImageModule'
19
- import { dataUri, hasSourcePlaceholder, isImageName } from './placeholders'
19
+ import {
20
+ dataUri,
21
+ hasSourcePlaceholder,
22
+ isImageName,
23
+ placeholderSource,
24
+ } from './placeholders'
20
25
  import { warnIfChildren, warnOnce } from './warnings'
21
26
 
22
27
  /** How the image fits the view: see [`objectFit`](#objectfit). */
@@ -175,6 +180,12 @@ export type Source = {
175
180
  /** A `thumbhash` or `blurDataUri` as `placeholder`. */
176
181
  export type SourcePlaceholder = Pick<Source, 'thumbhash' | 'blurDataUri'>
177
182
 
183
+ /** A url as `placeholder`, loaded like a source (see `placeholder`). */
184
+ export type PlaceholderSource = Pick<
185
+ Source,
186
+ 'uri' | 'headers' | 'cacheKey' | 'cache'
187
+ >
188
+
178
189
  /** How the image fades in: see [`transition`](#transition). */
179
190
  export type Transition = {
180
191
  /**
@@ -365,17 +376,27 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
365
376
  * `placeholder={{ thumbhash }}`.
366
377
  * - A tiny image as a data uri, shown blurred:
367
378
  * `placeholder={{ blurDataUri }}`.
379
+ * - A url, loaded and cached like a source:
380
+ * `placeholder="https://example.com/placeholder.png"`, or a source with
381
+ * `headers` or a `cacheKey`: `placeholder={{ uri, headers }}`.
382
+ * - A url only shown if it's already cached, e.g. a list's thumbnail while
383
+ * the full-size image loads: `placeholder={{ uri, cache: 'cacheOnly' }}`.
368
384
  *
369
385
  * A source's own `thumbhash` or `blurDataUri` is shown instead, if it has
370
- * one.
386
+ * one; a url placeholder shows over it once it has loaded.
371
387
  *
372
388
  * Notes:
373
389
  *
374
- * - A url isn't loaded: it's ignored, with a warning in development.
375
390
  * - On Android, a `require()`d image doesn't show in debug builds, where
376
391
  * the dev server serves it. Release builds show it.
392
+ * - On the web, a url loads like any image, also with `'cacheOnly'`: a
393
+ * browser can't look only in its cache.
377
394
  */
378
- placeholder?: ImageRequireSource | string | SourcePlaceholder
395
+ placeholder?:
396
+ | ImageRequireSource
397
+ | string
398
+ | SourcePlaceholder
399
+ | PlaceholderSource
379
400
  /**
380
401
  * @deprecated Use `placeholder`, which does the same.
381
402
  */
@@ -555,6 +576,25 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
555
576
  * @default true
556
577
  */
557
578
  downsample?: boolean
579
+ /**
580
+ * How bright HDR photos (e.g. photos taken on an iPhone) can be, on
581
+ * displays that support it:
582
+ *
583
+ * - `'standard'`: in standard range, like the rest of the screen.
584
+ * - `'constrained'`: in HDR, their highlights brighter than the rest of
585
+ * the screen but held below the display's peak, e.g. for images among
586
+ * other content (a feed).
587
+ * - `'no-limit'`: in full HDR, e.g. for a full-screen photo viewer.
588
+ *
589
+ * Not set, iOS shows them in standard range, and the web as the browser
590
+ * does by default (usually HDR). On iOS and tvOS 17 and later; Android
591
+ * shows them in standard range. On the web it's CSS's
592
+ * `dynamic-range-limit`.
593
+ *
594
+ * @platform ios
595
+ * @platform web
596
+ */
597
+ dynamicRangeLimit?: 'standard' | 'constrained' | 'no-limit'
558
598
  // The radius is converted from points to the bitmap's pixels on both
559
599
  // platforms, so it matches React Native's Image.
560
600
  /**
@@ -687,7 +727,8 @@ function sourcePlaceholder(source: unknown): string {
687
727
  }
688
728
 
689
729
  const resolvePlaceholder = (
690
- placeholder?: ImageRequireSource | string,
730
+ // A source object here has no url (e.g. { uri: name }, a bundled image).
731
+ placeholder?: ImageRequireSource | string | PlaceholderSource,
691
732
  ): string | object | null => {
692
733
  if (typeof placeholder === 'string') {
693
734
  // An image bundled with the app, by name, as React Native's Image
@@ -706,7 +747,9 @@ const resolvePlaceholder = (
706
747
  if (!placeholder) {
707
748
  return null
708
749
  }
709
- const resolved = Image.resolveAssetSource(placeholder)
750
+ const resolved = Image.resolveAssetSource(
751
+ placeholder as ImageRequireSource | { uri: string },
752
+ )
710
753
  if (!resolved) {
711
754
  return null
712
755
  }
@@ -904,13 +947,17 @@ function FastImageBase({
904
947
  : undefined,
905
948
  }
906
949
  }, [source, shared])
907
- // A thumbhash or blurDataUri in `placeholder` goes in sourcePlaceholders
908
- // instead: it isn't an image to show as it is, but defaultSource still is.
909
- const resolvedPlaceholder = resolvePlaceholder(
910
- hasSourcePlaceholder(placeholder)
911
- ? defaultSource
912
- : (placeholder ?? defaultSource),
913
- )
950
+ // A url in `placeholder` goes to the native view as a source, loaded
951
+ // apart from the image, in place of defaultSource. A thumbhash or
952
+ // blurDataUri goes in sourcePlaceholders instead: it isn't an image to
953
+ // show as it is, but defaultSource still is.
954
+ const resolvedPlaceholder =
955
+ placeholderSource(placeholder) ??
956
+ resolvePlaceholder(
957
+ hasSourcePlaceholder(placeholder)
958
+ ? defaultSource
959
+ : (placeholder ?? defaultSource),
960
+ )
914
961
 
915
962
  return (
916
963
  <FastImageView