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 +14 -0
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +6 -0
- package/dist/index.cjs.js.flow +1 -0
- package/dist/index.cjs.web.js +6 -3
- package/dist/index.cjs.web.js.flow +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js.flow +1 -0
- package/dist/index.web.js +6 -3
- package/dist/index.web.js.flow +1 -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 +1 -0
- package/src/index.tsx +19 -0
- package/src/index.web.tsx +9 -0
- package/src/specs/FastImageViewNativeComponent.ts +3 -0
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
|
package/dist/index.cjs.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/dist/index.cjs.web.js
CHANGED
|
@@ -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.
|
package/dist/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/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) => {
|
package/dist/index.web.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,
|
|
@@ -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
|
@@ -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
|