react-native-fast-image 10.1.1 → 10.3.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.
Files changed (35) hide show
  1. package/README.md +107 -14
  2. package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
  3. package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
  4. package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
  5. package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
  6. package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
  7. package/dist/index.cjs.js +47 -9
  8. package/dist/index.cjs.js.flow +11 -0
  9. package/dist/index.cjs.web.js +67 -14
  10. package/dist/index.cjs.web.js.flow +11 -0
  11. package/dist/index.d.ts +77 -13
  12. package/dist/index.js +48 -10
  13. package/dist/index.js.flow +11 -0
  14. package/dist/index.web.js +68 -15
  15. package/dist/index.web.js.flow +11 -0
  16. package/dist/placeholders.d.ts +4 -0
  17. package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
  18. package/dist/thumbhash.cjs.js +241 -0
  19. package/dist/thumbhash.d.ts +12 -0
  20. package/dist/thumbhash.js +240 -0
  21. package/ios/FastImage/FFFBoxBlur.h +7 -0
  22. package/ios/FastImage/FFFBoxBlur.m +29 -0
  23. package/ios/FastImage/FFFThumbHash.h +24 -0
  24. package/ios/FastImage/FFFThumbHash.m +269 -0
  25. package/ios/FastImage/FFFastImageView.h +7 -1
  26. package/ios/FastImage/FFFastImageView.m +93 -46
  27. package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
  28. package/package.json +4 -2
  29. package/src/index.js.flow +11 -0
  30. package/src/index.tsx +166 -31
  31. package/src/index.web.tsx +166 -33
  32. package/src/placeholders.ts +45 -0
  33. package/src/specs/FastImageViewNativeComponent.ts +8 -2
  34. package/src/thumbhash.ts +363 -0
  35. package/thumbhash/package.json +8 -0
@@ -0,0 +1,269 @@
1
+ #import "FFFThumbHash.h"
2
+ #import "FFFBoxBlur.h"
3
+ #include <math.h>
4
+
5
+ // FFFThumbHashToRGBA is Evan Wallace's reference decoder
6
+ // (https://github.com/evanw/thumbhash, thumbHashToRGBA in java/ and js/) in C,
7
+ // with a check that the hash is long enough for what its header says it
8
+ // holds:
9
+ //
10
+ // Copyright (c) 2023 Evan Wallace
11
+ //
12
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ // of this software and associated documentation files (the "Software"), to
14
+ // deal in the Software without restriction, including without limitation the
15
+ // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
16
+ // sell copies of the Software, and to permit persons to whom the Software is
17
+ // furnished to do so, subject to the following conditions:
18
+ //
19
+ // The above copyright notice and this permission notice shall be included in
20
+ // all copies or substantial portions of the Software.
21
+ //
22
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27
+ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
28
+ // IN THE SOFTWARE.
29
+
30
+ // How many AC factors a channel of nx × ny has.
31
+ static int FFFThumbHashCount(int nx, int ny) {
32
+ int n = 0;
33
+ for (int cy = 0; cy < ny; cy++)
34
+ for (int cx = cy > 0 ? 0 : 1; cx * ny < nx * (ny - cy); cx++)
35
+ n++;
36
+ return n;
37
+ }
38
+
39
+ static void FFFThumbHashDecodeChannel(const uint8_t* hash, int start, int* index, int nx, int ny, float scale, float* ac) {
40
+ int n = FFFThumbHashCount(nx, ny);
41
+ for (int i = 0; i < n; i++) {
42
+ int data = hash[start + (*index >> 1)] >> ((*index & 1) << 2);
43
+ ac[i] = ((float) (data & 15) / 7.5f - 1.0f) * scale;
44
+ (*index)++;
45
+ }
46
+ }
47
+
48
+ static uint8_t FFFThumbHashChannel(float value) {
49
+ return (uint8_t) fmaxf(0, roundf(255.0f * fminf(1, value)));
50
+ }
51
+
52
+ // The hash decoded as unpremultiplied RGBA (malloc'd, the caller frees it),
53
+ // with its width and height, or NULL if it's too short.
54
+ static uint8_t* FFFThumbHashToRGBA(const uint8_t* hash, NSUInteger length, int* width, int* height) {
55
+ if (length < 5) {
56
+ return NULL;
57
+ }
58
+ // Read the constants
59
+ int header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16);
60
+ int header16 = hash[3] | (hash[4] << 8);
61
+ float l_dc = (float) (header24 & 63) / 63.0f;
62
+ float p_dc = (float) ((header24 >> 6) & 63) / 31.5f - 1.0f;
63
+ float q_dc = (float) ((header24 >> 12) & 63) / 31.5f - 1.0f;
64
+ float l_scale = (float) ((header24 >> 18) & 31) / 31.0f;
65
+ BOOL hasAlpha = (header24 >> 23) != 0;
66
+ float p_scale = (float) ((header16 >> 3) & 63) / 63.0f;
67
+ float q_scale = (float) ((header16 >> 9) & 63) / 63.0f;
68
+ BOOL isLandscape = (header16 >> 15) != 0;
69
+ int lx = MAX(3, isLandscape ? (hasAlpha ? 5 : 7) : header16 & 7);
70
+ int ly = MAX(3, isLandscape ? header16 & 7 : (hasAlpha ? 5 : 7));
71
+ int ac_start = hasAlpha ? 6 : 5;
72
+ int l_count = FFFThumbHashCount(lx, ly);
73
+ int pq_count = FFFThumbHashCount(3, 3);
74
+ int a_count = hasAlpha ? FFFThumbHashCount(5, 5) : 0;
75
+ if (length < (NSUInteger) (ac_start + (l_count + 2 * pq_count + a_count + 1) / 2)) {
76
+ return NULL;
77
+ }
78
+ float a_dc = hasAlpha ? (float) (hash[5] & 15) / 15.0f : 1.0f;
79
+ float a_scale = hasAlpha ? (float) ((hash[5] >> 4) & 15) / 15.0f : 0;
80
+
81
+ // Read the varying factors (boost saturation by 1.25x to compensate for quantization)
82
+ float l_ac[64], p_ac[16], q_ac[16], a_ac[32];
83
+ int ac_index = 0;
84
+ FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, lx, ly, l_scale, l_ac);
85
+ FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, 3, 3, p_scale * 1.25f, p_ac);
86
+ FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, 3, 3, q_scale * 1.25f, q_ac);
87
+ if (hasAlpha) {
88
+ FFFThumbHashDecodeChannel(hash, ac_start, &ac_index, 5, 5, a_scale, a_ac);
89
+ }
90
+
91
+ // Decode using the DCT into RGB
92
+ int ratio_lx = isLandscape ? (hasAlpha ? 5 : 7) : header16 & 7;
93
+ int ratio_ly = isLandscape ? header16 & 7 : (hasAlpha ? 5 : 7);
94
+ float ratio = ratio_ly > 0 ? (float) ratio_lx / (float) ratio_ly : 1;
95
+ int w = MAX(1, (int) roundf(ratio > 1.0f ? 32.0f : 32.0f * ratio));
96
+ int h = MAX(1, (int) roundf(ratio > 1.0f ? 32.0f / ratio : 32.0f));
97
+ uint8_t* rgba = malloc((size_t) w * h * 4);
98
+ if (!rgba) {
99
+ return NULL;
100
+ }
101
+ int cx_stop = MAX(lx, hasAlpha ? 5 : 3);
102
+ int cy_stop = MAX(ly, hasAlpha ? 5 : 3);
103
+ float fx[8], fy[8];
104
+ for (int y = 0, i = 0; y < h; y++) {
105
+ for (int x = 0; x < w; x++, i += 4) {
106
+ float l = l_dc, p = p_dc, q = q_dc, a = a_dc;
107
+
108
+ // Precompute the coefficients
109
+ for (int cx = 0; cx < cx_stop; cx++)
110
+ fx[cx] = (float) cos(M_PI / w * (x + 0.5f) * cx);
111
+ for (int cy = 0; cy < cy_stop; cy++)
112
+ fy[cy] = (float) cos(M_PI / h * (y + 0.5f) * cy);
113
+
114
+ // Decode L
115
+ for (int cy = 0, j = 0; cy < ly; cy++) {
116
+ float fy2 = fy[cy] * 2.0f;
117
+ for (int cx = cy > 0 ? 0 : 1; cx * ly < lx * (ly - cy); cx++, j++)
118
+ l += l_ac[j] * fx[cx] * fy2;
119
+ }
120
+
121
+ // Decode P and Q
122
+ for (int cy = 0, j = 0; cy < 3; cy++) {
123
+ float fy2 = fy[cy] * 2.0f;
124
+ for (int cx = cy > 0 ? 0 : 1; cx < 3 - cy; cx++, j++) {
125
+ float f = fx[cx] * fy2;
126
+ p += p_ac[j] * f;
127
+ q += q_ac[j] * f;
128
+ }
129
+ }
130
+
131
+ // Decode A
132
+ if (hasAlpha)
133
+ for (int cy = 0, j = 0; cy < 5; cy++) {
134
+ float fy2 = fy[cy] * 2.0f;
135
+ for (int cx = cy > 0 ? 0 : 1; cx < 5 - cy; cx++, j++)
136
+ a += a_ac[j] * fx[cx] * fy2;
137
+ }
138
+
139
+ // Convert to RGB
140
+ float b = l - 2.0f / 3.0f * p;
141
+ float r = (3.0f * l - b + q) / 2.0f;
142
+ float g = r - q;
143
+ rgba[i] = FFFThumbHashChannel(r);
144
+ rgba[i + 1] = FFFThumbHashChannel(g);
145
+ rgba[i + 2] = FFFThumbHashChannel(b);
146
+ rgba[i + 3] = FFFThumbHashChannel(a);
147
+ }
148
+ }
149
+ *width = w;
150
+ *height = h;
151
+ return rgba;
152
+ }
153
+
154
+ // A blurDataUri's image scaled so its longer side is 32 px (a ThumbHash's
155
+ // size) and blurred by about half of one of its pixels, so that a few pixels
156
+ // scale up smoothly instead of in steps: a box blur three times each way
157
+ // (about a Gaussian), on premultiplied colors, the edges extended outwards.
158
+ static UIImage* FFFBlurredPreview(UIImage* image) {
159
+ CGImageRef source = image.CGImage;
160
+ if (!source) {
161
+ return nil;
162
+ }
163
+ size_t width = CGImageGetWidth(source), height = CGImageGetHeight(source);
164
+ if (width == 0 || height == 0) {
165
+ return nil;
166
+ }
167
+ float scale = 32.0f / MAX(width, height);
168
+ int w = MAX(1, (int) lroundf(width * scale)), h = MAX(1, (int) lroundf(height * scale));
169
+ int radius = MAX(1, (int) lroundf(scale / 2));
170
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
171
+ CGContextRef context = CGBitmapContextCreate(NULL, w, h, 8, w * 4, colorSpace,
172
+ kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
173
+ CGColorSpaceRelease(colorSpace);
174
+ if (!context) {
175
+ return nil;
176
+ }
177
+ CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
178
+ CGContextDrawImage(context, CGRectMake(0, 0, w, h), source);
179
+ FFFBoxBlur(context, 2 * radius + 1);
180
+ CGImageRef blurred = CGBitmapContextCreateImage(context);
181
+ CGContextRelease(context);
182
+ if (!blurred) {
183
+ return nil;
184
+ }
185
+ UIImage* result = [UIImage imageWithCGImage: blurred scale: 1 orientation: UIImageOrientationUp];
186
+ CGImageRelease(blurred);
187
+ return result;
188
+ }
189
+
190
+ @implementation FFFThumbHash
191
+
192
+ + (NSCache<NSString*, UIImage*>*) cache {
193
+ static NSCache* cache;
194
+ static dispatch_once_t once;
195
+ dispatch_once(&once, ^{
196
+ cache = [NSCache new];
197
+ // Recent placeholders, at most 4 KB each.
198
+ cache.countLimit = 256;
199
+ });
200
+ return cache;
201
+ }
202
+
203
+ + (UIImage*) imageForPlaceholder: (NSString*)placeholder {
204
+ if ([placeholder rangeOfString: @"data:" options: NSCaseInsensitiveSearch | NSAnchoredSearch].location == NSNotFound) {
205
+ return [self imageForThumbHash: placeholder];
206
+ }
207
+ UIImage* cached = [[self cache] objectForKey: placeholder];
208
+ if (cached) {
209
+ return cached;
210
+ }
211
+ NSURL* url = [NSURL URLWithString: placeholder];
212
+ NSData* data = url ? [NSData dataWithContentsOfURL: url] : nil;
213
+ UIImage* decoded = data ? [UIImage imageWithData: data scale: 1] : nil;
214
+ UIImage* image = decoded ? FFFBlurredPreview(decoded) : nil;
215
+ if (image) {
216
+ [[self cache] setObject: image forKey: placeholder];
217
+ }
218
+ return image;
219
+ }
220
+
221
+ + (UIImage*) imageForThumbHash: (NSString*)thumbhash {
222
+ if (thumbhash.length == 0) {
223
+ return nil;
224
+ }
225
+ UIImage* cached = [[self cache] objectForKey: thumbhash];
226
+ if (cached) {
227
+ return cached;
228
+ }
229
+ NSMutableString* base64 = [[thumbhash stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
230
+ [base64 replaceOccurrencesOfString: @"-" withString: @"+" options: 0 range: NSMakeRange(0, base64.length)];
231
+ [base64 replaceOccurrencesOfString: @"_" withString: @"/" options: 0 range: NSMakeRange(0, base64.length)];
232
+ while (base64.length % 4 != 0) {
233
+ [base64 appendString: @"="];
234
+ }
235
+ NSData* hash = [[NSData alloc] initWithBase64EncodedString: base64 options: 0];
236
+ if (!hash) {
237
+ return nil;
238
+ }
239
+ int width = 0, height = 0;
240
+ uint8_t* rgba = FFFThumbHashToRGBA(hash.bytes, hash.length, &width, &height);
241
+ if (!rgba) {
242
+ return nil;
243
+ }
244
+ // Premultiplied, as Core Graphics draws it.
245
+ for (int i = 0; i < width * height * 4; i += 4) {
246
+ int alpha = rgba[i + 3];
247
+ rgba[i] = (uint8_t) ((rgba[i] * alpha + 127) / 255);
248
+ rgba[i + 1] = (uint8_t) ((rgba[i + 1] * alpha + 127) / 255);
249
+ rgba[i + 2] = (uint8_t) ((rgba[i + 2] * alpha + 127) / 255);
250
+ }
251
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
252
+ CGContextRef context = CGBitmapContextCreate(rgba, width, height, 8, width * 4, colorSpace,
253
+ kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
254
+ CGImageRef cgImage = context ? CGBitmapContextCreateImage(context) : NULL;
255
+ if (context) {
256
+ CGContextRelease(context);
257
+ }
258
+ CGColorSpaceRelease(colorSpace);
259
+ free(rgba);
260
+ if (!cgImage) {
261
+ return nil;
262
+ }
263
+ UIImage* image = [UIImage imageWithCGImage: cgImage scale: 1 orientation: UIImageOrientationUp];
264
+ CGImageRelease(cgImage);
265
+ [[self cache] setObject: image forKey: thumbhash];
266
+ return image;
267
+ }
268
+
269
+ @end
@@ -32,7 +32,13 @@ typedef NS_ENUM(NSInteger, FFFObjectFit) {
32
32
  // Several sources (2 or more) of the same image at different sizes: the view
33
33
  // loads the one whose size is closest to its own.
34
34
  @property (nonatomic, copy) NSArray<FFFastImageSource *> *sources;
35
- @property (nonatomic, strong) UIImage *defaultSource;
35
+ // The `placeholder` prop.
36
+ @property (nonatomic, strong) UIImage *placeholder;
37
+ // The `sourcePlaceholders` prop, by the source's index (an empty string for
38
+ // none; one for a single source): the picked source's ThumbHash or
39
+ // blurDataUri (a data uri) shows instead of placeholder. JS sends them apart
40
+ // from the sources, so a new one for the same image doesn't load it again.
41
+ @property (nonatomic, copy) NSArray<NSString *> *sourcePlaceholders;
36
42
  @property (nonatomic, strong) UIColor *imageColor;
37
43
  // When it changes, the next image doesn't replace the current one: the view
38
44
  // clears first (for views reused for other content, like list rows).
@@ -5,7 +5,8 @@
5
5
  #import <SDWebImage/SDWebImageError.h>
6
6
  #import <SDWebImage/SDImageTransformer.h>
7
7
  #import "FFFDownsampledImage.h"
8
- #import <Accelerate/Accelerate.h>
8
+ #import "FFFThumbHash.h"
9
+ #import "FFFBoxBlur.h"
9
10
 
10
11
  @interface FFFastImageView ()
11
12
 
@@ -28,15 +29,16 @@
28
29
  // The image being blurred fades in once it's blurred (transition).
29
30
  @property(nonatomic, assign) BOOL fadesBlurredImage;
30
31
  // The image being blurred is the first loaded one the view shows: until it's
31
- // blurred, the view shows no loaded image (nothing, or defaultSource).
32
+ // blurred, the view shows no loaded image (nothing, or the placeholder prop).
32
33
  @property(nonatomic, assign) BOOL blursOverNothing;
33
34
  // Whether the current load was already restarted after the app came back
34
35
  // from the background (see downloadImage:).
35
36
  @property(nonatomic, assign) BOOL retriedAfterBackground;
36
37
  // Waits for the app to be active again, to restart a load.
37
38
  @property(nonatomic, strong) id activeObserver;
38
- // Whether the view shows an image that loaded (not defaultSource or nothing).
39
- // A new source then keeps it until the new image has loaded (see reloadImage).
39
+ // Whether the view shows an image that loaded (not the placeholder prop or
40
+ // nothing). A new source then keeps it until the new image has loaded (see
41
+ // reloadImage).
40
42
  @property(nonatomic, assign) BOOL showsLoadedImage;
41
43
  // downsample, or several sources: a load waits for the view's size (see
42
44
  // didSetProps).
@@ -62,6 +64,15 @@
62
64
  // The view's own mask, which clips it to the box while it's moved, when React
63
65
  // Native gives it none (the view doesn't clip: overflow visible).
64
66
  @property(nonatomic, strong) CALayer* ownMask;
67
+ // The picked source's placeholder (its ThumbHash or blurDataUri) decoded, and
68
+ // the string, or nil: it shows instead of the placeholder prop (see
69
+ // shownPlaceholder).
70
+ @property(nonatomic, strong) UIImage* hashImage;
71
+ @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).
74
+ @property(nonatomic, assign) BOOL showsPlaceholder;
75
+ @property(nonatomic, assign) BOOL showsHash;
65
76
 
66
77
  @end
67
78
 
@@ -266,12 +277,14 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
266
277
  contentMode = UIViewContentModeScaleToFill;
267
278
  break;
268
279
  case FFFObjectFitNone:
269
- contentMode = UIViewContentModeCenter;
280
+ // A source's placeholder stands for an image whose size isn't
281
+ // known yet: as with contain, here and for scale-down.
282
+ contentMode = self.showsHash ? UIViewContentModeScaleAspectFit : UIViewContentModeCenter;
270
283
  break;
271
284
  case FFFObjectFitScaleDown: {
272
285
  CGSize imageSize = super.image.size;
273
286
  CGSize viewSize = self.bounds.size;
274
- contentMode = imageSize.width > viewSize.width || imageSize.height > viewSize.height
287
+ contentMode = self.showsHash || imageSize.width > viewSize.width || imageSize.height > viewSize.height
275
288
  ? UIViewContentModeScaleAspectFit
276
289
  : UIViewContentModeCenter;
277
290
  break;
@@ -427,6 +440,13 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
427
440
 
428
441
  - (void) setImage: (UIImage*)image {
429
442
  self.imageCount++;
443
+ self.showsHash = image != nil && image == self.hashImage;
444
+ self.showsPlaceholder = self.showsHash || (image != nil && image == _placeholder);
445
+ if ([_imageRendering isEqualToString: @"pixelated"]) {
446
+ // A source's placeholder is a blurred preview, scaled up smoothly
447
+ // (pixelated is for the image).
448
+ self.layer.magnificationFilter = self.showsHash ? kCAFilterLinear : kCAFilterNearest;
449
+ }
430
450
  if ([self blurs: image]) {
431
451
  self.originalImage = image;
432
452
  self.blurredImage = nil;
@@ -513,10 +533,10 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
513
533
  }
514
534
  }
515
535
 
516
- // Whether the image is blurred: a loaded image, not defaultSource (as with
517
- // React Native's Image, and on Android).
536
+ // Whether the image is blurred: a loaded image, not the placeholder prop (as
537
+ // with React Native's Image, and on Android).
518
538
  - (BOOL) blurs: (UIImage*)image {
519
- return _blurRadius > 0 && image && image != _defaultSource;
539
+ return _blurRadius > 0 && image && image != _placeholder && image != self.hashImage;
520
540
  }
521
541
 
522
542
  // Blurs the image off the main thread, and shows it (tinted as any image) unless
@@ -649,27 +669,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
649
669
  [image drawInRect: CGRectMake(0, 0, width, height)];
650
670
  UIGraphicsPopContext();
651
671
 
652
- if (boxSize > 1) {
653
- vImage_Buffer buffer = {
654
- .data = CGBitmapContextGetData(context),
655
- .height = height,
656
- .width = width,
657
- .rowBytes = CGBitmapContextGetBytesPerRow(context),
658
- };
659
- vImage_Buffer other = buffer;
660
- other.data = malloc(buffer.rowBytes * height);
661
- vImage_Error tempSize = vImageBoxConvolve_ARGB8888(&buffer, &other, NULL, 0, 0, boxSize, boxSize, NULL,
662
- kvImageGetTempBufferSize | kvImageEdgeExtend);
663
- void* temp = tempSize > 0 ? malloc(tempSize) : NULL;
664
- if (other.data && temp) {
665
- vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
666
- vImageBoxConvolve_ARGB8888(&other, &buffer, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
667
- vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
668
- memcpy(buffer.data, other.data, buffer.rowBytes * height);
669
- }
670
- free(other.data);
671
- free(temp);
672
- }
672
+ FFFBoxBlur(context, boxSize);
673
673
 
674
674
  CGImageRef cgImage = CGBitmapContextCreateImage(context);
675
675
  CGContextRelease(context);
@@ -798,13 +798,51 @@ NSString *FFFErrorMessage(NSError *error)
798
798
  }
799
799
  }
800
800
 
801
- - (void) setDefaultSource: (UIImage*)defaultSource {
802
- if (_defaultSource != defaultSource) {
803
- _defaultSource = defaultSource;
801
+ - (void) setPlaceholder: (UIImage*)placeholder {
802
+ if (_placeholder != placeholder) {
803
+ _placeholder = placeholder;
804
804
  _needsReload = YES;
805
805
  }
806
806
  }
807
807
 
808
+ - (void) setSourcePlaceholders: (NSArray<NSString*>*)sourcePlaceholders {
809
+ _sourcePlaceholders = [sourcePlaceholders copy];
810
+ [self updateHashImage];
811
+ }
812
+
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.
815
+ - (UIImage*) shownPlaceholder {
816
+ return self.hashImage ?: _placeholder;
817
+ }
818
+
819
+ // The picked source's ThumbHash or blurDataUri, or nil.
820
+ - (NSString*) pickedSourcePlaceholder {
821
+ NSUInteger index = 0;
822
+ if ([self picksSource]) {
823
+ index = _source ? [_sources indexOfObjectIdenticalTo: _source] : NSNotFound;
824
+ }
825
+ NSString* hash = index < _sourcePlaceholders.count ? _sourcePlaceholders[index] : nil;
826
+ return hash.length > 0 ? hash : nil;
827
+ }
828
+
829
+ // Decodes the picked source's placeholder. A view showing the previous
830
+ // placeholder shows the new one at once, without loading the image again.
831
+ - (void) updateHashImage {
832
+ NSString* hash = [self pickedSourcePlaceholder];
833
+ if (hash == self.hashImageKey || [hash isEqualToString: self.hashImageKey]) {
834
+ return;
835
+ }
836
+ UIImage* previous = [self shownPlaceholder];
837
+ BOOL showing = self.showsPlaceholder;
838
+ self.hashImageKey = hash;
839
+ self.hashImage = [FFFThumbHash imageForPlaceholder: hash];
840
+ UIImage* placeholder = [self shownPlaceholder];
841
+ if (showing && placeholder != previous) {
842
+ [self setImage: placeholder];
843
+ }
844
+ }
845
+
808
846
  - (void) didSetProps {
809
847
  if (_needsReload) {
810
848
  // With downsample on, the image is decoded for the view's size, and
@@ -906,6 +944,7 @@ NSString *FFFErrorMessage(NSError *error)
906
944
  if ([self picksSource]) {
907
945
  _source = [self sourceForSize];
908
946
  }
947
+ [self updateHashImage];
909
948
  self.decodedBox = CGSizeZero;
910
949
  // The previous load, if it's still running, is for a source the view no
911
950
  // longer shows: cancel it (a new download would, but a data uri or no
@@ -924,8 +963,9 @@ NSString *FFFErrorMessage(NSError *error)
924
963
  // Use SDWebImage API to support external format like WebP images
925
964
  UIImage* image = [UIImage sd_imageWithData: [NSData dataWithContentsOfURL: _source.url]];
926
965
  if (!image) {
927
- // Not decodable: fail like a remote image, showing defaultSource.
928
- [self setImage: _defaultSource];
966
+ // Not decodable: fail like a remote image, showing the
967
+ // placeholder.
968
+ [self setImage: [self shownPlaceholder]];
929
969
  self.showsLoadedImage = NO;
930
970
  [self sendOnError: @"Failed to decode the image"];
931
971
  if (self.onFastImageLoadEnd) {
@@ -954,9 +994,10 @@ NSString *FFFErrorMessage(NSError *error)
954
994
  SDWebImageOptions options = [self loadOptions];
955
995
 
956
996
  // Keep showing the loaded image until the new one has loaded, instead
957
- // of clearing it to defaultSource (or nothing) while it loads, which
958
- // flashed (#747). defaultSource shows if the new image fails. As React
959
- // Native's Image does; a `key` that changes starts from blank instead.
997
+ // of clearing it to the placeholder prop (or nothing) while it loads,
998
+ // which flashed (#747). The placeholder prop shows if the new image
999
+ // fails. As React Native's Image does; a `key` that changes starts from
1000
+ // blank instead.
960
1001
  if (self.showsLoadedImage) {
961
1002
  options |= SDWebImageDelayPlaceholder;
962
1003
  }
@@ -970,8 +1011,8 @@ NSString *FFFErrorMessage(NSError *error)
970
1011
  [self stopWaitingForActive];
971
1012
 
972
1013
  [self downloadImage: _source options: options context: [self loadContext] events: YES];
973
- } else if (_defaultSource) {
974
- [self setImage: _defaultSource];
1014
+ } else if ([self shownPlaceholder]) {
1015
+ [self setImage: [self shownPlaceholder]];
975
1016
  self.showsLoadedImage = NO;
976
1017
  }
977
1018
  }
@@ -1003,8 +1044,8 @@ NSString *FFFErrorMessage(NSError *error)
1003
1044
  }
1004
1045
 
1005
1046
  // Whether the view loads as it's laid out, so its image shows in this frame:
1006
- // one in the memory cache, where SDWebImage finds it as the load starts, or
1007
- // one that isn't downloaded (no source, which shows defaultSource, or a data
1047
+ // one in the memory cache, where SDWebImage finds it as the load starts, or one
1048
+ // that isn't downloaded (no source, which shows the placeholder prop, or a data
1008
1049
  // uri). Also when that can't be told (an app's own image cache), as before.
1009
1050
  - (BOOL) loadsDuringLayout {
1010
1051
  FFFastImageSource* source = [self picksSource] ? [self sourceForSize] : _source;
@@ -1143,13 +1184,13 @@ NSString *FFFErrorMessage(NSError *error)
1143
1184
  // class to SDAnimatedImage (loadContext sets it). Only while this is the
1144
1185
  // current load: SDWebImage sets the placeholder when a load it cancelled
1145
1186
  // completes, which could replace a newer image.
1146
- UIImage* placeholder = _defaultSource;
1187
+ UIImage* placeholder = [self shownPlaceholder];
1147
1188
  SDSetImageBlock setImage = ^(UIImage* _Nullable image, NSData* _Nullable data, SDImageCacheType cacheType, NSURL* _Nullable imageURL) {
1148
1189
  if (weakSelf.loadCount != load) {
1149
1190
  return;
1150
1191
  }
1151
- // The loaded image, not defaultSource (shown while it loads, or if it
1152
- // fails).
1192
+ // The loaded image, not the placeholder prop (shown while it loads, or
1193
+ // if it fails).
1153
1194
  BOOL loaded = image && image != placeholder;
1154
1195
  [weakSelf showLoadedImage: image fade: loaded && [weakSelf fadesImageFromCache: cacheType]];
1155
1196
  };
@@ -1194,7 +1235,13 @@ NSString *FFFErrorMessage(NSError *error)
1194
1235
  return;
1195
1236
  }
1196
1237
  if (error) {
1197
- // SDWebImage shows the placeholder (defaultSource or nothing).
1238
+ // SDWebImage shows the placeholder the load started with
1239
+ // (the placeholder prop, a source's, or nothing): the
1240
+ // current one if the source's changed since.
1241
+ UIImage* current = [weakSelf shownPlaceholder];
1242
+ if (current != placeholder) {
1243
+ [weakSelf setImage: current];
1244
+ }
1198
1245
  weakSelf.showsLoadedImage = NO;
1199
1246
  [weakSelf sendOnError: [source errorMessage: error]];
1200
1247
  if (weakSelf.onFastImageLoadEnd) {
@@ -95,7 +95,7 @@ static folly::dynamic FFFDynamicFromObject(id value)
95
95
  return nullptr;
96
96
  }
97
97
 
98
- // A prop of any type (a source, sources, defaultSource) as JSON: nil when
98
+ // A prop of any type (a source, sources, placeholder) as JSON: nil when
99
99
  // it's null (not set).
100
100
  static id _Nullable FFFJSON(const folly::dynamic &value)
101
101
  {
@@ -232,9 +232,16 @@ typedef NS_OPTIONS(int32_t, FFFEvent) {
232
232
  id sources = FFFJSON(newViewProps.sources);
233
233
  view.sources = sources ? [RCTConvert FFFastImageSourceArray:sources] : nil;
234
234
  }
235
- if (all || oldViewProps.defaultSource != newViewProps.defaultSource) {
236
- id defaultSource = FFFJSON(newViewProps.defaultSource);
237
- view.defaultSource = defaultSource ? [RCTConvert UIImage:defaultSource] : nil;
235
+ if (all || oldViewProps.placeholder != newViewProps.placeholder) {
236
+ id placeholder = FFFJSON(newViewProps.placeholder);
237
+ view.placeholder = placeholder ? [RCTConvert UIImage:placeholder] : nil;
238
+ }
239
+ if (all || oldViewProps.sourcePlaceholders != newViewProps.sourcePlaceholders) {
240
+ NSMutableArray<NSString *> *placeholders = [NSMutableArray arrayWithCapacity:newViewProps.sourcePlaceholders.size()];
241
+ for (const auto &placeholder : newViewProps.sourcePlaceholders) {
242
+ [placeholders addObject:[NSString stringWithUTF8String:placeholder.c_str()]];
243
+ }
244
+ view.sourcePlaceholders = placeholders.count > 0 ? placeholders : nil;
238
245
  }
239
246
  if (all || oldViewProps.objectFit != newViewProps.objectFit) {
240
247
  view.objectFit = [RCTConvert FFFObjectFit:FFFString(newViewProps.objectFit)];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "10.1.1",
3
+ "version": "10.3.0",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
@@ -32,9 +32,11 @@
32
32
  "app.plugin.js",
33
33
  "src",
34
34
  "!src/**/*.test.tsx",
35
+ "!src/**/*.test.ts",
35
36
  "!src/__snapshots__",
36
37
  "dist",
37
- "RNFastImage.podspec"
38
+ "RNFastImage.podspec",
39
+ "thumbhash"
38
40
  ],
39
41
  "scripts": {
40
42
  "build": "bun run build:package && bun run build:docs",
package/src/index.js.flow CHANGED
@@ -110,6 +110,11 @@ export type PreloadResult = PreloadSuccess | PreloadFailure
110
110
  export type PreloadFn = (
111
111
  sources: Array<FastImageSource>,
112
112
  ) => Promise<Array<PreloadResult>>
113
+ export type SourcePlaceholder = {
114
+ thumbhash?: string,
115
+ blurDataUri?: string,
116
+ }
117
+
113
118
  export type FastImageSource = {
114
119
  uri?: string,
115
120
  headers?: Object,
@@ -118,6 +123,8 @@ export type FastImageSource = {
118
123
  cacheKey?: string,
119
124
  memoryCache?: boolean,
120
125
  diskCache?: boolean,
126
+ thumbhash?: string,
127
+ blurDataUri?: string,
121
128
  width?: number,
122
129
  height?: number,
123
130
  scale?: number,
@@ -132,6 +139,8 @@ export type FastImageProps = $ReadOnly<{|
132
139
  onProgress?: ?(event: OnProgressEvent) => void,
133
140
 
134
141
  source?: ?(FastImageSource | Array<FastImageSource> | number),
142
+ placeholder?: ?(number | string | SourcePlaceholder),
143
+ // Deprecated: use placeholder, which is the same.
135
144
  defaultSource?: ?number,
136
145
 
137
146
  tintColor?: number | string,
@@ -140,6 +149,8 @@ export type FastImageProps = $ReadOnly<{|
140
149
  recyclingKey?: ?string,
141
150
  loop?: ?(boolean | number),
142
151
  imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
152
+ loading?: ?('lazy' | 'eager'),
153
+ sizes?: ?string,
143
154
  paused?: ?boolean,
144
155
  transition?: ?(boolean | number | Transition),
145
156
  downsample?: ?boolean,