react-native-fast-image 9.2.2 → 9.3.1

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 (28) hide show
  1. package/README.md +36 -7
  2. package/android/src/main/java/com/dylanvann/fastimage/FastImageBlur.java +18 -5
  3. package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +5 -1
  4. package/android/src/main/java/com/dylanvann/fastimage/FastImageSharedDownloads.java +64 -19
  5. package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +22 -0
  6. package/android/src/main/java/com/dylanvann/fastimage/FastImageSvg.java +27 -0
  7. package/android/src/main/java/com/dylanvann/fastimage/FastImageSvgRenderer.java +10 -0
  8. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +5 -0
  9. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +1 -1
  10. package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +157 -14
  11. package/dist/index.cjs.js +50 -18
  12. package/dist/index.cjs.js.flow +3 -0
  13. package/dist/index.cjs.web.js +241 -130
  14. package/dist/index.cjs.web.js.flow +3 -0
  15. package/dist/index.d.ts +36 -6
  16. package/dist/index.js +50 -18
  17. package/dist/index.js.flow +3 -0
  18. package/dist/index.web.js +243 -132
  19. package/dist/index.web.js.flow +3 -0
  20. package/dist/objectFit.d.ts +3 -0
  21. package/ios/FastImage/FFFDownsampledImage.h +8 -0
  22. package/ios/FastImage/FFFDownsampledImage.m +16 -0
  23. package/ios/FastImage/FFFastImageView.h +7 -1
  24. package/ios/FastImage/FFFastImageView.m +10 -5
  25. package/ios/FastImage/FFFastImageViewManager.m +3 -1
  26. package/ios/FastImage/RCTConvert+FFFastImage.h +3 -0
  27. package/ios/FastImage/RCTConvert+FFFastImage.m +11 -0
  28. package/package.json +1 -1
package/README.md CHANGED
@@ -91,7 +91,7 @@ const YourImage = () => (
91
91
  headers: { Authorization: 'someAuthToken' },
92
92
  priority: 'normal',
93
93
  }}
94
- resizeMode="contain"
94
+ objectFit="contain"
95
95
  />
96
96
  )
97
97
  ```
@@ -235,16 +235,36 @@ On Android, `defaultSource` doesn't show in debug builds: there the dev server s
235
235
 
236
236
  ---
237
237
 
238
+ #### `objectFit`
239
+
240
+ **Type:** `ObjectFit` · **Default:** `'cover'`
241
+
242
+ How the image fits the view, as CSS's `object-fit` does.
243
+
244
+ - `'cover'`: scales it uniformly (keeping its aspect ratio) so it covers the view (minus padding), cropping what doesn't fit.
245
+ - `'contain'`: scales it uniformly (keeping its aspect ratio) so all of it fits in the view (minus padding).
246
+ - `'fill'`: scales its width and height separately to fill the view, which can change its aspect ratio.
247
+ - `'none'`: shows it at its own size, centered, cropped if it's larger than the view.
248
+ - `'scale-down'`: shows it at its own size, centered, or scaled down uniformly to fit if it's larger than the view (the smaller of `'none'` and `'contain'`).
249
+
250
+ An image's own size is its size in pixels, as points (dp on Android), as CSS counts an image's pixels: a 300 × 200 image is 300 × 200 points. A bundled image (`require()`) is its size in points.
251
+
252
+ It can also be set in `style`; the prop wins. Either one overrides `resizeMode`.
253
+
254
+ ---
255
+
238
256
  #### `resizeMode`
239
257
 
240
258
  **Type:** `ResizeMode` · **Default:** `'cover'`
241
259
 
260
+ **Deprecated:** Use `objectFit` instead.
261
+
242
262
  How the image fills the view.
243
263
 
244
- - `'contain'`: scales it uniformly (keeping its aspect ratio) so all of it fits in the view (minus padding).
245
- - `'cover'`: scales it uniformly (keeping its aspect ratio) so it covers the view (minus padding), cropping what doesn't fit.
246
- - `'stretch'`: scales its width and height separately to fill the view, which can change its aspect ratio.
247
- - `'center'`: centers it at its own size, scaled down uniformly to fit if it's larger than the view.
264
+ - `'contain'`: as `objectFit="contain"`.
265
+ - `'cover'`: as `objectFit="cover"`.
266
+ - `'stretch'`: as `objectFit="fill"`.
267
+ - `'center'`: as `objectFit="scale-down"`, except that on Android an image smaller than the view is shown at its size in pixels on the screen, so smaller than on iOS and the web.
248
268
  - `'repeat'`: repeats it to cover the view, from its top-left corner, at the image's own size in pixels (a bundled image at its size in points), scaled down to fit if it's larger than the view. An animated image repeats its first frame, and `defaultSource` repeats too.
249
269
 
250
270
  ---
@@ -686,6 +706,14 @@ A load's result, as `onLoadEnd` gets it: `ok` with the image's size, or not `ok`
686
706
 
687
707
  ---
688
708
 
709
+ ### `ObjectFit`
710
+
711
+ **Type:** `'fill' | 'contain' | 'cover' | 'none' | 'scale-down'`
712
+
713
+ How the image fits the view: see [`objectFit`](#objectfit).
714
+
715
+ ---
716
+
689
717
  ### `OnErrorEvent`
690
718
 
691
719
  `onError`'s event: `nativeEvent.error` says what went wrong, e.g. an HTTP status code or an image that can't be decoded.
@@ -826,9 +854,10 @@ FastImage works on the web with [react-native-web](https://necolas.github.io/rea
826
854
 
827
855
  Support is minimal:
828
856
 
829
- - Props that work: `source` (a `uri`, a `require()`d image, or several sizes), `defaultSource`, `resizeMode`, `tintColor`, `blurRadius`, `style`, children, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
857
+ - Props that work: `source` (a `uri`, a `require()`d image, or several sizes), `defaultSource`, `objectFit`, `resizeMode`, `tintColor`, `blurRadius`, `style`, children, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
830
858
  - Ignored: `source.headers` (a browser can't send them for an image), `source.priority`, `source.cache`, `source.cacheKey`, `source.memoryCache`, `recyclingKey`, `loop`, `imageRendering`, `paused`, `transition`, `downsample`, `fallback` and `onProgress`.
831
- - Several sizes are shown with a lazily loaded `<img>` whose `srcset` lists them, with `sizes="auto, 100vw"`: the browser loads the one for the width the image is shown at, in device pixels (usually the smallest that's at least as wide). Browsers that don't support `sizes="auto"` use the viewport's width. `tintColor`, `defaultSource` and `resizeMode="repeat"` don't apply to them.
859
+ - The image is an `<img>` element that fills the view inside its borders, with `objectFit` as its CSS `object-fit` (and `resizeMode`'s `stretch` as `fill`, `center` as `scale-down`). With `resizeMode="repeat"`, it's a repeated CSS background, with a transparent `<img>` over it. `tintColor` is an SVG filter and `blurRadius` a CSS `blur()`, and `defaultSource` is another `<img>`, under the image until it has loaded. A new `source` gets a new `<img>`, so the previous image doesn't stay while it loads (with several sizes, the browser shows it until the new one has loaded).
860
+ - Several sizes are shown with a lazily loaded `<img>` whose `srcset` lists them, with `sizes="auto, 100vw"`: the browser loads the one for the width the image is shown at, in device pixels (usually the smallest that's at least as wide). Browsers that don't support `sizes="auto"` use the viewport's width. `resizeMode="repeat"` shows them as `cover`.
832
861
  - `FastImage.preload` loads the images into the browser's cache and resolves with a result per source. `clearMemoryCache`, `clearDiskCache` and `configureCache` resolve without doing anything (the browser manages its cache), and `getCachePath` and `writeToCache` resolve with `{ ok: false, error: 'Not supported on the web' }`.
833
862
 
834
863
  ## Troubleshooting
@@ -32,10 +32,17 @@ final class FastImageBlur extends BitmapTransformation {
32
32
  // In pixels on screen.
33
33
  private final float radius;
34
34
  private final ImageView.ScaleType scaleType;
35
+ // Pixels on screen per pixel of the image at its own size (scale-down and
36
+ // none show it at that size: see FastImageViewWithUrl.updateImageMatrix;
37
+ // 1 for resizeMode center).
38
+ private final float density;
35
39
 
36
- FastImageBlur(float radius, ImageView.ScaleType scaleType) {
40
+ FastImageBlur(float radius, ImageView.ScaleType scaleType, float density) {
37
41
  this.radius = radius;
38
42
  this.scaleType = scaleType;
43
+ this.density = scaleType == ImageView.ScaleType.CENTER_INSIDE || scaleType == ImageView.ScaleType.CENTER
44
+ ? density
45
+ : 1;
39
46
  }
40
47
 
41
48
  @Override
@@ -80,7 +87,9 @@ final class FastImageBlur extends BitmapTransformation {
80
87
  case FIT_XY:
81
88
  return (float) Math.sqrt(x * y);
82
89
  case CENTER_INSIDE:
83
- return Math.min(Math.min(x, y), 1);
90
+ return Math.min(Math.min(x, y), density);
91
+ case CENTER:
92
+ return density;
84
93
  default:
85
94
  return 1;
86
95
  }
@@ -197,17 +206,21 @@ final class FastImageBlur extends BitmapTransformation {
197
206
  public boolean equals(Object o) {
198
207
  return o instanceof FastImageBlur
199
208
  && ((FastImageBlur) o).radius == radius
200
- && ((FastImageBlur) o).scaleType == scaleType;
209
+ && ((FastImageBlur) o).scaleType == scaleType
210
+ && ((FastImageBlur) o).density == density;
201
211
  }
202
212
 
203
213
  @Override
204
214
  public int hashCode() {
205
- return ID.hashCode() * 31 + Float.floatToIntBits(radius) * 31 + (scaleType == null ? 0 : scaleType.ordinal());
215
+ return (ID.hashCode() * 31 + Float.floatToIntBits(radius) * 31 + (scaleType == null ? 0 : scaleType.ordinal())) * 31
216
+ + Float.floatToIntBits(density);
206
217
  }
207
218
 
208
219
  @Override
209
220
  public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
210
221
  messageDigest.update(ID_BYTES);
211
- messageDigest.update((radius + "," + scaleType).getBytes(Charset.forName("UTF-8")));
222
+ String key = radius + "," + scaleType;
223
+ if (density != 1) key += "," + density;
224
+ messageDigest.update(key.getBytes(Charset.forName("UTF-8")));
212
225
  }
213
226
  }
@@ -82,6 +82,8 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
82
82
  boolean local = !(model instanceof FastImageUrl);
83
83
  int[] size = FastImageSourceSize.get(resource, model, local,
84
84
  dataSource == DataSource.RESOURCE_DISK_CACHE);
85
+ // Before the view shows it (the target does once this returns).
86
+ view.onImageSize(size);
85
87
  if (size != null) {
86
88
  sendLoad(view, size);
87
89
  return false;
@@ -92,7 +94,9 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
92
94
  FastImageSourceSize.readLocal(view.getContext(), source, resource, model, new FastImageSourceSize.Callback() {
93
95
  @Override
94
96
  public void onSize(int[] size) {
95
- if (target.getRequest() == request) sendLoad(view, size);
97
+ if (target.getRequest() != request) return;
98
+ view.onImageSize(size);
99
+ sendLoad(view, size);
96
100
  }
97
101
  });
98
102
  return false;
@@ -22,10 +22,13 @@ import java.util.ArrayList;
22
22
  import java.util.Arrays;
23
23
  import java.util.Comparator;
24
24
  import java.util.HashMap;
25
+ import java.util.HashSet;
26
+ import java.util.Iterator;
25
27
  import java.util.List;
26
28
  import java.util.Map;
27
- import java.util.PriorityQueue;
29
+ import java.util.Set;
28
30
  import java.util.TreeMap;
31
+ import java.util.TreeSet;
29
32
  import java.util.concurrent.ConcurrentHashMap;
30
33
  import java.util.concurrent.LinkedBlockingQueue;
31
34
  import java.util.concurrent.ThreadFactory;
@@ -64,13 +67,18 @@ import okio.BufferedSource;
64
67
  // once every request has read what it needed.
65
68
  //
66
69
  // At most MAX_RUNNING downloads run at once, as SDWebImage's downloader limits
67
- // them; the others wait to send their request, most urgent first (Glide's
68
- // priority: views before preloads). A running download's response is read as
69
- // soon as it arrives (not on OkHttp's dispatcher, which React Native's
70
- // networking shares): a response left unread on an HTTP/2 connection would
71
- // hold back the others on it. A download that stalls fails like any other,
72
- // with the timeouts of the clients (FastImageOkHttpProgressGlideModule), which
73
- // only count once it has started.
70
+ // them, and at most as many wait for their response from a host as the
71
+ // client's dispatcher sends to a host at once (5 by default): OkHttp would
72
+ // hold the others in the order they came, where a more urgent request can't
73
+ // move them up. The others wait to send their request, most urgent first
74
+ // (Glide's priority: views before preloads), and the most urgent one waiting
75
+ // for a host's room keeps a running slot, so less urgent downloads to other
76
+ // hosts can't take them all. A running download's response is read as soon
77
+ // as it arrives, on its own thread (OkHttp counts a request to its host until
78
+ // its callback returns): a response left unread on an HTTP/2 connection
79
+ // would hold back the others on it. A download that stalls fails like any
80
+ // other, with the timeouts of the clients (FastImageOkHttpProgressGlideModule),
81
+ // which only count once it has started.
74
82
  final class FastImageSharedDownloads {
75
83
  // The longest response kept in memory, and the most bytes held in memory
76
84
  // for downloads at once: encoded images (not decoded bitmaps, which
@@ -98,9 +106,9 @@ final class FastImageSharedDownloads {
98
106
  // lock).
99
107
  private static final Map<String, Download> downloads = new HashMap<>();
100
108
  // Guarded by downloads: downloads waiting to start, most urgent first, then
101
- // in the order they were asked for; how many are running; and the bytes
102
- // held in memory.
103
- private static final PriorityQueue<Download> queue = new PriorityQueue<>(11, new Comparator<Download>() {
109
+ // in the order they were asked for; how many are running, and how many
110
+ // wait for their response from each host; and the bytes held in memory.
111
+ private static final TreeSet<Download> queue = new TreeSet<>(new Comparator<Download>() {
104
112
  @Override
105
113
  public int compare(Download a, Download b) {
106
114
  int byPriority = a.priority.compareTo(b.priority);
@@ -109,6 +117,7 @@ final class FastImageSharedDownloads {
109
117
  });
110
118
  private static long orders = 0;
111
119
  private static int running = 0;
120
+ private static final Map<String, Integer> sentTo = new HashMap<>();
112
121
  private static long held = 0;
113
122
  // Stores not yet released (see busy()).
114
123
  private static final AtomicInteger open = new AtomicInteger();
@@ -165,9 +174,10 @@ final class FastImageSharedDownloads {
165
174
  static String busy() {
166
175
  synchronized (downloads) {
167
176
  int stores = open.get();
168
- if (downloads.isEmpty() && queue.isEmpty() && running == 0 && held == 0 && stores == 0) return null;
177
+ if (downloads.isEmpty() && queue.isEmpty() && running == 0 && sentTo.isEmpty() && held == 0
178
+ && stores == 0) return null;
169
179
  return downloads.size() + " downloads, " + queue.size() + " waiting, " + running + " running, "
170
- + held + " bytes in memory, " + stores + " stores open";
180
+ + sentTo + " waiting for a response, " + held + " bytes in memory, " + stores + " stores open";
171
181
  }
172
182
  }
173
183
 
@@ -253,13 +263,25 @@ final class FastImageSharedDownloads {
253
263
  }
254
264
  }
255
265
 
256
- // Starts the downloads waiting that can run now.
266
+ // Starts the downloads waiting that can run now, most urgent first: those
267
+ // whose host has room, leaving a slot for each host without (see above).
257
268
  private static void startQueued() {
258
269
  List<Download> starting = new ArrayList<>();
259
270
  List<Request> requests = new ArrayList<>();
260
271
  synchronized (downloads) {
261
- while (running < MAX_RUNNING && !queue.isEmpty()) {
262
- Download download = queue.poll();
272
+ Set<String> full = new HashSet<>();
273
+ Iterator<Download> waiting = queue.iterator();
274
+ while (running + full.size() < MAX_RUNNING && waiting.hasNext()) {
275
+ Download download = waiting.next();
276
+ String host = download.request.url().host();
277
+ Integer sent = sentTo.get(host);
278
+ if (sent != null && sent >= download.client.dispatcher().getMaxRequestsPerHost()) {
279
+ full.add(host);
280
+ continue;
281
+ }
282
+ waiting.remove();
283
+ sentTo.put(host, sent == null ? 1 : sent + 1);
284
+ download.host = host;
263
285
  download.queued = false;
264
286
  download.running = true;
265
287
  running++;
@@ -539,6 +561,10 @@ final class FastImageSharedDownloads {
539
561
  private final long order;
540
562
  private boolean queued = false;
541
563
  private boolean running = false;
564
+ // Guarded by downloads. The host its request was sent to, until its
565
+ // response (or failure) arrives.
566
+ @Nullable
567
+ private String host;
542
568
  // Guarded by this. The call, once started, and whether the download
543
569
  // was cancelled (before it started, it doesn't).
544
570
  @Nullable
@@ -585,7 +611,9 @@ final class FastImageSharedDownloads {
585
611
  return;
586
612
  }
587
613
  Download download;
588
- boolean queuedOne = false;
614
+ // Queued one, or moved a waiting one to another url (whose host
615
+ // can have room).
616
+ boolean queueChanged = false;
589
617
  InputStream ready = null;
590
618
  synchronized (downloads) {
591
619
  // Cancelled before it got here.
@@ -601,7 +629,7 @@ final class FastImageSharedDownloads {
601
629
  downloads.put(fetcher.key, download);
602
630
  download.queued = true;
603
631
  queue.add(download);
604
- queuedOne = true;
632
+ queueChanged = true;
605
633
  } else if (download.queued) {
606
634
  // A more urgent request moves it up (until it leaves).
607
635
  if (fetcher.priority.compareTo(download.priority) < 0) download.reprioritize(fetcher.priority);
@@ -612,6 +640,7 @@ final class FastImageSharedDownloads {
612
640
  download.url = fetcher.url;
613
641
  download.client = fetcher.client;
614
642
  download.request = request;
643
+ queueChanged = true;
615
644
  }
616
645
  }
617
646
  fetcher.download = download;
@@ -627,7 +656,7 @@ final class FastImageSharedDownloads {
627
656
  }
628
657
  }
629
658
  if (ready != null) fetcher.deliver(ready, callback);
630
- if (queuedOne) startQueued();
659
+ if (queueChanged) startQueued();
631
660
  }
632
661
 
633
662
  // Sends the request (off the main thread, and outside the locks:
@@ -656,10 +685,24 @@ final class FastImageSharedDownloads {
656
685
  if (started != null) started.cancel();
657
686
  }
658
687
 
688
+ // With downloads locked: its request has its response, failed, or
689
+ // wasn't sent, so another can be sent to its host.
690
+ private void responded() {
691
+ if (host == null) return;
692
+ int sent = sentTo.get(host) - 1;
693
+ if (sent == 0) {
694
+ sentTo.remove(host);
695
+ } else {
696
+ sentTo.put(host, sent);
697
+ }
698
+ host = null;
699
+ }
700
+
659
701
  // It no longer runs: the next download waiting can start.
660
702
  private void stopped() {
661
703
  synchronized (downloads) {
662
704
  if (!running) return;
705
+ responded();
663
706
  running = false;
664
707
  FastImageSharedDownloads.running--;
665
708
  }
@@ -853,6 +896,7 @@ final class FastImageSharedDownloads {
853
896
  fromHttpCache = response.networkResponse() == null && response.cacheResponse() != null;
854
897
  final long length = body.contentLength();
855
898
  synchronized (downloads) {
899
+ responded();
856
900
  // No progress without a length.
857
901
  if (length <= 0) silent = true;
858
902
  }
@@ -862,6 +906,7 @@ final class FastImageSharedDownloads {
862
906
  readInto(body, length);
863
907
  }
864
908
  });
909
+ startQueued();
865
910
  }
866
911
 
867
912
  // Sends a progress step, unless the download has stopped: checked and
@@ -128,6 +128,8 @@ final class FastImageSourceSize {
128
128
  private static DownsampleStrategy strategy(@Nullable ImageView.ScaleType scaleType) {
129
129
  if (scaleType == null) return DownsampleStrategy.DEFAULT;
130
130
  switch (scaleType) {
131
+ case CENTER:
132
+ return FULL_SIZE;
131
133
  case CENTER_CROP:
132
134
  return DownsampleStrategy.CENTER_OUTSIDE;
133
135
  case CENTER_INSIDE:
@@ -142,6 +144,26 @@ final class FastImageSourceSize {
142
144
  }
143
145
  }
144
146
 
147
+ // At the image's own size (objectFit none shows it at that size, whatever
148
+ // the view's), or scaled down to at most MAX_PIXELS pixels, which a
149
+ // bitmap can be drawn at (Android won't draw one over 100 MB).
150
+ static final DownsampleStrategy FULL_SIZE = new FullSize();
151
+
152
+ private static final float MAX_PIXELS = 4096f * 4096f;
153
+
154
+ private static final class FullSize extends DownsampleStrategy {
155
+ @Override
156
+ public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
157
+ float pixels = (float) sourceWidth * sourceHeight;
158
+ return pixels > MAX_PIXELS ? (float) Math.sqrt(MAX_PIXELS / pixels) : 1;
159
+ }
160
+
161
+ @Override
162
+ public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
163
+ return SampleSizeRounding.QUALITY;
164
+ }
165
+ }
166
+
145
167
  // The image's size, or null when it has to be read from a local image's
146
168
  // header first (see readLocal).
147
169
  @Nullable
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
6
6
  import androidx.annotation.Nullable;
7
7
 
8
8
  import com.bumptech.glide.Registry;
9
+ import com.bumptech.glide.load.Option;
9
10
  import com.bumptech.glide.load.Options;
10
11
  import com.bumptech.glide.load.ResourceDecoder;
11
12
  import com.bumptech.glide.load.engine.Resource;
@@ -17,6 +18,7 @@ import java.io.IOException;
17
18
  import java.io.InputStream;
18
19
  import java.nio.ByteBuffer;
19
20
  import java.nio.charset.StandardCharsets;
21
+ import java.security.MessageDigest;
20
22
  import java.util.Locale;
21
23
 
22
24
  // SVG images (static), drawn with AndroidSVG, which FastImage includes
@@ -36,6 +38,31 @@ final class FastImageSvg {
36
38
  @Nullable
37
39
  private static Boolean available;
38
40
 
41
+ // objectFit scale-down and none show an image at its own size in dp
42
+ // (FastImageViewWithUrl.updateImageMatrix): an SVG is drawn at that size,
43
+ // the screen's pixels per dp (DENSITY) times its own, rather than at its
44
+ // size in pixels and then enlarged. With scale-down (FITS), no larger than
45
+ // the view. Part of the disk cache key, since the bitmap's size depends on
46
+ // them. Unset (0) for the other fits, which draw it for the view's size.
47
+ static final Option<Float> DENSITY = Option.disk(
48
+ "com.dylanvann.fastimage.FastImageSvg.Density", 0f,
49
+ new Option.CacheKeyUpdater<Float>() {
50
+ @Override
51
+ public void update(@NonNull byte[] keyBytes, @NonNull Float value, @NonNull MessageDigest messageDigest) {
52
+ messageDigest.update(keyBytes);
53
+ messageDigest.update(ByteBuffer.allocate(4).putFloat(value).array());
54
+ }
55
+ });
56
+ static final Option<Boolean> FITS = Option.disk(
57
+ "com.dylanvann.fastimage.FastImageSvg.Fits", false,
58
+ new Option.CacheKeyUpdater<Boolean>() {
59
+ @Override
60
+ public void update(@NonNull byte[] keyBytes, @NonNull Boolean value, @NonNull MessageDigest messageDigest) {
61
+ messageDigest.update(keyBytes);
62
+ messageDigest.update((byte) (value ? 1 : 0));
63
+ }
64
+ });
65
+
39
66
  private FastImageSvg() {
40
67
  }
41
68
 
@@ -70,6 +70,16 @@ final class FastImageSvgRenderer {
70
70
  float scale = strategy != null
71
71
  ? strategy.getScaleFactor(sourceWidth, sourceHeight, targetWidth, targetHeight)
72
72
  : 1f;
73
+ // objectFit scale-down and none: at its own size in dp (see
74
+ // FastImageSvg.DENSITY), and for scale-down no larger than the view.
75
+ Float density = options.get(FastImageSvg.DENSITY);
76
+ if (density != null && density > 0) {
77
+ scale = density;
78
+ if (Boolean.TRUE.equals(options.get(FastImageSvg.FITS))) {
79
+ scale = Math.min(scale, Math.min(
80
+ targetWidth / (float) sourceWidth, targetHeight / (float) sourceHeight));
81
+ }
82
+ }
73
83
  if (Float.isNaN(scale) || Float.isInfinite(scale) || scale <= 0) scale = 1f;
74
84
  float pixels = sourceWidth * scale * sourceHeight * scale;
75
85
  if (pixels > MAX_PIXELS) scale *= (float) Math.sqrt(MAX_PIXELS / pixels);
@@ -49,6 +49,11 @@ class FastImageViewConverter {
49
49
  put("cover", ScaleType.CENTER_CROP);
50
50
  put("stretch", ScaleType.FIT_XY);
51
51
  put("center", ScaleType.CENTER_INSIDE);
52
+ // objectFit="scale-down": as center, laid out in dp (see
53
+ // FastImageViewWithUrl.setResizeMode).
54
+ put("scale-down", ScaleType.CENTER_INSIDE);
55
+ // objectFit="none": the image at its own size, centered.
56
+ put("none", ScaleType.CENTER);
52
57
  }};
53
58
 
54
59
  // Whether the source has a uri to load (not null, missing or blank).
@@ -184,7 +184,7 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> {
184
184
  boolean repeat = "repeat".equals(resizeMode);
185
185
  final FastImageViewWithUrl.ScaleType scaleType =
186
186
  repeat ? FastImageViewWithUrl.ScaleType.FIT_XY : FastImageViewConverter.getScaleType(resizeMode);
187
- view.setResizeMode(scaleType, repeat);
187
+ view.setResizeMode(scaleType, repeat, "center".equals(resizeMode));
188
188
  }
189
189
 
190
190
  @Override