react-native-fast-image 8.6.32 → 8.6.33

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
@@ -176,7 +176,7 @@ e.g. `onProgress={e => console.log(e.nativeEvent.loaded / e.nativeEvent.total)}`
176
176
 
177
177
  ### `onLoad?: (event) => void`
178
178
 
179
- Called on a successful image fetch. Called with the width and height of the loaded image.
179
+ Called on a successful image fetch. Called with the width and height of the image itself, not of the view (on iOS, in points: an `@2x` asset reports half its pixel size).
180
180
 
181
181
  e.g. `onLoad={e => console.log(e.nativeEvent.width, e.nativeEvent.height)}`
182
182
 
@@ -4,6 +4,8 @@ import android.graphics.drawable.Drawable;
4
4
 
5
5
  import com.bumptech.glide.load.DataSource;
6
6
  import com.bumptech.glide.load.engine.GlideException;
7
+ import com.bumptech.glide.load.model.GlideUrl;
8
+ import com.bumptech.glide.request.Request;
7
9
  import com.bumptech.glide.request.RequestListener;
8
10
  import com.bumptech.glide.request.target.ImageViewTarget;
9
11
  import com.bumptech.glide.request.target.Target;
@@ -17,16 +19,11 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
17
19
  static final String REACT_ON_LOAD_EVENT = "onFastImageLoad";
18
20
  static final String REACT_ON_LOAD_END_EVENT = "onFastImageLoadEnd";
19
21
  private final String key;
22
+ private final FastImageSource source;
20
23
 
21
- FastImageRequestListener(String key) {
24
+ FastImageRequestListener(String key, FastImageSource source) {
22
25
  this.key = key;
23
- }
24
-
25
- private static WritableMap mapFromResource(Drawable resource) {
26
- WritableMap resourceData = new WritableNativeMap();
27
- resourceData.putInt("width", resource.getIntrinsicWidth());
28
- resourceData.putInt("height", resource.getIntrinsicHeight());
29
- return resourceData;
26
+ this.source = source;
30
27
  }
31
28
 
32
29
  @Override
@@ -48,19 +45,41 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
48
45
  }
49
46
 
50
47
  @Override
51
- public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
48
+ public boolean onResourceReady(Drawable resource, Object model, final Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
52
49
  if (!(target instanceof ImageViewTarget)) {
53
50
  return false;
54
51
  }
55
- FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
52
+ final FastImageViewWithUrl view = (FastImageViewWithUrl) ((ImageViewTarget) target).getView();
53
+ boolean local = !(model instanceof GlideUrl);
54
+ int[] size = FastImageSourceSize.get(resource, model, local,
55
+ dataSource == DataSource.RESOURCE_DISK_CACHE);
56
+ if (size != null) {
57
+ sendLoad(view, size);
58
+ return false;
59
+ }
60
+ // A local image whose size isn't known: read it from the image first,
61
+ // unless the view starts another load meanwhile.
62
+ final Request request = target.getRequest();
63
+ FastImageSourceSize.readLocal(view.getContext(), source, resource, model, new FastImageSourceSize.Callback() {
64
+ @Override
65
+ public void onSize(int[] size) {
66
+ if (target.getRequest() == request) sendLoad(view, size);
67
+ }
68
+ });
69
+ return false;
70
+ }
71
+
72
+ private static void sendLoad(FastImageViewWithUrl view, int[] size) {
56
73
  ReactContext context = FastImageViewManager.getReactContext(view.getContext());
57
74
  if (context == null) {
58
- return false;
75
+ return;
59
76
  }
60
77
  RCTEventEmitter eventEmitter = context.getJSModule(RCTEventEmitter.class);
61
78
  int viewId = view.getId();
62
- eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_EVENT, mapFromResource(resource));
79
+ WritableMap event = new WritableNativeMap();
80
+ event.putInt("width", size[0]);
81
+ event.putInt("height", size[1]);
82
+ eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_EVENT, event);
63
83
  eventEmitter.receiveEvent(viewId, REACT_ON_LOAD_END_EVENT, new WritableNativeMap());
64
- return false;
65
84
  }
66
85
  }
@@ -0,0 +1,237 @@
1
+ package com.dylanvann.fastimage;
2
+
3
+ import android.content.Context;
4
+ import android.graphics.BitmapFactory;
5
+ import android.graphics.drawable.Drawable;
6
+ import android.net.Uri;
7
+ import android.os.Handler;
8
+ import android.os.Looper;
9
+ import android.util.Base64;
10
+ import android.util.LruCache;
11
+ import android.widget.ImageView;
12
+
13
+ import androidx.annotation.Nullable;
14
+
15
+ import com.bumptech.glide.Glide;
16
+ import com.bumptech.glide.load.ImageHeaderParserUtils;
17
+ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
18
+ import com.bumptech.glide.load.resource.gif.GifDrawable;
19
+ import com.bumptech.glide.request.RequestOptions;
20
+
21
+ import java.io.ByteArrayInputStream;
22
+ import java.io.IOException;
23
+ import java.io.InputStream;
24
+ import java.nio.ByteBuffer;
25
+ import java.util.concurrent.Executor;
26
+ import java.util.concurrent.Executors;
27
+
28
+ // Works out an image's own size for onLoad, as iOS reports it (#608). Glide
29
+ // decodes images to fit the view, so the loaded drawable is usually smaller
30
+ // than the image.
31
+ final class FastImageSourceSize {
32
+ // Sizes of recently decoded images, for loads from the memory cache, which
33
+ // don't decode.
34
+ private static final LruCache<String, int[]> SIZES = new LruCache<>(500);
35
+ private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
36
+ private static final Handler MAIN = new Handler(Looper.getMainLooper());
37
+
38
+ private FastImageSourceSize() {
39
+ }
40
+
41
+ // Wraps the downsample strategy Glide would use, to record the image's
42
+ // size (after EXIF rotation) when Glide decodes it. Views loading the same
43
+ // image at the same size share one decode, which uses the first view's
44
+ // options, so the size is recorded by image rather than by request.
45
+ static final class Capture extends DownsampleStrategy {
46
+ private final DownsampleStrategy strategy;
47
+ private final String key;
48
+ private boolean recorded;
49
+
50
+ Capture(DownsampleStrategy strategy, String key) {
51
+ this.strategy = strategy;
52
+ this.key = key;
53
+ }
54
+
55
+ @Override
56
+ public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
57
+ // Called again with the sampled size; the first call has the image's.
58
+ if (!recorded) {
59
+ recorded = true;
60
+ SIZES.put(key, new int[]{sourceWidth, sourceHeight});
61
+ }
62
+ return strategy.getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight);
63
+ }
64
+
65
+ @Override
66
+ public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
67
+ return strategy.getSampleSizeRounding(sourceWidth, sourceHeight, requestedWidth, requestedHeight);
68
+ }
69
+
70
+ // Glide's memory cache keys include the strategy, so match the wrapped
71
+ // one to keep sharing cached images between views.
72
+ @Override
73
+ public boolean equals(Object o) {
74
+ return o instanceof Capture && ((Capture) o).strategy.equals(strategy);
75
+ }
76
+
77
+ @Override
78
+ public int hashCode() {
79
+ return strategy.hashCode();
80
+ }
81
+ }
82
+
83
+ // What RequestBuilder.into(ImageView) applies for the view's scale type
84
+ // (it leaves a transformation that's already set alone), with the
85
+ // downsample strategy it implies wrapped by capture.
86
+ static RequestOptions scaleTypeOptions(@Nullable ImageView.ScaleType scaleType, Capture capture) {
87
+ RequestOptions options = new RequestOptions();
88
+ if (scaleType != null) {
89
+ switch (scaleType) {
90
+ case CENTER_CROP:
91
+ options = options.optionalCenterCrop();
92
+ break;
93
+ case CENTER_INSIDE:
94
+ case FIT_XY:
95
+ options = options.optionalCenterInside();
96
+ break;
97
+ case FIT_CENTER:
98
+ case FIT_START:
99
+ case FIT_END:
100
+ options = options.optionalFitCenter();
101
+ break;
102
+ default:
103
+ break;
104
+ }
105
+ }
106
+ return options.downsample(capture);
107
+ }
108
+
109
+ static Capture capture(@Nullable ImageView.ScaleType scaleType, Object model) {
110
+ return new Capture(strategy(scaleType), String.valueOf(model));
111
+ }
112
+
113
+ private static DownsampleStrategy strategy(@Nullable ImageView.ScaleType scaleType) {
114
+ if (scaleType == null) return DownsampleStrategy.DEFAULT;
115
+ switch (scaleType) {
116
+ case CENTER_CROP:
117
+ return DownsampleStrategy.CENTER_OUTSIDE;
118
+ case CENTER_INSIDE:
119
+ case FIT_XY:
120
+ return DownsampleStrategy.CENTER_INSIDE;
121
+ case FIT_CENTER:
122
+ case FIT_START:
123
+ case FIT_END:
124
+ return DownsampleStrategy.FIT_CENTER;
125
+ default:
126
+ return DownsampleStrategy.DEFAULT;
127
+ }
128
+ }
129
+
130
+ // The image's size, or null when it has to be read from a local image's
131
+ // header first (see readLocal).
132
+ @Nullable
133
+ static int[] get(Drawable resource, Object model, boolean local, boolean fromResourceCache) {
134
+ String key = String.valueOf(model);
135
+ if (resource instanceof GifDrawable) {
136
+ // Glide decodes GIFs itself (sampled to fit the view), without the
137
+ // downsample strategy; their header has the size.
138
+ int[] size = gifSize((GifDrawable) resource);
139
+ if (size != null) return size;
140
+ } else if (fromResourceCache) {
141
+ // Glide caches local images already resized for the view, and the
142
+ // decode from there recorded the resized size.
143
+ SIZES.remove(key);
144
+ return null;
145
+ } else {
146
+ int[] size = SIZES.get(key);
147
+ if (size != null) return size;
148
+ if (local) return null;
149
+ }
150
+ return new int[]{resource.getIntrinsicWidth(), resource.getIntrinsicHeight()};
151
+ }
152
+
153
+ @Nullable
154
+ private static int[] gifSize(GifDrawable gif) {
155
+ ByteBuffer buffer = gif.getBuffer();
156
+ if (buffer == null || buffer.limit() < 10) return null;
157
+ // The logical screen width and height (little-endian) follow the
158
+ // 6-byte signature.
159
+ int width = (buffer.get(6) & 0xff) | (buffer.get(7) & 0xff) << 8;
160
+ int height = (buffer.get(8) & 0xff) | (buffer.get(9) & 0xff) << 8;
161
+ return new int[]{width, height};
162
+ }
163
+
164
+ interface Callback {
165
+ void onSize(int[] size);
166
+ }
167
+
168
+ // Reads a local image's size from its header, off the main thread.
169
+ static void readLocal(Context context, final FastImageSource source, final Drawable resource, final Object model, final Callback callback) {
170
+ final Context appContext = context.getApplicationContext();
171
+ EXECUTOR.execute(new Runnable() {
172
+ @Override
173
+ public void run() {
174
+ int[] size = readBounds(appContext, source);
175
+ if (size != null) {
176
+ SIZES.put(String.valueOf(model), size);
177
+ } else {
178
+ size = new int[]{resource.getIntrinsicWidth(), resource.getIntrinsicHeight()};
179
+ }
180
+ final int[] result = size;
181
+ MAIN.post(new Runnable() {
182
+ @Override
183
+ public void run() {
184
+ callback.onSize(result);
185
+ }
186
+ });
187
+ }
188
+ });
189
+ }
190
+
191
+ @Nullable
192
+ private static int[] readBounds(Context context, FastImageSource source) {
193
+ try {
194
+ BitmapFactory.Options options = new BitmapFactory.Options();
195
+ options.inJustDecodeBounds = true;
196
+ InputStream stream = open(context, source);
197
+ try {
198
+ BitmapFactory.decodeStream(stream, null, options);
199
+ } finally {
200
+ stream.close();
201
+ }
202
+ if (options.outWidth <= 0 || options.outHeight <= 0) return null;
203
+ int orientation;
204
+ stream = open(context, source);
205
+ try {
206
+ Glide glide = Glide.get(context);
207
+ orientation = ImageHeaderParserUtils.getOrientation(
208
+ glide.getRegistry().getImageHeaderParsers(), stream, glide.getArrayPool());
209
+ } finally {
210
+ stream.close();
211
+ }
212
+ // EXIF orientations 5 to 8 rotate by 90 or 270 degrees.
213
+ boolean rotated = orientation >= 5 && orientation <= 8;
214
+ return rotated
215
+ ? new int[]{options.outHeight, options.outWidth}
216
+ : new int[]{options.outWidth, options.outHeight};
217
+ } catch (IOException | RuntimeException e) {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ private static InputStream open(Context context, FastImageSource source) throws IOException {
223
+ if (source.isBase64Resource()) {
224
+ String data = source.getSource();
225
+ byte[] bytes = Base64.decode(data.substring(data.indexOf(',') + 1), Base64.DEFAULT);
226
+ return new ByteArrayInputStream(bytes);
227
+ }
228
+ Uri uri = source.getUri();
229
+ String path = uri.getPath();
230
+ if ("file".equals(uri.getScheme()) && path != null && path.startsWith("/android_asset/")) {
231
+ return context.getAssets().open(path.substring("/android_asset/".length()));
232
+ }
233
+ InputStream stream = context.getContentResolver().openInputStream(uri);
234
+ if (stream == null) throw new IOException("Can't open " + uri);
235
+ return stream;
236
+ }
237
+ }
@@ -152,6 +152,9 @@ class FastImageViewWithUrl extends AppCompatImageView {
152
152
  }
153
153
 
154
154
  if (requestManager != null) {
155
+ // Records the image's own size when Glide decodes it, for onLoad.
156
+ Object model = imageSource == null ? null : imageSource.getSourceForLoad();
157
+ FastImageSourceSize.Capture capture = FastImageSourceSize.capture(getScaleType(), model);
155
158
  RequestBuilder<Drawable> builder =
156
159
  requestManager
157
160
  // This will make this work for remote and local images. e.g.
@@ -160,14 +163,17 @@ class FastImageViewWithUrl extends AppCompatImageView {
160
163
  // - res:/
161
164
  // - android.resource://
162
165
  // - data:image/png;base64
163
- .load(imageSource == null ? null : imageSource.getSourceForLoad())
166
+ .load(model)
164
167
  .apply(FastImageViewConverter
165
168
  .getOptions(getContext(), imageSource, mSource)
166
169
  .placeholder(mDefaultSource) // show until loaded
167
- .fallback(mDefaultSource)); // null will not be treated as error
170
+ .fallback(mDefaultSource)) // null will not be treated as error
171
+ // What into() would apply for the scale type, with
172
+ // the size capture.
173
+ .apply(FastImageSourceSize.scaleTypeOptions(getScaleType(), capture));
168
174
 
169
175
  if (key != null)
170
- builder.listener(new FastImageRequestListener(key));
176
+ builder.listener(new FastImageRequestListener(key, imageSource));
171
177
 
172
178
  builder.into(this);
173
179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "8.6.32",
3
+ "version": "8.6.33",
4
4
  "description": "🚩 FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",