react-native-fast-image 8.9.0 → 8.11.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
@@ -186,6 +186,16 @@ Changing it restarts the animation.
186
186
 
187
187
  ---
188
188
 
189
+ ### `imageRendering?: 'auto' | 'smooth' | 'pixelated'`
190
+
191
+ How the image is filtered when it's drawn smaller or larger than its size (like CSS's `image-rendering`):
192
+
193
+ - `'auto'` **(Default)** - The platform's usual filtering.
194
+ - `'smooth'` - iOS only. Keeps a large image drawn much smaller than its size (e.g. a big photo as a thumbnail, or fine lines and text) from looking jagged or noisy. **It uses more memory:** the image is also kept at smaller sizes for drawing, about a third more than the decoded image. On Android it's the same as `'auto'` (images are already decoded at about the view's size there).
195
+ - `'pixelated'` - Sharp pixels, without smoothing, e.g. for pixel art drawn larger than its size. On Android, animated images are still smoothed.
196
+
197
+ ---
198
+
189
199
  ### `onLoadStart?: () => void`
190
200
 
191
201
  Called when the image starts to load.
@@ -208,9 +218,11 @@ e.g. `onLoad={e => console.log(e.nativeEvent.width, e.nativeEvent.height)}`
208
218
 
209
219
  ---
210
220
 
211
- ### `onError?: () => void`
221
+ ### `onError?: (event) => void`
222
+
223
+ Called on an image fetching error, with a message describing it (e.g. the HTTP status code).
212
224
 
213
- Called on an image fetching error.
225
+ e.g. `onError={e => console.log(e.nativeEvent.error)}`
214
226
 
215
227
  ---
216
228
 
@@ -41,11 +41,26 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
41
41
  }
42
42
  FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
43
43
  view.onImageFailed(thumbnail);
44
- FastImageEvents.send(view, REACT_ON_ERROR_EVENT);
44
+ WritableMap event = new WritableNativeMap();
45
+ event.putString("error", errorMessage(e));
46
+ FastImageEvents.send(view, REACT_ON_ERROR_EVENT, event);
45
47
  FastImageEvents.send(view, REACT_ON_LOAD_END_EVENT);
46
48
  return false;
47
49
  }
48
50
 
51
+ // The first root cause's message, e.g. "Not Found, status code: 404".
52
+ // The first root cause's message, e.g. "Not Found, status code: 404" (also
53
+ // for preload results).
54
+ static String errorMessage(@androidx.annotation.Nullable GlideException e) {
55
+ if (e != null) {
56
+ for (Throwable cause : e.getRootCauses()) {
57
+ if (cause.getMessage() != null) return cause.getMessage();
58
+ }
59
+ if (e.getMessage() != null) return e.getMessage();
60
+ }
61
+ return "Failed to load the image";
62
+ }
63
+
49
64
  @Override
50
65
  public boolean onResourceReady(Drawable resource, Object model, final Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
51
66
  if (!(target instanceof ImageViewTarget)) {
@@ -94,6 +94,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
94
94
  view.setLoopCount(loopCount);
95
95
  }
96
96
 
97
+ @ReactProp(name = "imageRendering")
98
+ public void setImageRendering(FastImageViewWithUrl view, @Nullable String imageRendering) {
99
+ view.setImageRendering(imageRendering);
100
+ }
101
+
97
102
  @ReactProp(name = "resizeMode")
98
103
  public void setResizeMode(FastImageViewWithUrl view, String resizeMode) {
99
104
  final FastImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.getScaleType(resizeMode);
@@ -122,7 +122,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
122
122
  .listener(new RequestListener<Drawable>() {
123
123
  @Override
124
124
  public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
125
- results[index] = failure(errorMessage(e));
125
+ results[index] = failure(FastImageRequestListener.errorMessage(e));
126
126
  preloadsInFlight--;
127
127
  finishOne.run();
128
128
  startPendingPreloads();
@@ -164,17 +164,6 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
164
164
  return result;
165
165
  }
166
166
 
167
- // The first root cause's message, e.g. "Not Found, status code: 404".
168
- private static String errorMessage(@Nullable GlideException e) {
169
- if (e != null) {
170
- for (Throwable cause : e.getRootCauses()) {
171
- if (cause.getMessage() != null) return cause.getMessage();
172
- }
173
- if (e.getMessage() != null) return e.getMessage();
174
- }
175
- return "Failed to load the image";
176
- }
177
-
178
167
  @ReactMethod
179
168
  public void clearMemoryCache(final Promise promise) {
180
169
  final Activity activity = getCurrentActivity();
@@ -5,6 +5,7 @@ import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_LOAD_END
5
5
 
6
6
  import android.annotation.SuppressLint;
7
7
  import android.content.Context;
8
+ import android.graphics.drawable.BitmapDrawable;
8
9
  import android.graphics.drawable.Drawable;
9
10
 
10
11
  import androidx.annotation.NonNull;
@@ -15,8 +16,10 @@ import androidx.core.view.ViewCompat;
15
16
  import com.bumptech.glide.RequestBuilder;
16
17
  import com.bumptech.glide.RequestManager;
17
18
  import com.bumptech.glide.load.model.GlideUrl;
19
+ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
18
20
  import com.bumptech.glide.load.resource.gif.GifDrawable;
19
21
  import com.bumptech.glide.request.Request;
22
+ import com.bumptech.glide.request.RequestOptions;
20
23
  import com.bumptech.glide.request.target.DrawableImageViewTarget;
21
24
  import com.bumptech.glide.request.target.SizeReadyCallback;
22
25
  import com.bumptech.glide.request.transition.Transition;
@@ -95,6 +98,13 @@ class FastImageViewWithUrl extends AppCompatImageView {
95
98
 
96
99
  @Override
97
100
  public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
101
+ if (resource instanceof BitmapDrawable) {
102
+ if (mPixelated) {
103
+ // Scaled by the view without filtering: sharp pixels.
104
+ resource = resource.mutate();
105
+ resource.setFilterBitmap(false);
106
+ }
107
+ }
98
108
  if (resource instanceof GifDrawable) {
99
109
  GifDrawable own = FastImageGif.copy(
100
110
  getContext(), (GifDrawable) resource, mLoadingWidth, mLoadingHeight);
@@ -134,6 +144,33 @@ class FastImageViewWithUrl extends AppCompatImageView {
134
144
  mNeedsReload = true;
135
145
  }
136
146
 
147
+ // imageRendering="pixelated": no resampling by Glide, and drawn without
148
+ // filtering (sharp pixels). "smooth" is iOS only; here it's the same as
149
+ // "auto": Glide already decodes a large image at about the view's size, and
150
+ // averaging it properly would need a full-size decode. Part of the
151
+ // request, so a change reloads.
152
+ private boolean mPixelated = false;
153
+
154
+ public void setImageRendering(@Nullable String imageRendering) {
155
+ boolean pixelated = "pixelated".equals(imageRendering);
156
+ if (pixelated == mPixelated) return;
157
+ mPixelated = pixelated;
158
+ mNeedsReload = true;
159
+ }
160
+
161
+ // The scale type's options (what into() would apply), or none when
162
+ // pixelated.
163
+ private RequestOptions renderingOptions(Object model) {
164
+ if (mPixelated) {
165
+ // Decoded as it is, then scaled by the view without filtering (see
166
+ // OwnGifTarget).
167
+ return new RequestOptions()
168
+ .downsample(new FastImageSourceSize.Capture(DownsampleStrategy.NONE, String.valueOf(model)))
169
+ .dontTransform();
170
+ }
171
+ return FastImageSourceSize.scaleTypeOptions(getScaleType(), FastImageSourceSize.capture(getScaleType(), model));
172
+ }
173
+
137
174
  // The request for the image the view shows once it has loaded, and the one
138
175
  // loading. A new source starts with the shown one as a thumbnail, from the
139
176
  // cache only, so the image stays until the new one has loaded instead of
@@ -308,7 +345,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
308
345
  // (#1028, #945).
309
346
  if (mSource != null && (imageSource == null || imageSource.getUri().toString().length() == 0)) {
310
347
  WritableMap event = new WritableNativeMap();
311
- event.putString("message", "Invalid source prop:" + mSource);
348
+ event.putString("error", "Invalid source: " + mSource);
312
349
  FastImageEvents.send(this, REACT_ON_ERROR_EVENT, event);
313
350
  FastImageEvents.send(this, REACT_ON_LOAD_END_EVENT);
314
351
 
@@ -353,7 +390,6 @@ class FastImageViewWithUrl extends AppCompatImageView {
353
390
  if (requestManager != null) {
354
391
  // Records the image's own size when Glide decodes it, for onLoad.
355
392
  Object model = imageSource == null ? null : imageSource.getSourceForLoad();
356
- FastImageSourceSize.Capture capture = FastImageSourceSize.capture(getScaleType(), model);
357
393
  RequestBuilder<Drawable> builder =
358
394
  requestManager
359
395
  // This will make this work for remote and local images. e.g.
@@ -368,8 +404,8 @@ class FastImageViewWithUrl extends AppCompatImageView {
368
404
  .placeholder(mDefaultSource) // show until loaded
369
405
  .fallback(mDefaultSource)) // null will not be treated as error
370
406
  // What into() would apply for the scale type, with
371
- // the size capture.
372
- .apply(FastImageSourceSize.scaleTypeOptions(getScaleType(), capture));
407
+ // the size capture, for imageRendering.
408
+ .apply(renderingOptions(model));
373
409
  RequestBuilder<Drawable> request = builder.clone();
374
410
 
375
411
  boolean thumbnail = shownRequest != null && model != null;
@@ -13,6 +13,12 @@ export type OnLoadEvent = SyntheticEvent<
13
13
  }>,
14
14
  >
15
15
 
16
+ export type OnErrorEvent = SyntheticEvent<
17
+ $ReadOnly<{|
18
+ error: string,
19
+ |}>,
20
+ >
21
+
16
22
  export type OnProgressEvent = SyntheticEvent<
17
23
  $ReadOnly<{|
18
24
  loaded: number,
@@ -70,7 +76,7 @@ export type FastImageSource = {
70
76
 
71
77
  export type FastImageProps = $ReadOnly<{|
72
78
  ...ViewProps,
73
- onError?: ?() => void,
79
+ onError?: ?(event: OnErrorEvent) => void,
74
80
  onLoad?: ?(event: OnLoadEvent) => void,
75
81
  onLoadEnd?: ?() => void,
76
82
  onLoadStart?: ?() => void,
@@ -84,6 +90,7 @@ export type FastImageProps = $ReadOnly<{|
84
90
  fallback?: ?boolean,
85
91
  recyclingKey?: ?string,
86
92
  loop?: ?(boolean | number),
93
+ imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
87
94
  testID?: ?string,
88
95
  |}>
89
96
 
package/dist/index.d.ts CHANGED
@@ -35,6 +35,11 @@ export interface OnLoadEvent {
35
35
  target?: number;
36
36
  };
37
37
  }
38
+ export interface OnErrorEvent {
39
+ nativeEvent: {
40
+ error: string;
41
+ };
42
+ }
38
43
  export interface OnProgressEvent {
39
44
  nativeEvent: {
40
45
  loaded: number;
@@ -75,10 +80,18 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
75
80
  * or a number of times. Changing it restarts the animation.
76
81
  */
77
82
  loop?: boolean | number;
83
+ /**
84
+ * How the image is filtered when it's drawn smaller or larger than its
85
+ * size: `'auto'` (default, the platform's usual filtering), `'smooth'`
86
+ * (iOS only: keeps a large image drawn much smaller from looking jagged
87
+ * or noisy; uses more memory, see the README) or `'pixelated'` (sharp
88
+ * pixels, e.g. for pixel art).
89
+ */
90
+ imageRendering?: 'auto' | 'smooth' | 'pixelated';
78
91
  onLoadStart?(): void;
79
92
  onProgress?(event: OnProgressEvent): void;
80
93
  onLoad?(event: OnLoadEvent): void;
81
- onError?(): void;
94
+ onError?(event: OnErrorEvent): void;
82
95
  onLoadEnd?(): void;
83
96
  /**
84
97
  * onLayout function
@@ -13,6 +13,12 @@ export type OnLoadEvent = SyntheticEvent<
13
13
  }>,
14
14
  >
15
15
 
16
+ export type OnErrorEvent = SyntheticEvent<
17
+ $ReadOnly<{|
18
+ error: string,
19
+ |}>,
20
+ >
21
+
16
22
  export type OnProgressEvent = SyntheticEvent<
17
23
  $ReadOnly<{|
18
24
  loaded: number,
@@ -70,7 +76,7 @@ export type FastImageSource = {
70
76
 
71
77
  export type FastImageProps = $ReadOnly<{|
72
78
  ...ViewProps,
73
- onError?: ?() => void,
79
+ onError?: ?(event: OnErrorEvent) => void,
74
80
  onLoad?: ?(event: OnLoadEvent) => void,
75
81
  onLoadEnd?: ?() => void,
76
82
  onLoadStart?: ?() => void,
@@ -84,6 +90,7 @@ export type FastImageProps = $ReadOnly<{|
84
90
  fallback?: ?boolean,
85
91
  recyclingKey?: ?string,
86
92
  loop?: ?(boolean | number),
93
+ imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
87
94
  testID?: ?string,
88
95
  |}>
89
96
 
@@ -25,6 +25,13 @@
25
25
  // How many times animated images play: -1 for the file's own loop count (the
26
26
  // `loop` prop not set), 0 for forever, or a number of times.
27
27
  @property (nonatomic, assign) NSInteger loopCount;
28
+ // How the image is filtered when drawn at another size: auto, smooth or
29
+ // pixelated.
30
+ @property (nonatomic, copy) NSString *imageRendering;
28
31
 
29
32
  @end
30
33
 
34
+ // An error's description, with the HTTP status code when there is one.
35
+ FOUNDATION_EXTERN NSString *FFFErrorMessage(NSError *error);
36
+
37
+
@@ -2,6 +2,7 @@
2
2
  #import <SDWebImage/UIImage+MultiFormat.h>
3
3
  #import <SDWebImage/UIView+WebCache.h>
4
4
  #import <React/RCTUtils.h>
5
+ #import <SDWebImage/SDWebImageError.h>
5
6
 
6
7
  @interface FFFastImageView ()
7
8
 
@@ -12,6 +13,7 @@
12
13
  @property(nonatomic, assign) BOOL needsReload;
13
14
 
14
15
  @property(nonatomic, strong) NSDictionary* onLoadEvent;
16
+ @property(nonatomic, strong) NSDictionary* onErrorEvent;
15
17
  // The image before tinting, kept while a tint is applied so the tint can be
16
18
  // changed or removed. nil when there's no tint (super.image is untinted).
17
19
  @property(nonatomic, strong) UIImage* untintedImage;
@@ -51,6 +53,24 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
51
53
  return self;
52
54
  }
53
55
 
56
+ - (void) setImageRendering: (NSString*)imageRendering {
57
+ _imageRendering = [imageRendering copy];
58
+ if ([imageRendering isEqualToString: @"smooth"]) {
59
+ // Trilinear: drawn smaller from mipmaps, so a large image drawn much
60
+ // smaller is smoothed instead of aliased (#445). The mipmaps take
61
+ // about a third more memory than the decoded image.
62
+ self.layer.minificationFilter = kCAFilterTrilinear;
63
+ self.layer.magnificationFilter = kCAFilterLinear;
64
+ } else if ([imageRendering isEqualToString: @"pixelated"]) {
65
+ // Nearest neighbor: sharp pixels, e.g. for pixel art (#926).
66
+ self.layer.minificationFilter = kCAFilterNearest;
67
+ self.layer.magnificationFilter = kCAFilterNearest;
68
+ } else {
69
+ self.layer.minificationFilter = kCAFilterLinear;
70
+ self.layer.magnificationFilter = kCAFilterLinear;
71
+ }
72
+ }
73
+
54
74
  - (void) setLoopCount: (NSInteger)loopCount {
55
75
  if (_loopCount == loopCount) {
56
76
  return;
@@ -120,7 +140,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
120
140
  - (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError {
121
141
  _onFastImageError = onFastImageError;
122
142
  if (self.hasErrored && _onFastImageError) {
123
- _onFastImageError(@{});
143
+ _onFastImageError(self.onErrorEvent);
124
144
  }
125
145
  }
126
146
 
@@ -194,6 +214,26 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
194
214
  [self updateContentMode];
195
215
  }
196
216
 
217
+ // The error's description, with the HTTP status code when there is one (as on
218
+ // Android): SDWebImage keeps the code out of the description. Also for preload
219
+ // results.
220
+ NSString *FFFErrorMessage(NSError *error)
221
+ {
222
+ NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
223
+ if (statusCode) {
224
+ return [NSString stringWithFormat: @"%@, status code: %@", error.localizedDescription, statusCode];
225
+ }
226
+ return error.localizedDescription ?: @"Failed to load the image";
227
+ }
228
+
229
+ - (void) sendOnError: (nullable NSString*)message {
230
+ self.hasErrored = YES;
231
+ self.onErrorEvent = @{ @"error": message ?: @"Failed to load the image" };
232
+ if (self.onFastImageError) {
233
+ self.onFastImageError(self.onErrorEvent);
234
+ }
235
+ }
236
+
197
237
  - (void) sendOnLoad: (UIImage*)image {
198
238
  self.onLoadEvent = @{
199
239
  @"width": [NSNumber numberWithDouble: image.size.width],
@@ -258,10 +298,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
258
298
  // Not decodable: fail like a remote image, showing defaultSource.
259
299
  [self setImage: _defaultSource];
260
300
  self.showsLoadedImage = NO;
261
- self.hasErrored = YES;
262
- if (self.onFastImageError) {
263
- self.onFastImageError(@{});
264
- }
301
+ [self sendOnError: @"Failed to decode the image"];
265
302
  if (self.onFastImageLoadEnd) {
266
303
  self.onFastImageLoadEnd(@{});
267
304
  }
@@ -391,10 +428,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
391
428
  if (error) {
392
429
  // SDWebImage shows the placeholder (defaultSource or nothing).
393
430
  weakSelf.showsLoadedImage = NO;
394
- weakSelf.hasErrored = YES;
395
- if (weakSelf.onFastImageError) {
396
- weakSelf.onFastImageError(@{});
397
- }
431
+ [weakSelf sendOnError: FFFErrorMessage(error)];
398
432
  if (weakSelf.onFastImageLoadEnd) {
399
433
  weakSelf.onFastImageLoadEnd(@{});
400
434
  }
@@ -19,6 +19,7 @@ RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage)
19
19
  RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
20
20
  RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
21
21
  RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
22
+ RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
22
23
  RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
23
24
  RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
24
25
  RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
@@ -26,16 +27,6 @@ RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock)
26
27
  RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadEnd, RCTDirectEventBlock)
27
28
  RCT_REMAP_VIEW_PROPERTY(tintColor, imageColor, UIColor)
28
29
 
29
- // The error's description, with the HTTP status code when there is one.
30
- static NSString *FFFErrorMessage(NSError *error)
31
- {
32
- NSNumber *statusCode = error.userInfo[SDWebImageErrorDownloadStatusCodeKey];
33
- if (statusCode) {
34
- return [NSString stringWithFormat:@"%@, status code: %@", error.localizedDescription, statusCode];
35
- }
36
- return error.localizedDescription ?: @"Failed to load the image";
37
- }
38
-
39
30
  // Preloads waiting to start, in the order they were added, and the number
40
31
  // loading, across all preload calls. Like SDWebImagePrefetcher, at most
41
32
  // maxConcurrentPrefetchCount load at a time, so a long list doesn't queue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.9.0",
3
+ "version": "8.11.0",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",