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.
- package/README.md +107 -14
- package/android/src/main/java/com/fastimage/FastImageBlur.java +3 -2
- package/android/src/main/java/com/fastimage/FastImageThumbHash.java +246 -0
- package/android/src/main/java/com/fastimage/FastImageViewConverter.java +1 -1
- package/android/src/main/java/com/fastimage/FastImageViewManager.java +20 -5
- package/android/src/main/java/com/fastimage/FastImageViewWithUrl.java +125 -48
- package/dist/index.cjs.js +47 -9
- package/dist/index.cjs.js.flow +11 -0
- package/dist/index.cjs.web.js +67 -14
- package/dist/index.cjs.web.js.flow +11 -0
- package/dist/index.d.ts +77 -13
- package/dist/index.js +48 -10
- package/dist/index.js.flow +11 -0
- package/dist/index.web.js +68 -15
- package/dist/index.web.js.flow +11 -0
- package/dist/placeholders.d.ts +4 -0
- package/dist/specs/FastImageViewNativeComponent.d.ts +2 -1
- package/dist/thumbhash.cjs.js +241 -0
- package/dist/thumbhash.d.ts +12 -0
- package/dist/thumbhash.js +240 -0
- package/ios/FastImage/FFFBoxBlur.h +7 -0
- package/ios/FastImage/FFFBoxBlur.m +29 -0
- package/ios/FastImage/FFFThumbHash.h +24 -0
- package/ios/FastImage/FFFThumbHash.m +269 -0
- package/ios/FastImage/FFFastImageView.h +7 -1
- package/ios/FastImage/FFFastImageView.m +93 -46
- package/ios/FastImage/FFFastImageViewComponentView.mm +11 -4
- package/package.json +4 -2
- package/src/index.js.flow +11 -0
- package/src/index.tsx +166 -31
- package/src/index.web.tsx +166 -33
- package/src/placeholders.ts +45 -0
- package/src/specs/FastImageViewNativeComponent.ts +8 -2
- package/src/thumbhash.ts +363 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
39
|
-
// A new source then keeps it until the new image has loaded (see
|
|
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
|
-
|
|
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
|
|
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 !=
|
|
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
|
-
|
|
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)
|
|
802
|
-
if (
|
|
803
|
-
|
|
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
|
|
928
|
-
|
|
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
|
|
958
|
-
// flashed (#747).
|
|
959
|
-
// Native's Image does; a `key` that changes starts from
|
|
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 (
|
|
974
|
-
[self setImage:
|
|
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
|
-
//
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
236
|
-
id
|
|
237
|
-
view.
|
|
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.
|
|
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,
|