react-native-fast-image 8.12.0 → 8.13.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
@@ -202,6 +202,16 @@ Pauses an animated image (GIF, and animated WebP on iOS) on the frame it's showi
202
202
 
203
203
  ---
204
204
 
205
+ ### `downsample?: boolean`
206
+
207
+ iOS only. Decodes a large image at about the size it's shown at, instead of at full size, so it takes much less memory.
208
+
209
+ Use it when you show images much larger than their views and can't get them at the right size, e.g. user uploads or other people's URLs in a list. If you control the images, serve them at the size they're shown instead (resized on your server or by an image CDN), which also saves bandwidth.
210
+
211
+ Decoding a smaller copy can take a little longer, so use it where the memory matters. Needs SDWebImage 5.19.7 or later. On Android images are already decoded at about the view's size.
212
+
213
+ ---
214
+
205
215
  ### `onLoadStart?: () => void`
206
216
 
207
217
  Called when the image starts to load.
@@ -92,6 +92,7 @@ export type FastImageProps = $ReadOnly<{|
92
92
  loop?: ?(boolean | number),
93
93
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
94
94
  paused?: ?boolean,
95
+ downsample?: ?boolean,
95
96
  testID?: ?string,
96
97
  |}>
97
98
 
package/dist/index.d.ts CHANGED
@@ -94,6 +94,13 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
94
94
  * its own, so pausing one doesn't pause others showing the same file.
95
95
  */
96
96
  paused?: boolean;
97
+ /**
98
+ * iOS only. Decodes a large image at about the size it's shown at, so it
99
+ * takes much less memory. For images much larger than their views that
100
+ * you can't get at the right size (if you can, resize them on your server
101
+ * or with an image CDN instead). See the README.
102
+ */
103
+ downsample?: boolean;
97
104
  onLoadStart?(): void;
98
105
  onProgress?(event: OnProgressEvent): void;
99
106
  onLoad?(event: OnLoadEvent): void;
@@ -92,6 +92,7 @@ export type FastImageProps = $ReadOnly<{|
92
92
  loop?: ?(boolean | number),
93
93
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
94
94
  paused?: ?boolean,
95
+ downsample?: ?boolean,
95
96
  testID?: ?string,
96
97
  |}>
97
98
 
@@ -0,0 +1,28 @@
1
+ #import <SDWebImage/SDAnimatedImage.h>
2
+ #import <SDWebImage/SDWebImageDefine.h>
3
+
4
+ // An image decoded at about the size it's shown at (the `downsample`
5
+ // prop), instead of at full size. The view asks for it with SDWebImage's
6
+ // thumbnail context: the box to fill (the view's size in pixels), and
7
+ // preserveAspectRatio NO when the image has to cover the box (resizeMode
8
+ // cover or stretch) rather than fit in it (contain or center). SDWebImage's
9
+ // own thumbnail decoding can only fit, so this class works out the size from
10
+ // the image's header before it decodes, as React Native's Image does.
11
+ @interface FFFDownsampledImage : SDAnimatedImage
12
+
13
+ // The url to load a source's url with, when it's downsampled.
14
+ + (NSURL*) loadURLForURL: (NSURL*)url;
15
+
16
+ // Sets a load's context to decode the image for a box (in pixels) to cover
17
+ // or fit in.
18
+ + (void) addToContext: (SDWebImageMutableContext*)context forURL: (NSURL*)url box: (CGSize)box cover: (BOOL)cover;
19
+
20
+ // The size of the full image (in points, like UIImage's size) that an image
21
+ // loaded this way was decoded from, for onLoad, or zero for other images.
22
+ + (CGSize) sourceSizeOfImage: (UIImage*)image;
23
+
24
+ // Whether this version of SDWebImage can decode through this class: it
25
+ // decodes static images through the animated image class since 5.19.
26
+ + (BOOL) isSupported;
27
+
28
+ @end
@@ -0,0 +1,169 @@
1
+ #import "FFFDownsampledImage.h"
2
+ #import <ImageIO/ImageIO.h>
3
+ #import <SDWebImage/SDImageCoder.h>
4
+ #import <SDWebImage/SDImageCacheDefine.h>
5
+ #import <SDWebImage/SDWebImageCacheKeyFilter.h>
6
+ #import <SDWebImage/UIImage+Metadata.h>
7
+
8
+ // The decode option that carries the full image's size to the view.
9
+ static SDImageCoderOption const FFFDecodeSourceSize = @"FFFDecodeSourceSize";
10
+ // Added to the url's fragment, which isn't sent to the server.
11
+ static NSString* const FFFDownsampledFragment = @"fastimage-downsampled";
12
+
13
+ // The image's size in pixels as it's shown (EXIF orientations 5 to 8 turn it
14
+ // sideways), from its header, or zero if ImageIO can't read it.
15
+ static CGSize FFFPixelSize(NSData* data) {
16
+ CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef) data, NULL);
17
+ if (!source) {
18
+ return CGSizeZero;
19
+ }
20
+ NSDictionary* properties = (__bridge_transfer NSDictionary*) CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
21
+ CFRelease(source);
22
+ CGFloat width = [properties[(__bridge NSString*) kCGImagePropertyPixelWidth] doubleValue];
23
+ CGFloat height = [properties[(__bridge NSString*) kCGImagePropertyPixelHeight] doubleValue];
24
+ NSUInteger orientation = [properties[(__bridge NSString*) kCGImagePropertyOrientation] unsignedIntegerValue];
25
+ if (orientation >= kCGImagePropertyOrientationLeftMirrored) {
26
+ return CGSizeMake(height, width);
27
+ }
28
+ return CGSizeMake(width, height);
29
+ }
30
+
31
+ // Where the decode puts the full image's size, passed in the load's decode
32
+ // options. SDWebImage attaches those options to the image it ends up with
33
+ // (sd_decodeOptions), also when its force decoding replaces the decoded image
34
+ // with another one, which drops anything kept on the image itself.
35
+ @interface FFFSourceSize : NSObject
36
+ @property (atomic, assign) CGSize size;
37
+ @end
38
+
39
+ @implementation FFFSourceSize
40
+
41
+ // Any two are equal: it's where the result goes, not part of the request, so
42
+ // the same request from several views is still decoded once (SDWebImage's
43
+ // downloader groups them by their decode options).
44
+ - (BOOL) isEqual: (id)object {
45
+ return [object isKindOfClass: [FFFSourceSize class]];
46
+ }
47
+
48
+ - (NSUInteger) hash {
49
+ return 0;
50
+ }
51
+
52
+ @end
53
+
54
+ @implementation FFFDownsampledImage
55
+
56
+ + (BOOL) isSupported {
57
+ static BOOL supported = NO;
58
+ static dispatch_once_t once;
59
+ dispatch_once(&once, ^{
60
+ // No animated image coder takes JPEG, so before SDWebImage 5.19
61
+ // SDAnimatedImage returned nil for one, and SDWebImage decoded it with
62
+ // its thumbnail options as they are (stretched to the box).
63
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
64
+ CGContextRef context = CGBitmapContextCreate(NULL, 1, 1, 8, 4, colorSpace, kCGImageAlphaNoneSkipLast);
65
+ CGColorSpaceRelease(colorSpace);
66
+ CGImageRef pixel = context ? CGBitmapContextCreateImage(context) : NULL;
67
+ if (context) {
68
+ CGContextRelease(context);
69
+ }
70
+ if (!pixel) {
71
+ return;
72
+ }
73
+ NSMutableData* jpeg = [NSMutableData data];
74
+ CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef) jpeg, CFSTR("public.jpeg"), 1, NULL);
75
+ if (destination) {
76
+ CGImageDestinationAddImage(destination, pixel, NULL);
77
+ CGImageDestinationFinalize(destination);
78
+ CFRelease(destination);
79
+ }
80
+ CGImageRelease(pixel);
81
+ supported = jpeg.length > 0 && [[SDAnimatedImage alloc] initWithData: jpeg scale: 1 options: nil] != nil;
82
+ });
83
+ return supported;
84
+ }
85
+
86
+ + (NSURL*) loadURLForURL: (NSURL*)url {
87
+ // SDWebImage's downloader shares a download between loads of the same
88
+ // url, and decodes each load's image with the first load's image class.
89
+ // After a load that isn't downsampled (e.g. a preload), this class
90
+ // wouldn't be used, and the box would be decoded as SDWebImage does (to
91
+ // fill it, stretched). With its own url, a downsampled load only shares
92
+ // downloads with other downsampled ones.
93
+ NSURLComponents* components = [NSURLComponents componentsWithURL: url resolvingAgainstBaseURL: NO];
94
+ if (!components) {
95
+ return url;
96
+ }
97
+ components.fragment = components.fragment.length > 0
98
+ ? [components.fragment stringByAppendingFormat: @"-%@", FFFDownsampledFragment]
99
+ : FFFDownsampledFragment;
100
+ return components.URL ?: url;
101
+ }
102
+
103
+ + (void) addToContext: (SDWebImageMutableContext*)context forURL: (NSURL*)url box: (CGSize)box cover: (BOOL)cover {
104
+ context[SDWebImageContextAnimatedImageClass] = [FFFDownsampledImage class];
105
+ // Cached under the url itself (see loadURLForURL:), so the disk cache
106
+ // keeps one download for every size and for loads that aren't downsampled.
107
+ NSString* key = url.absoluteString;
108
+ context[SDWebImageContextCacheKeyFilter] = [SDWebImageCacheKeyFilter cacheKeyFilterWithBlock: ^NSString* (NSURL* _Nonnull loadURL) {
109
+ return key;
110
+ }];
111
+ // The memory cache key is the url with these (SDWebImage's thumbnail
112
+ // key), so views of other sizes don't get this image.
113
+ context[SDWebImageContextImageThumbnailPixelSize] = [NSValue valueWithCGSize: box];
114
+ context[SDWebImageContextImagePreserveAspectRatio] = @(!cover);
115
+ context[SDWebImageContextImageDecodeOptions] = @{FFFDecodeSourceSize: [FFFSourceSize new]};
116
+ // Only in memory: the disk cache has the downloaded file.
117
+ context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeMemory);
118
+ context[SDWebImageContextQueryCacheType] = @(SDImageCacheTypeMemory);
119
+ }
120
+
121
+ + (CGSize) sourceSizeOfImage: (UIImage*)image {
122
+ id sourceSize = image.sd_decodeOptions[FFFDecodeSourceSize];
123
+ return [sourceSize isKindOfClass: [FFFSourceSize class]] ? ((FFFSourceSize*) sourceSize).size : CGSizeZero;
124
+ }
125
+
126
+ - (instancetype) initWithData: (NSData*)data scale: (CGFloat)scale options: (SDImageCoderOptions*)options {
127
+ NSValue* boxValue = options[SDImageCoderDecodeThumbnailPixelSize];
128
+ CGSize box = boxValue ? boxValue.CGSizeValue : CGSizeZero;
129
+ NSNumber* preserveAspectRatio = options[SDImageCoderDecodePreserveAspectRatio];
130
+ BOOL cover = preserveAspectRatio != nil && !preserveAspectRatio.boolValue;
131
+ CGSize pixelSize = FFFPixelSize(data);
132
+
133
+ // Decode at full size unless the image is at least twice as large as it
134
+ // needs to be (or its size can't be read). Decoding smaller is done in
135
+ // software, while newer iPhones decode JPEG and HEIC at full size in
136
+ // hardware: on an iPhone 15 Pro Max a 12 MP JPEG took about 20 ms at full
137
+ // size, and 120-130 ms with a peak of 100-125 MB decoded to 60-90% of its
138
+ // size (1-6 MB and 27-40 ms for a thumbnail).
139
+ NSMutableDictionary* decodeOptions = options ? [options mutableCopy] : [NSMutableDictionary dictionary];
140
+ [decodeOptions removeObjectForKey: SDImageCoderDecodeThumbnailPixelSize];
141
+ decodeOptions[SDImageCoderDecodePreserveAspectRatio] = @YES;
142
+ if (box.width > 0 && box.height > 0 && pixelSize.width > 0 && pixelSize.height > 0) {
143
+ CGFloat widthRatio = box.width / pixelSize.width;
144
+ CGFloat heightRatio = box.height / pixelSize.height;
145
+ CGFloat ratio = cover ? MAX(widthRatio, heightRatio) : MIN(widthRatio, heightRatio);
146
+ if (ratio <= 0.5) {
147
+ // ImageIO takes the longest side (kCGImageSourceThumbnailMaxPixelSize).
148
+ // SDWebImage works it out from the box with the image's stored
149
+ // width and height, before EXIF orientation, so a square box is
150
+ // the one that gives this side for any orientation.
151
+ CGFloat longest = ceil(MAX(pixelSize.width, pixelSize.height) * ratio);
152
+ decodeOptions[SDImageCoderDecodeThumbnailPixelSize] = [NSValue valueWithCGSize: CGSizeMake(longest, longest)];
153
+ }
154
+ }
155
+
156
+ self = [super initWithData: data scale: scale options: decodeOptions];
157
+ if (self) {
158
+ CGFloat imageScale = MAX(scale, 1);
159
+ FFFSourceSize* sourceSize = options[FFFDecodeSourceSize];
160
+ if ([sourceSize isKindOfClass: [FFFSourceSize class]]) {
161
+ sourceSize.size = pixelSize.width > 0 && pixelSize.height > 0
162
+ ? CGSizeMake(pixelSize.width / imageScale, pixelSize.height / imageScale)
163
+ : self.size;
164
+ }
165
+ }
166
+ return self;
167
+ }
168
+
169
+ @end
@@ -30,6 +30,8 @@
30
30
  @property (nonatomic, copy) NSString *imageRendering;
31
31
  // Pauses animated images on the frame they're showing.
32
32
  @property (nonatomic, assign) BOOL paused;
33
+ // Decodes images at about the view's size instead of at full size.
34
+ @property (nonatomic, assign) BOOL downsample;
33
35
 
34
36
  @end
35
37
 
@@ -3,6 +3,7 @@
3
3
  #import <SDWebImage/UIView+WebCache.h>
4
4
  #import <React/RCTUtils.h>
5
5
  #import <SDWebImage/SDWebImageError.h>
6
+ #import "FFFDownsampledImage.h"
6
7
 
7
8
  @interface FFFastImageView ()
8
9
 
@@ -25,6 +26,14 @@
25
26
  // Whether the view shows an image that loaded (not defaultSource or nothing).
26
27
  // A new source then keeps it until the new image has loaded (see reloadImage).
27
28
  @property(nonatomic, assign) BOOL showsLoadedImage;
29
+ // downsample: a load waits for the view's size (see didSetProps).
30
+ @property(nonatomic, assign) BOOL waitsForSize;
31
+ // The size (in pixels) the image showing or loading was decoded for, and
32
+ // whether it covers it; zero for a full-size image.
33
+ @property(nonatomic, assign) CGSize decodedBox;
34
+ @property(nonatomic, assign) BOOL decodedCover;
35
+ // Counts loads, so a quiet reload's completion can tell if it's still current.
36
+ @property(nonatomic, assign) NSUInteger loadCount;
28
37
 
29
38
  @end
30
39
 
@@ -140,6 +149,13 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
140
149
  - (void) layoutSubviews {
141
150
  [super layoutSubviews];
142
151
  [self updateContentMode];
152
+ if (self.waitsForSize) {
153
+ if ([self hasSize]) {
154
+ [self reloadImage];
155
+ }
156
+ } else {
157
+ [self reloadIfResized];
158
+ }
143
159
  }
144
160
 
145
161
  - (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd {
@@ -254,9 +270,14 @@ NSString *FFFErrorMessage(NSError *error)
254
270
  }
255
271
 
256
272
  - (void) sendOnLoad: (UIImage*)image {
273
+ // The full image's size, also when it was decoded smaller.
274
+ CGSize size = [FFFDownsampledImage sourceSizeOfImage: image];
275
+ if (CGSizeEqualToSize(size, CGSizeZero)) {
276
+ size = image.size;
277
+ }
257
278
  self.onLoadEvent = @{
258
- @"width": [NSNumber numberWithDouble: image.size.width],
259
- @"height": [NSNumber numberWithDouble: image.size.height]
279
+ @"width": [NSNumber numberWithDouble: size.width],
280
+ @"height": [NSNumber numberWithDouble: size.height]
260
281
  };
261
282
  if (self.onFastImageLoad) {
262
283
  self.onFastImageLoad(self.onLoadEvent);
@@ -294,12 +315,95 @@ NSString *FFFErrorMessage(NSError *error)
294
315
 
295
316
  - (void) didSetProps: (NSArray<NSString*>*)changedProps {
296
317
  if (_needsReload) {
318
+ // With downsample on, the image is decoded for the view's size, so
319
+ // a view that hasn't been laid out yet loads once it has. Props and
320
+ // layout are applied in the same update, so that's before the next
321
+ // frame (in layoutSubviews). A view that still has no size then
322
+ // (e.g. one sized from onLoad) loads at full size.
323
+ if ([self downsamples] && ![self hasSize]) {
324
+ if (!self.waitsForSize) {
325
+ self.waitsForSize = YES;
326
+ __weak typeof(self) weakSelf = self;
327
+ dispatch_async(dispatch_get_main_queue(), ^{
328
+ if (weakSelf.waitsForSize) {
329
+ [weakSelf reloadImage];
330
+ }
331
+ });
332
+ }
333
+ return;
334
+ }
297
335
  [self reloadImage];
336
+ } else {
337
+ [self reloadIfResized];
338
+ }
339
+ }
340
+
341
+ // Whether images are decoded at about the view's size (downsample).
342
+ // Not for `repeat`, which tiles the image at its own size, or SDWebImage
343
+ // before 5.19.
344
+ - (BOOL) downsamples {
345
+ return _downsample && _resizeMode != RCTResizeModeRepeat && [FFFDownsampledImage isSupported];
346
+ }
347
+
348
+ // Whether the view has been laid out with an area. One that's 0 wide or tall
349
+ // (e.g. sized from onLoad) has no size to decode the image for.
350
+ - (BOOL) hasSize {
351
+ return self.bounds.size.width > 0 && self.bounds.size.height > 0;
352
+ }
353
+
354
+ // The size in pixels to decode the image for, or zero for full size.
355
+ - (CGSize) decodeBox {
356
+ if (![self downsamples] || ![self hasSize]) {
357
+ return CGSizeZero;
298
358
  }
359
+ CGFloat scale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
360
+ CGSize size = self.bounds.size;
361
+ return CGSizeMake(ceil(size.width * scale), ceil(size.height * scale));
362
+ }
363
+
364
+ // Whether the image has to cover the box, rather than fit in it.
365
+ - (BOOL) decodeCovers {
366
+ return _resizeMode == RCTResizeModeCover || _resizeMode == RCTResizeModeStretch;
367
+ }
368
+
369
+ // The view grew (by more than a fifth, as React Native's Image reloads), or
370
+ // now needs a covering or full-size image: loads the image again for its
371
+ // size, keeping the current one until then. It comes from the disk cache
372
+ // (the downloaded file is kept there whatever size it's decoded at).
373
+ - (void) reloadIfResized {
374
+ if (!_source || _needsReload || self.hasErrored || CGSizeEqualToSize(self.decodedBox, CGSizeZero)) {
375
+ return;
376
+ }
377
+ // Already at full size (it's no larger than the view it was decoded for).
378
+ UIImage* image = self.untintedImage ?: super.image;
379
+ if (self.hasCompleted && CGSizeEqualToSize(image.size, [FFFDownsampledImage sourceSizeOfImage: image])) {
380
+ return;
381
+ }
382
+ CGSize box = [self decodeBox];
383
+ BOOL cover = [self decodeCovers];
384
+ if ([self downsamples]) {
385
+ if (![self hasSize]) {
386
+ return;
387
+ }
388
+ BOOL grew = box.width > self.decodedBox.width * 1.2 || box.height > self.decodedBox.height * 1.2;
389
+ if (!grew && (self.decodedCover || !cover)) {
390
+ return;
391
+ }
392
+ }
393
+ SDWebImageOptions options = [self loadOptions];
394
+ if (self.showsLoadedImage) {
395
+ options |= SDWebImageDelayPlaceholder;
396
+ }
397
+ // Without events once it has loaded: it's the same image. If it's still
398
+ // loading, it restarts for the new size, and sends its events as usual.
399
+ BOOL events = !self.hasCompleted;
400
+ [self downloadImage: _source options: options context: [self loadContext] events: events];
299
401
  }
300
402
 
301
403
  - (void) reloadImage {
302
404
  _needsReload = NO;
405
+ self.waitsForSize = NO;
406
+ self.decodedBox = CGSizeZero;
303
407
 
304
408
  if (_source) {
305
409
  // Load base64 images.
@@ -340,33 +444,7 @@ NSString *FFFErrorMessage(NSError *error)
340
444
  return;
341
445
  }
342
446
 
343
- // Set headers.
344
- SDWebImageContext* context = @{SDWebImageContextDownloadRequestModifier: _source.requestModifier};
345
-
346
- // Set priority.
347
- SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
348
- switch (_source.priority) {
349
- case FFFPriorityLow:
350
- options |= SDWebImageLowPriority;
351
- break;
352
- case FFFPriorityNormal:
353
- // Priority is normal by default.
354
- break;
355
- case FFFPriorityHigh:
356
- options |= SDWebImageHighPriority;
357
- break;
358
- }
359
-
360
- switch (_source.cacheControl) {
361
- case FFFCacheControlWeb:
362
- options |= SDWebImageRefreshCached;
363
- break;
364
- case FFFCacheControlCacheOnly:
365
- options |= SDWebImageFromCacheOnly;
366
- break;
367
- case FFFCacheControlImmutable:
368
- break;
369
- }
447
+ SDWebImageOptions options = [self loadOptions];
370
448
 
371
449
  // Keep showing the loaded image until the new one has loaded, instead
372
450
  // of clearing it to defaultSource (or nothing) while it loads, which
@@ -387,20 +465,66 @@ NSString *FFFErrorMessage(NSError *error)
387
465
  self.retriedAfterBackground = NO;
388
466
  [self stopWaitingForActive];
389
467
 
390
- [self downloadImage: _source options: options context: context];
468
+ [self downloadImage: _source options: options context: [self loadContext] events: YES];
391
469
  } else if (_defaultSource) {
392
470
  [self setImage: _defaultSource];
393
471
  self.showsLoadedImage = NO;
394
472
  }
395
473
  }
396
474
 
397
- - (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context {
475
+ - (SDWebImageOptions) loadOptions {
476
+ SDWebImageOptions options = SDWebImageRetryFailed | SDWebImageHandleCookies;
477
+ switch (_source.priority) {
478
+ case FFFPriorityLow:
479
+ options |= SDWebImageLowPriority;
480
+ break;
481
+ case FFFPriorityNormal:
482
+ // Priority is normal by default.
483
+ break;
484
+ case FFFPriorityHigh:
485
+ options |= SDWebImageHighPriority;
486
+ break;
487
+ }
488
+
489
+ switch (_source.cacheControl) {
490
+ case FFFCacheControlWeb:
491
+ options |= SDWebImageRefreshCached;
492
+ break;
493
+ case FFFCacheControlCacheOnly:
494
+ options |= SDWebImageFromCacheOnly;
495
+ break;
496
+ case FFFCacheControlImmutable:
497
+ break;
498
+ }
499
+ return options;
500
+ }
501
+
502
+ // Headers, and the size to decode at (see FFFDownsampledImage), which it
503
+ // records as decodedBox.
504
+ - (SDWebImageContext*) loadContext {
505
+ SDWebImageMutableContext* context = [NSMutableDictionary dictionary];
506
+ context[SDWebImageContextDownloadRequestModifier] = _source.requestModifier;
507
+ CGSize box = [self decodeBox];
508
+ self.decodedBox = box;
509
+ self.decodedCover = [self decodeCovers];
510
+ if (CGSizeEqualToSize(box, CGSizeZero)) {
511
+ context[SDWebImageContextAnimatedImageClass] = [SDAnimatedImage class];
512
+ return context;
513
+ }
514
+ [FFFDownsampledImage addToContext: context forURL: _source.url box: box cover: self.decodedCover];
515
+ return context;
516
+ }
517
+
518
+ // events: NO for a quiet reload at a new size (see reloadIfResized), which
519
+ // sends no events and keeps the current image if it fails.
520
+ - (void) downloadImage: (FFFastImageSource*)source options: (SDWebImageOptions)options context: (SDWebImageContext*)context events: (BOOL)events {
398
521
  __weak typeof(self) weakSelf = self; // Always use a weak reference to self in blocks
522
+ NSUInteger load = ++self.loadCount;
399
523
  // Most images have no onProgress, so only ask SDWebImage for progress when
400
524
  // there's a handler as the load starts. A handler added while loading is
401
525
  // used from the next load.
402
526
  SDImageLoaderProgressBlock progress = nil;
403
- if (self.onFastImageProgress) {
527
+ if (events && self.onFastImageProgress) {
404
528
  progress = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL* _Nullable targetURL) {
405
529
  // Without a Content-Length the total is unknown (-1 or 0), and a
406
530
  // percentage can't be worked out from it, so don't send those.
@@ -423,15 +547,49 @@ NSString *FFFErrorMessage(NSError *error)
423
547
  };
424
548
  }
425
549
  CFTimeInterval startedAt = CACurrentMediaTime();
426
- [self sd_setImageWithURL: _source.url
427
- placeholderImage: _defaultSource
428
- options: options
429
- context: context
430
- progress: progress
431
- completed: ^(UIImage* _Nullable image,
550
+ NSURL* url = context[SDWebImageContextImageThumbnailPixelSize] ? [FFFDownsampledImage loadURLForURL: source.url] : source.url;
551
+ if (!events) {
552
+ // Only this load's image: SDWebImage would clear the view (to the
553
+ // placeholder) if it fails, and a cancelled load can still complete.
554
+ SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
555
+ if (image && weakSelf.loadCount == load) {
556
+ weakSelf.image = image;
557
+ }
558
+ };
559
+ [self sd_internalSetImageWithURL: url
560
+ placeholderImage: nil
561
+ options: options
562
+ context: context
563
+ setImageBlock: setImage
564
+ progress: nil
565
+ completed: ^(UIImage* _Nullable image, NSData* _Nullable data, NSError* _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL* _Nullable imageURL) {
566
+ if (error && weakSelf.loadCount == load) {
567
+ // Keeps the image it has, without trying again on every
568
+ // layout (e.g. while offline).
569
+ weakSelf.decodedBox = CGSizeZero;
570
+ }
571
+ }];
572
+ return;
573
+ }
574
+ // Like SDAnimatedImageView's sd_setImageWithURL, which sets the image
575
+ // class to SDAnimatedImage (loadContext sets it).
576
+ [self sd_internalSetImageWithURL: url
577
+ placeholderImage: _defaultSource
578
+ options: options
579
+ context: context
580
+ setImageBlock: nil
581
+ progress: progress
582
+ completed: ^(UIImage* _Nullable image,
583
+ NSData* _Nullable data,
432
584
  NSError* _Nullable error,
433
585
  SDImageCacheType cacheType,
586
+ BOOL finished,
434
587
  NSURL* _Nullable imageURL) {
588
+ // Restarted for a new size (reloadIfResized): the new load
589
+ // sends the events.
590
+ if (weakSelf.loadCount != load && weakSelf.source == source) {
591
+ return;
592
+ }
435
593
  // The download was running when the app went to the
436
594
  // background, and failed: iOS suspends it there, and its
437
595
  // timeout keeps counting, so it times out as the app comes
@@ -468,7 +626,7 @@ NSString *FFFErrorMessage(NSError *error)
468
626
  // nil in app extensions, which don't get these notifications.
469
627
  UIApplication* application = RCTSharedApplication();
470
628
  if (!application || application.applicationState == UIApplicationStateActive) {
471
- [self downloadImage: source options: options context: context];
629
+ [self downloadImage: source options: options context: context events: YES];
472
630
  return;
473
631
  }
474
632
  [self stopWaitingForActive];
@@ -479,7 +637,7 @@ NSString *FFFErrorMessage(NSError *error)
479
637
  usingBlock: ^(NSNotification* notification) {
480
638
  [weakSelf stopWaitingForActive];
481
639
  if (weakSelf.source == source) {
482
- [weakSelf downloadImage: source options: options context: context];
640
+ [weakSelf downloadImage: source options: options context: context events: YES];
483
641
  }
484
642
  }];
485
643
  }
@@ -21,6 +21,7 @@ RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
21
21
  RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
22
22
  RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
23
23
  RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
24
+ RCT_EXPORT_VIEW_PROPERTY(downsample, BOOL)
24
25
  RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
25
26
  RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
26
27
  RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.12.0",
3
+ "version": "8.13.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",