react-native-fast-image 8.18.1 → 8.19.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.
@@ -5,11 +5,97 @@
5
5
  #import <SDWebImage/SDWebImageManager.h>
6
6
  #import <SDWebImage/SDWebImageError.h>
7
7
  #import <SDWebImage/SDWebImagePrefetcher.h>
8
+ #import <SDWebImage/NSData+ImageContentType.h>
9
+
10
+ // configureCache's limits, by their names in JS. Each is set in the app's
11
+ // Info.plist (FastImageMaxDiskSize, ...) or changed at runtime, which is saved
12
+ // (FastImage.maxDiskSize, ...) and wins until it's reset with null.
13
+ static NSArray<NSString *> *FFFCacheLimitNames(void)
14
+ {
15
+ return @[@"maxDiskSize", @"maxDiskAge", @"maxMemorySize"];
16
+ }
17
+
18
+ static NSString *FFFSavedCacheLimitKey(NSString *name)
19
+ {
20
+ return [@"FastImage." stringByAppendingString:name];
21
+ }
22
+
23
+ // A limit's value: the one saved by configureCache, or the app's Info.plist's,
24
+ // or nil for SDWebImage's default.
25
+ static NSNumber *FFFCacheLimit(NSString *name)
26
+ {
27
+ id saved = [NSUserDefaults.standardUserDefaults objectForKey:FFFSavedCacheLimitKey(name)];
28
+ if ([saved isKindOfClass:NSNumber.class]) {
29
+ return saved;
30
+ }
31
+ NSString *infoKey = [NSString stringWithFormat:@"FastImage%@%@",
32
+ [name substringToIndex:1].uppercaseString, [name substringFromIndex:1]];
33
+ id info = [NSBundle.mainBundle objectForInfoDictionaryKey:infoKey];
34
+ if ([info isKindOfClass:NSNumber.class]) {
35
+ return info;
36
+ }
37
+ if ([info isKindOfClass:NSString.class]) {
38
+ return @([info doubleValue]);
39
+ }
40
+ return nil;
41
+ }
42
+
43
+ // Sets a limit in SDWebImage's cache config: 0 for no limit, nil for
44
+ // SDWebImage's default. maxDiskAge counts from when an image was last used
45
+ // (SDWebImage's default from 5.21; before, from when it was stored).
46
+ static void FFFSetCacheLimit(SDImageCacheConfig *config, NSString *name, NSNumber *value)
47
+ {
48
+ double limit = MAX(value.doubleValue, 0);
49
+ if ([name isEqualToString:@"maxDiskSize"]) {
50
+ config.maxDiskSize = limit;
51
+ } else if ([name isEqualToString:@"maxMemorySize"]) {
52
+ config.maxMemoryCost = limit;
53
+ } else if (value) {
54
+ // SDWebImage never expires images with a negative age (with 0 it
55
+ // would remove them all).
56
+ config.maxDiskAge = limit > 0 ? limit : -1;
57
+ config.diskCacheExpireType = SDImageCacheConfigExpireTypeAccessDate;
58
+ } else {
59
+ config.maxDiskAge = 60 * 60 * 24 * 7;
60
+ }
61
+ }
62
+
63
+ // The bytes of the files in a folder.
64
+ static unsigned long long FFFFolderSize(NSString *path)
65
+ {
66
+ unsigned long long size = 0;
67
+ NSDirectoryEnumerator<NSURL *> *files = [NSFileManager.defaultManager enumeratorAtURL:[NSURL fileURLWithPath:path isDirectory:YES]
68
+ includingPropertiesForKeys:@[NSURLFileSizeKey]
69
+ options:0
70
+ errorHandler:nil];
71
+ for (NSURL *file in files) {
72
+ NSNumber *fileSize;
73
+ [file getResourceValue:&fileSize forKey:NSURLFileSizeKey error:nil];
74
+ size += fileSize.unsignedLongLongValue;
75
+ }
76
+ return size;
77
+ }
8
78
 
9
79
  @implementation FFFastImageViewManager
10
80
 
11
81
  RCT_EXPORT_MODULE(FastImageView)
12
82
 
83
+ // The app's cache limits (saved or in its Info.plist), before FastImage loads
84
+ // any image: this class is used first, to create the views and for preload.
85
+ + (void)initialize
86
+ {
87
+ if (self != [FFFastImageViewManager class]) {
88
+ return;
89
+ }
90
+ SDImageCacheConfig *config = SDImageCache.sharedImageCache.config;
91
+ for (NSString *name in FFFCacheLimitNames()) {
92
+ NSNumber *value = FFFCacheLimit(name);
93
+ if (value) {
94
+ FFFSetCacheLimit(config, name, value);
95
+ }
96
+ }
97
+ }
98
+
13
99
  - (FFFastImageView*)view {
14
100
  return [[FFFastImageView alloc] init];
15
101
  }
@@ -68,6 +154,20 @@ static SDWebImageOptions FFFPreloadOptions(FFFastImageSource *source)
68
154
  return options | [source cacheOptions];
69
155
  }
70
156
 
157
+ // The key a source's image is in SDWebImage's cache under: its cacheKey
158
+ // (without needing a uri), or its url as a view converts it. `web` images
159
+ // ignore cacheKey. nil without either.
160
+ static NSString *FFFDiskCacheKey(FFFastImageSource *source)
161
+ {
162
+ if (source.cacheKey.length > 0 && source.cacheControl != FFFCacheControlWeb) {
163
+ return source.cacheKey;
164
+ }
165
+ if (!source.url) {
166
+ return nil;
167
+ }
168
+ return [SDWebImageManager.sharedManager cacheKeyForURL:source.url context:FFFPreloadContext(source)];
169
+ }
170
+
71
171
  static NSUInteger FFFPreloadLimit(void)
72
172
  {
73
173
  NSUInteger limit = [SDWebImagePrefetcher sharedImagePrefetcher].maxConcurrentPrefetchCount;
@@ -89,6 +189,64 @@ static void FFFStartPendingPreloads(void)
89
189
  }
90
190
  }
91
191
 
192
+ // Loads the source in the preloads' queue, then calls completed once with its
193
+ // image or error. Only on the main queue (SDWebImageManager calls its
194
+ // completion blocks there); call FFFStartPendingPreloads after adding loads.
195
+ static void FFFQueueLoad(FFFastImageSource *source,
196
+ SDWebImageOptions options,
197
+ SDWebImageContext *context,
198
+ void (^completed)(UIImage *image, NSError *error))
199
+ {
200
+ if (!FFFPendingPreloads) {
201
+ FFFPendingPreloads = [NSMutableArray array];
202
+ }
203
+ [FFFPendingPreloads addObject:[^{
204
+ // Once per slot: SDWebImage calls this once with finished set (a
205
+ // progressive load calls it more, without).
206
+ __block BOOL done = NO;
207
+ [[SDWebImageManager sharedManager] loadImageWithURL:source.url
208
+ options:options
209
+ context:context
210
+ progress:nil
211
+ completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
212
+ if (!finished || done) {
213
+ return;
214
+ }
215
+ done = YES;
216
+ if (error) {
217
+ [source forgetResponseAfterError:error];
218
+ }
219
+ FFFPreloadsInFlight--;
220
+ completed(image, error);
221
+ FFFStartPendingPreloads();
222
+ }];
223
+ } copy]];
224
+ }
225
+
226
+ // Calls found with the path of the file cached under key, or missing if it
227
+ // isn't on disk. On the main queue.
228
+ static void FFFFindCachedFile(NSString *key, void (^found)(NSString *path), dispatch_block_t missing)
229
+ {
230
+ SDImageCache *cache = SDImageCache.sharedImageCache;
231
+ [cache diskImageExistsWithKey:key completion:^(BOOL exists) {
232
+ if (exists) {
233
+ found([cache cachePathForKey:key]);
234
+ } else {
235
+ missing();
236
+ }
237
+ }];
238
+ }
239
+
240
+ static NSDictionary *FFFFailure(NSString *error)
241
+ {
242
+ return @{@"ok": @NO, @"error": error};
243
+ }
244
+
245
+ static NSDictionary *FFFFile(NSString *path)
246
+ {
247
+ return @{@"ok": @YES, @"path": path};
248
+ }
249
+
92
250
  // Resolves with a result per source, in order, once all have loaded or failed:
93
251
  // { ok, width, height } or { ok: false, error }. Never rejects. A source with
94
252
  // memoryCache false is only stored on disk, without decoding it (its size
@@ -100,9 +258,6 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
100
258
  // This runs on the UIManager queue; the queue state is only touched on
101
259
  // the main queue.
102
260
  dispatch_async(dispatch_get_main_queue(), ^{
103
- if (!FFFPendingPreloads) {
104
- FFFPendingPreloads = [NSMutableArray array];
105
- }
106
261
  NSMutableArray *results = [NSMutableArray arrayWithCapacity:sources.count];
107
262
  // Sources of this call still to finish. It starts at the number of
108
263
  // sources, so the promise can't resolve before they've all been added.
@@ -117,7 +272,7 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
117
272
  if (!source.url) {
118
273
  // An empty, missing or null uri (JS sends null sources as {}).
119
274
  // It fails without taking a slot.
120
- [results addObject:@{@"ok": @NO, @"error": @"Invalid source: no uri"}];
275
+ [results addObject:FFFFailure(@"Invalid source: no uri")];
121
276
  finishOne();
122
277
  return;
123
278
  }
@@ -130,30 +285,12 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
130
285
  options |= SDWebImageAvoidDecodeImage;
131
286
  context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeDisk);
132
287
  }
133
- [FFFPendingPreloads addObject:[^{
134
- // Once per slot: SDWebImage calls this once with finished set
135
- // (a progressive load calls it more, without).
136
- __block BOOL done = NO;
137
- [[SDWebImageManager sharedManager] loadImageWithURL:source.url
138
- options:options
139
- context:[context copy]
140
- progress:nil
141
- completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
142
- if (!finished || done) {
143
- return;
144
- }
145
- done = YES;
146
- if (error) {
147
- [source forgetResponseAfterError:error];
148
- }
149
- results[idx] = image
150
- ? @{@"ok": @YES, @"width": @(image.size.width), @"height": @(image.size.height)}
151
- : @{@"ok": @NO, @"error": FFFErrorMessage(error)};
152
- FFFPreloadsInFlight--;
153
- finishOne();
154
- FFFStartPendingPreloads();
155
- }];
156
- } copy]];
288
+ FFFQueueLoad(source, options, [context copy], ^(UIImage *image, NSError *error) {
289
+ results[idx] = image
290
+ ? @{@"ok": @YES, @"width": @(image.size.width), @"height": @(image.size.height)}
291
+ : FFFFailure(FFFErrorMessage(error));
292
+ finishOne();
293
+ });
157
294
  }];
158
295
 
159
296
  if (sources.count == 0) {
@@ -167,77 +304,155 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
167
304
  // The source's downloaded file in the disk cache, downloading it first if it
168
305
  // isn't there (without decoding it or keeping it in memory, and in the
169
306
  // preloads' queue): { ok, path } or { ok: false, error }. Never rejects. With
170
- // `cacheOnly` it doesn't download. A local file is its own path.
307
+ // `cacheOnly`, or a cacheKey without a uri, it doesn't download. A local
308
+ // file is its own path.
171
309
  RCT_EXPORT_METHOD(getCachePath:(FFFastImageSource *)source
172
310
  resolve:(RCTPromiseResolveBlock)resolve
173
311
  reject:(__unused RCTPromiseRejectBlock)reject)
174
312
  {
175
313
  if (!source.url) {
176
- resolve(@{@"ok": @NO, @"error": @"Invalid source: no uri"});
314
+ NSString *key = FFFDiskCacheKey(source);
315
+ if (!key) {
316
+ resolve(FFFFailure(@"Invalid source: no uri or cacheKey"));
317
+ return;
318
+ }
319
+ FFFFindCachedFile(key, ^(NSString *path) {
320
+ resolve(FFFFile(path));
321
+ }, ^{
322
+ resolve(FFFFailure(@"Not in the disk cache"));
323
+ });
177
324
  return;
178
325
  }
179
326
  if (source.url.isFileURL) {
180
327
  BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:source.url.path];
181
- resolve(exists
182
- ? @{@"ok": @YES, @"path": source.url.path}
183
- : @{@"ok": @NO, @"error": @"No file at this uri"});
328
+ resolve(exists ? FFFFile(source.url.path) : FFFFailure(@"No file at this uri"));
184
329
  return;
185
330
  }
186
- SDWebImageMutableContext *context = FFFPreloadContext(source);
187
- // The key SDWebImage stores it under (the cacheKey, or the converted url).
188
- NSString *key = [SDWebImageManager.sharedManager cacheKeyForURL:source.url context:context];
189
- SDImageCache *cache = SDImageCache.sharedImageCache;
190
- void (^resolveFromDisk)(NSString *) = ^(NSString *error) {
191
- [cache diskImageExistsWithKey:key completion:^(BOOL exists) {
192
- resolve(exists
193
- ? @{@"ok": @YES, @"path": [cache cachePathForKey:key]}
194
- : @{@"ok": @NO, @"error": error});
195
- }];
331
+ NSString *key = FFFDiskCacheKey(source);
332
+ void (^resolveFile)(NSString *) = ^(NSString *path) {
333
+ resolve(FFFFile(path));
196
334
  };
197
- [cache diskImageExistsWithKey:key completion:^(BOOL exists) {
198
- // On the main queue, where the preloads' queue is used.
199
- if (exists) {
200
- resolve(@{@"ok": @YES, @"path": [cache cachePathForKey:key]});
201
- return;
202
- }
335
+ FFFFindCachedFile(key, resolveFile, ^{
203
336
  if (source.cacheControl == FFFCacheControlCacheOnly) {
204
- resolve(@{@"ok": @NO, @"error": @"Not in the disk cache"});
337
+ resolve(FFFFailure(@"Not in the disk cache"));
205
338
  return;
206
339
  }
207
- if (!FFFPendingPreloads) {
208
- FFFPendingPreloads = [NSMutableArray array];
209
- }
210
340
  // As a disk-only preload: not decoded, and not looked up in or kept in
211
- // memory (an image in memory may not be on disk). It resolves once the
212
- // file is written.
341
+ // memory (an image in memory may not be on disk). It completes once
342
+ // the file is written.
213
343
  SDWebImageOptions options = FFFPreloadOptions(source) | SDWebImageAvoidDecodeImage | SDWebImageWaitStoreCache;
344
+ SDWebImageMutableContext *context = FFFPreloadContext(source);
214
345
  context[SDWebImageContextQueryCacheType] = @(SDImageCacheTypeDisk);
215
346
  context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeDisk);
216
- [FFFPendingPreloads addObject:[^{
217
- __block BOOL done = NO;
218
- [[SDWebImageManager sharedManager] loadImageWithURL:source.url
219
- options:options
220
- context:[context copy]
221
- progress:nil
222
- completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
223
- if (!finished || done) {
224
- return;
225
- }
226
- done = YES;
227
- FFFPreloadsInFlight--;
228
- FFFStartPendingPreloads();
229
- if (error) {
230
- [source forgetResponseAfterError:error];
231
- }
232
- if (!image) {
233
- resolve(@{@"ok": @NO, @"error": FFFErrorMessage(error)});
234
- return;
235
- }
236
- resolveFromDisk(@"Not stored in the disk cache");
237
- }];
238
- } copy]];
347
+ FFFQueueLoad(source, options, [context copy], ^(UIImage *image, NSError *error) {
348
+ if (!image) {
349
+ resolve(FFFFailure(FFFErrorMessage(error)));
350
+ return;
351
+ }
352
+ FFFFindCachedFile(key, resolveFile, ^{
353
+ resolve(FFFFailure(@"Not stored in the disk cache"));
354
+ });
355
+ });
239
356
  FFFStartPendingPreloads();
240
- }];
357
+ });
358
+ }
359
+
360
+ // Stores a local image file as the source's image in the disk cache, so views
361
+ // and preloads of the source load it without downloading it, and resolves
362
+ // with its cached file: { ok, path } or { ok: false, error }. Never rejects.
363
+ // It doesn't replace an image that's already cached (Glide can't on Android;
364
+ // a new image should get a new cacheKey), and doesn't store `web` sources
365
+ // (Android keeps them only in an HTTP cache).
366
+ RCT_EXPORT_METHOD(writeToCache:(FFFastImageSource *)source
367
+ file:(NSString *)file
368
+ resolve:(RCTPromiseResolveBlock)resolve
369
+ reject:(__unused RCTPromiseRejectBlock)reject)
370
+ {
371
+ if (source.cacheControl == FFFCacheControlWeb) {
372
+ resolve(FFFFailure(@"Can't store cache: 'web' images (they're kept in an HTTP cache)"));
373
+ return;
374
+ }
375
+ // A cacheKey is enough (the image doesn't need a url yet); otherwise a
376
+ // remote url.
377
+ NSString *scheme = source.url.scheme.lowercaseString;
378
+ if (source.cacheKey.length == 0 && ![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"]) {
379
+ resolve(FFFFailure(@"Invalid source: no remote uri or cacheKey"));
380
+ return;
381
+ }
382
+ // A file:// url or a path.
383
+ NSURL *fileURL = [RCTConvert NSURL:file];
384
+ if (!fileURL.isFileURL) {
385
+ resolve(FFFFailure(@"Not a local file"));
386
+ return;
387
+ }
388
+ NSString *key = FFFDiskCacheKey(source);
389
+ FFFFindCachedFile(key, ^(NSString *path) {
390
+ resolve(FFFFailure(@"Already in the disk cache"));
391
+ }, ^{
392
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
393
+ NSData *data = [NSData dataWithContentsOfURL:fileURL];
394
+ if (!data) {
395
+ resolve(FFFFailure(@"Can't read the file"));
396
+ return;
397
+ }
398
+ if ([NSData sd_imageFormatForImageData:data] == SDImageFormatUndefined) {
399
+ resolve(FFFFailure(@"Not an image"));
400
+ return;
401
+ }
402
+ SDImageCache *cache = SDImageCache.sharedImageCache;
403
+ [cache storeImageDataToDisk:data forKey:key];
404
+ // An image a view showed under this key before, which isn't on
405
+ // disk anymore.
406
+ [cache removeImageFromMemoryForKey:key];
407
+ resolve(FFFFile([cache cachePathForKey:key]));
408
+ });
409
+ });
410
+ }
411
+
412
+ // Changes cache limits at runtime and saves them, so they're used on the next
413
+ // launches too: a number (0 for no limit), or null to go back to the app's
414
+ // Info.plist or SDWebImage's default. A lower disk limit applies at once (what's
415
+ // over it is removed; the disk cache is trimmed to half of maxDiskSize when
416
+ // it's over), instead of when the app next goes to the background. Resolves
417
+ // with the limits in effect (0 for no limit), and diskSize: the bytes the disk
418
+ // cache uses now.
419
+ RCT_EXPORT_METHOD(configureCache:(NSDictionary *)limits
420
+ resolve:(RCTPromiseResolveBlock)resolve
421
+ reject:(__unused RCTPromiseRejectBlock)reject)
422
+ {
423
+ SDImageCache *cache = SDImageCache.sharedImageCache;
424
+ SDImageCacheConfig *config = cache.config;
425
+ NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
426
+ NSUInteger maxDiskSize = config.maxDiskSize;
427
+ NSTimeInterval maxDiskAge = config.maxDiskAge;
428
+ for (NSString *name in FFFCacheLimitNames()) {
429
+ id value = limits[name];
430
+ if (value == (id)kCFNull) {
431
+ [defaults removeObjectForKey:FFFSavedCacheLimitKey(name)];
432
+ } else if ([value isKindOfClass:NSNumber.class]) {
433
+ [defaults setObject:value forKey:FFFSavedCacheLimitKey(name)];
434
+ } else {
435
+ // Not given: unchanged.
436
+ continue;
437
+ }
438
+ FFFSetCacheLimit(config, name, FFFCacheLimit(name));
439
+ }
440
+ void (^finish)(void) = ^{
441
+ // Not on the disk cache's queue, which image loads use.
442
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
443
+ resolve(@{
444
+ @"maxDiskSize": @(config.maxDiskSize),
445
+ @"maxDiskAge": @(MAX(config.maxDiskAge, 0)),
446
+ @"maxMemorySize": @(config.maxMemoryCost),
447
+ @"diskSize": @(FFFFolderSize(cache.diskCachePath)),
448
+ });
449
+ });
450
+ };
451
+ if (config.maxDiskSize != maxDiskSize || config.maxDiskAge != maxDiskAge) {
452
+ [cache deleteOldFilesWithCompletionBlock:finish];
453
+ } else {
454
+ finish();
455
+ }
241
456
  }
242
457
 
243
458
  RCT_EXPORT_METHOD(clearMemoryCache:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.18.1",
3
+ "version": "8.19.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",
@@ -29,6 +29,7 @@
29
29
  "ios/FastImage",
30
30
  "ios/FastImage.xcodeproj/project.pbxproj",
31
31
  "ios/FastImage.xcodeproj/xcshareddata",
32
+ "app.plugin.js",
32
33
  "dist",
33
34
  "RNFastImage.podspec"
34
35
  ],