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/dist/index.web.js CHANGED
@@ -1,5 +1,6 @@
1
- import React, { forwardRef, memo, useEffect, useRef } from "react";
2
- import { Image, StyleSheet, View } from "react-native";
1
+ import React, { forwardRef, memo, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { Image, PixelRatio, StyleSheet, View } from "react-native";
3
+ import { getAssetByID } from "react-native-web/dist/modules/AssetRegistry";
3
4
  //#region src/constants.ts
4
5
  const resizeMode = {
5
6
  contain: "contain",
@@ -19,142 +20,279 @@ const cacheControl = {
19
20
  cacheOnly: "cacheOnly"
20
21
  };
21
22
  //#endregion
23
+ //#region src/objectFit.ts
24
+ function fromStyle(style, key) {
25
+ if (Array.isArray(style)) {
26
+ for (let i = style.length - 1; i >= 0; i--) {
27
+ const found = fromStyle(style[i], key);
28
+ if (found !== void 0) return found;
29
+ }
30
+ return;
31
+ }
32
+ if (typeof style === "number") {
33
+ const flattened = StyleSheet.flatten(style);
34
+ return flattened ? flattened[key] : void 0;
35
+ }
36
+ return style && typeof style === "object" ? style[key] : void 0;
37
+ }
38
+ const RESIZE_MODE_FIT = {
39
+ contain: "contain",
40
+ cover: "cover",
41
+ stretch: "fill",
42
+ center: "center",
43
+ repeat: "repeat"
44
+ };
45
+ const FITS = {
46
+ fill: true,
47
+ contain: true,
48
+ cover: true,
49
+ none: true,
50
+ "scale-down": true
51
+ };
52
+ function resolveObjectFit(objectFit, style, resizeMode) {
53
+ var _RESIZE_MODE_FIT$resi;
54
+ const fit = objectFit != null ? objectFit : fromStyle(style, "objectFit");
55
+ if (fit && FITS[fit] === true) return fit;
56
+ return (_RESIZE_MODE_FIT$resi = RESIZE_MODE_FIT[resizeMode]) !== null && _RESIZE_MODE_FIT$resi !== void 0 ? _RESIZE_MODE_FIT$resi : "cover";
57
+ }
58
+ //#endregion
22
59
  //#region src/index.web.tsx
60
+ var _useId;
23
61
  const notSupported = {
24
62
  ok: false,
25
63
  error: "Not supported on the web"
26
64
  };
27
- function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, resizeMode: mode = "cover", forwardedRef, onLayout, pointerEvents, fallback: _fallback, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
28
- const result = useRef(void 0);
29
- const sent = useRef(Promise.resolve());
30
- const image = useRef(null);
31
- const sizes = Array.isArray(source) && source.length > 1 ? source : void 0;
32
- const single = Array.isArray(source) ? source[0] : source;
33
- const uri = typeof single === "object" && single ? single.uri : void 0;
34
- const webSource = typeof single === "number" ? single : uri ? { uri } : void 0;
65
+ let ids = 0;
66
+ const useCounterId = () => useState(() => `${++ids}`)[0];
67
+ const useSvgId = (_useId = React.useId) !== null && _useId !== void 0 ? _useId : useCounterId;
68
+ const useClientLayoutEffect = globalThis.document === void 0 ? useEffect : useLayoutEffect;
69
+ function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, objectFit, resizeMode: mode = "cover", forwardedRef, onLayout, pointerEvents, fallback: _fallback, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
70
+ const { accessibilityLabel, "aria-label": ariaLabel, ...viewProps } = props;
71
+ const label = ariaLabel || accessibilityLabel || void 0;
72
+ const several = Array.isArray(source) && source.length > 1 ? source : void 0;
73
+ const sized = several ? sizedSources(several) : void 0;
74
+ const src = sized ? sized.src : resolveUri(source);
75
+ const srcSet = sized === null || sized === void 0 ? void 0 : sized.srcSet;
76
+ const key = srcSet !== null && srcSet !== void 0 ? srcSet : src;
77
+ const defaultUri = resolveUri(defaultSource);
78
+ const [result, setResult] = useState();
79
+ const ok = result && result.key === key ? result.ok : void 0;
80
+ const tintId = `fast-image-tint-${useSvgId().replace(/[^\w-]/g, "")}`;
81
+ const resolved = resolveObjectFit(objectFit, style, mode);
82
+ const fit = resolved === "center" ? "scale-down" : several && resolved === "repeat" ? "cover" : resolved;
83
+ const filter = [blurRadius ? `blur(${blurRadius}px)` : "", tintColor ? `url(#${tintId})` : ""].filter(Boolean).join(" ") || void 0;
84
+ const showDefault = !!defaultUri && ok !== true;
85
+ useClientLayoutEffect(() => {
86
+ if (key) onLoadStart === null || onLoadStart === void 0 || onLoadStart();
87
+ }, [key]);
35
88
  return /* @__PURE__ */ React.createElement(View, {
89
+ ...viewProps,
90
+ "aria-label": label,
36
91
  style: [styles.container, style],
37
92
  onLayout,
38
93
  pointerEvents,
39
94
  ref: forwardedRef
40
- }, sizes ? /* @__PURE__ */ React.createElement(View, {
41
- ...props,
42
- style: styles.image
43
- }, /* @__PURE__ */ React.createElement(ImageSizes, {
44
- sources: sizes,
45
- resizeMode: mode,
46
- blurRadius,
47
- onLoadStart,
48
- onLoad,
49
- onError,
50
- onLoadEnd
51
- })) : /* @__PURE__ */ React.createElement(Image, {
52
- ...props,
53
- style: styles.image,
54
- source: webSource,
55
- defaultSource,
56
- resizeMode: mode,
57
- tintColor,
58
- blurRadius,
59
- ref: image,
60
- onLoadStart,
61
- onLoad: (event) => {
62
- var _event$nativeEvent;
63
- sent.current = loadedSize(event === null || event === void 0 || (_event$nativeEvent = event.nativeEvent) === null || _event$nativeEvent === void 0 ? void 0 : _event$nativeEvent.target, image.current, uri).then(({ width, height }) => {
64
- result.current = {
65
- ok: true,
66
- width,
67
- height
68
- };
69
- onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
70
- width,
71
- height
72
- } });
95
+ }, defaultUri && showDefault ? /* @__PURE__ */ React.createElement(Picture, {
96
+ key: `default ${defaultUri}`,
97
+ src: defaultUri,
98
+ fit,
99
+ filter,
100
+ alt: key ? "" : label !== null && label !== void 0 ? label : ""
101
+ }) : null, key && src ? /* @__PURE__ */ React.createElement(Picture, {
102
+ key: several ? "sizes" : key,
103
+ src,
104
+ srcSet,
105
+ lazy: !!several,
106
+ fit,
107
+ filter,
108
+ hidden: ok === false,
109
+ alt: label !== null && label !== void 0 ? label : "",
110
+ onLoad: (image) => {
111
+ const { width, height } = loadedSize(image, sized);
112
+ setResult({
113
+ key,
114
+ ok: true
115
+ });
116
+ onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
117
+ width,
118
+ height
119
+ } });
120
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
121
+ ok: true,
122
+ width,
123
+ height
73
124
  });
74
125
  },
75
- onError: (event) => {
76
- var _event$nativeEvent$er, _event$nativeEvent2;
77
- const error = String((_event$nativeEvent$er = event === null || event === void 0 || (_event$nativeEvent2 = event.nativeEvent) === null || _event$nativeEvent2 === void 0 ? void 0 : _event$nativeEvent2.error) !== null && _event$nativeEvent$er !== void 0 ? _event$nativeEvent$er : "Failed to load the image");
78
- result.current = {
126
+ onError: (uri) => {
127
+ const error = `Failed to load resource ${uri}`;
128
+ setResult({
129
+ key,
130
+ ok: false
131
+ });
132
+ onError === null || onError === void 0 || onError({ nativeEvent: { error } });
133
+ onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
79
134
  ok: false,
80
135
  error
81
- };
82
- sent.current = Promise.resolve();
83
- onError === null || onError === void 0 || onError({ nativeEvent: { error } });
84
- },
85
- onLoadEnd: () => {
86
- sent.current.then(() => {
87
- var _result$current;
88
- onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd((_result$current = result.current) !== null && _result$current !== void 0 ? _result$current : {
89
- ok: false,
90
- error: "Failed to load the image"
91
- });
92
- result.current = void 0;
93
136
  });
94
137
  }
95
- }), children);
138
+ }) : null, tintColor ? /* @__PURE__ */ React.createElement(TintFilter, {
139
+ id: tintId,
140
+ color: tintColor
141
+ }) : null, children);
142
+ }
143
+ function resolveUri(source) {
144
+ let uri;
145
+ if (typeof source === "number") {
146
+ var _asset$scales$;
147
+ const asset = getAssetByID(source);
148
+ if (!asset) return void 0;
149
+ const scale = asset.scales.reduce((best, scale) => Math.abs(scale - PixelRatio.get()) < Math.abs(best - PixelRatio.get()) ? scale : best, (_asset$scales$ = asset.scales[0]) !== null && _asset$scales$ !== void 0 ? _asset$scales$ : 1);
150
+ const suffix = scale !== 1 ? `@${scale}x` : "";
151
+ uri = `${asset.httpServerLocation}/${asset.name}${suffix}.${asset.type}`;
152
+ } else if (typeof source === "string") uri = source;
153
+ else if (Array.isArray(source)) return resolveUri(source[0]);
154
+ else if (source && typeof source.uri === "string") uri = source.uri;
155
+ const svg = uri === null || uri === void 0 ? void 0 : uri.match(/^(data:image\/svg\+xml;utf8,)(.*)/);
156
+ return svg ? svg[1] + encodeURIComponent(svg[2]) : uri || void 0;
96
157
  }
97
- function ImageSizes({ sources, resizeMode: mode, blurRadius, onLoadStart, onLoad, onError, onLoadEnd }) {
158
+ function sizedSources(sources) {
98
159
  var _largest$uri, _sources$find;
99
- const sized = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
160
+ const widths = sources.filter((source) => (source === null || source === void 0 ? void 0 : source.uri) && source.width).map((source) => {
100
161
  var _source$width, _source$scale;
101
162
  return {
102
163
  uri: source.uri,
103
164
  width: Math.round(((_source$width = source.width) !== null && _source$width !== void 0 ? _source$width : 0) * ((_source$scale = source.scale) !== null && _source$scale !== void 0 ? _source$scale : 1))
104
165
  };
105
166
  });
106
- const largest = sized.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
107
- const src = (_largest$uri = largest === null || largest === void 0 ? void 0 : largest.uri) !== null && _largest$uri !== void 0 ? _largest$uri : (_sources$find = sources.find((source) => source === null || source === void 0 ? void 0 : source.uri)) === null || _sources$find === void 0 ? void 0 : _sources$find.uri;
108
- const srcSet = sized.length > 0 ? sized.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0;
167
+ const largest = widths.reduce((best, source) => !best || source.width > best.width ? source : best, void 0);
168
+ return {
169
+ src: (_largest$uri = largest === null || largest === void 0 ? void 0 : largest.uri) !== null && _largest$uri !== void 0 ? _largest$uri : (_sources$find = sources.find((source) => source === null || source === void 0 ? void 0 : source.uri)) === null || _sources$find === void 0 ? void 0 : _sources$find.uri,
170
+ srcSet: widths.length > 0 ? widths.map((source) => `${source.uri} ${source.width}w`).join(", ") : void 0,
171
+ widths
172
+ };
173
+ }
174
+ function loadedSize(image, sized) {
175
+ var _picked$width;
176
+ const picked = (sized === null || sized === void 0 ? void 0 : sized.srcSet) ? sized.widths.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
177
+ return {
178
+ width: (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth,
179
+ height: picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight
180
+ };
181
+ }
182
+ const fill = {
183
+ position: "absolute",
184
+ top: 0,
185
+ left: 0,
186
+ width: "100%",
187
+ height: "100%"
188
+ };
189
+ function Picture({ src, srcSet, lazy, fit, filter, hidden, alt, onLoad, onError }) {
190
+ const repeat = fit === "repeat";
191
+ const image = useRef(null);
192
+ const tiles = useRef(null);
193
+ const mounted = useRef(true);
194
+ const handled = useRef(void 0);
195
+ const [natural, setNatural] = useState();
196
+ const [box, setBox] = useState();
197
+ const loaded = (target) => {
198
+ var _handled$current, _target$decode;
199
+ const file = target.currentSrc || src;
200
+ const sources = srcSet !== null && srcSet !== void 0 ? srcSet : src;
201
+ if (!mounted.current || ((_handled$current = handled.current) === null || _handled$current === void 0 ? void 0 : _handled$current.sources) === sources && handled.current.file === file) return;
202
+ handled.current = {
203
+ sources,
204
+ file
205
+ };
206
+ setNatural({
207
+ width: target.naturalWidth,
208
+ height: target.naturalHeight
209
+ });
210
+ const done = () => {
211
+ if (mounted.current) onLoad === null || onLoad === void 0 || onLoad(target);
212
+ };
213
+ Promise.resolve((_target$decode = target.decode) === null || _target$decode === void 0 ? void 0 : _target$decode.call(target)).then(done, done);
214
+ };
109
215
  useEffect(() => {
110
- onLoadStart === null || onLoadStart === void 0 || onLoadStart();
111
- }, [src, srcSet]);
112
- return /* @__PURE__ */ React.createElement("img", {
216
+ mounted.current = true;
217
+ return () => {
218
+ mounted.current = false;
219
+ };
220
+ }, []);
221
+ useClientLayoutEffect(() => {
222
+ var _target$decode2;
223
+ const target = image.current;
224
+ if (!(target === null || target === void 0 ? void 0 : target.complete)) return;
225
+ if (target.naturalWidth > 0) return loaded(target);
226
+ Promise.resolve((_target$decode2 = target.decode) === null || _target$decode2 === void 0 ? void 0 : _target$decode2.call(target)).then(() => loaded(target), () => {
227
+ if (mounted.current) onError === null || onError === void 0 || onError(target.currentSrc || src);
228
+ });
229
+ }, []);
230
+ useClientLayoutEffect(() => {
231
+ const Observer = globalThis.ResizeObserver;
232
+ const node = tiles.current;
233
+ if (!repeat || !node || !Observer) return;
234
+ const observer = new Observer((entries) => {
235
+ const { width, height } = entries[0].contentRect;
236
+ setBox({
237
+ width,
238
+ height
239
+ });
240
+ });
241
+ observer.observe(node);
242
+ return () => observer.disconnect();
243
+ }, [repeat]);
244
+ const img = /* @__PURE__ */ React.createElement("img", {
245
+ ref: image,
113
246
  src,
114
247
  srcSet,
115
248
  sizes: srcSet ? "auto, 100vw" : void 0,
116
- loading: "lazy",
117
- alt: "",
249
+ loading: lazy ? "lazy" : void 0,
250
+ alt,
118
251
  draggable: false,
119
252
  style: {
120
- position: "absolute",
121
- width: "100%",
122
- height: "100%",
123
- objectFit: OBJECT_FIT[mode !== null && mode !== void 0 ? mode : "cover"],
124
- filter: blurRadius ? `blur(${blurRadius}px)` : void 0
253
+ ...fill,
254
+ objectFit: repeat ? "cover" : fit,
255
+ filter: repeat ? void 0 : filter,
256
+ opacity: repeat || hidden ? 0 : void 0
125
257
  },
126
- onLoad: (event) => {
127
- var _picked$width;
128
- const image = event.currentTarget;
129
- const picked = srcSet ? sized.find((source) => new URL(source.uri, image.currentSrc).href === image.currentSrc) : void 0;
130
- const loadedWidth = (_picked$width = picked === null || picked === void 0 ? void 0 : picked.width) !== null && _picked$width !== void 0 ? _picked$width : image.naturalWidth;
131
- const loadedHeight = picked ? Math.round(image.naturalHeight * picked.width / image.naturalWidth) : image.naturalHeight;
132
- onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
133
- width: loadedWidth,
134
- height: loadedHeight
135
- } });
136
- onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
137
- ok: true,
138
- width: loadedWidth,
139
- height: loadedHeight
140
- });
141
- },
142
- onError: () => {
143
- const error = "Failed to load the image";
144
- onError === null || onError === void 0 || onError({ nativeEvent: { error } });
145
- onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd({
146
- ok: false,
147
- error
148
- });
258
+ onLoad: (event) => loaded(event.currentTarget),
259
+ onError: (event) => {
260
+ const target = event.currentTarget;
261
+ if (mounted.current) onError === null || onError === void 0 || onError((target === null || target === void 0 ? void 0 : target.currentSrc) || src);
149
262
  }
150
263
  });
264
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, repeat ? /* @__PURE__ */ React.createElement("div", {
265
+ ref: tiles,
266
+ style: {
267
+ ...fill,
268
+ backgroundImage: `url("${src}")`,
269
+ backgroundRepeat: "repeat",
270
+ backgroundPosition: "0",
271
+ backgroundSize: tileSize(natural, box),
272
+ filter,
273
+ opacity: hidden ? 0 : void 0
274
+ }
275
+ }) : null, img);
151
276
  }
152
- const OBJECT_FIT = {
153
- cover: "cover",
154
- contain: "contain",
155
- stretch: "fill",
156
- center: "scale-down",
157
- repeat: "cover"
277
+ function tileSize(natural, box) {
278
+ if (!(natural === null || natural === void 0 ? void 0 : natural.width) || !natural.height || !(box === null || box === void 0 ? void 0 : box.width) || !box.height) return;
279
+ const scale = Math.min(1, box.width / natural.width, box.height / natural.height);
280
+ return `${Math.ceil(scale * natural.width)}px ${Math.ceil(scale * natural.height)}px`;
281
+ }
282
+ function TintFilter({ id, color }) {
283
+ return /* @__PURE__ */ React.createElement("svg", { style: tintStyle }, /* @__PURE__ */ React.createElement("defs", null, /* @__PURE__ */ React.createElement("filter", { id }, /* @__PURE__ */ React.createElement("feFlood", {
284
+ floodColor: String(color),
285
+ key: String(color)
286
+ }), /* @__PURE__ */ React.createElement("feComposite", {
287
+ in2: "SourceAlpha",
288
+ operator: "in"
289
+ }))));
290
+ }
291
+ const tintStyle = {
292
+ position: "absolute",
293
+ width: 0,
294
+ height: 0,
295
+ visibility: "hidden"
158
296
  };
159
297
  const FastImageMemo = memo(FastImageBase);
160
298
  const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.createElement(FastImageMemo, {
@@ -162,24 +300,6 @@ const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.crea
162
300
  ...props
163
301
  }));
164
302
  FastImageComponent.displayName = "FastImage";
165
- async function loadedSize(loaded, view, uri) {
166
- var _view$querySelector;
167
- if (loaded === null || loaded === void 0 ? void 0 : loaded.naturalWidth) return {
168
- width: loaded.naturalWidth,
169
- height: loaded.naturalHeight
170
- };
171
- await new Promise((resolve) => setTimeout(resolve, 0));
172
- const shown = view === null || view === void 0 || (_view$querySelector = view.querySelector) === null || _view$querySelector === void 0 ? void 0 : _view$querySelector.call(view, "img");
173
- if (shown === null || shown === void 0 ? void 0 : shown.naturalWidth) return {
174
- width: shown.naturalWidth,
175
- height: shown.naturalHeight
176
- };
177
- const src = uri !== null && uri !== void 0 ? uri : shown === null || shown === void 0 ? void 0 : shown.src;
178
- return src ? getSize(src) : {
179
- width: 0,
180
- height: 0
181
- };
182
- }
183
303
  function getSize(uri) {
184
304
  return new Promise((resolve) => {
185
305
  Image.getSize(uri, (width, height) => resolve({
@@ -217,16 +337,7 @@ FastImage.clearDiskCache = () => Promise.resolve(notSupported);
217
337
  FastImage.configureCache = () => Promise.resolve({});
218
338
  FastImage.writeToCache = () => Promise.resolve(notSupported);
219
339
  FastImage.getCachePath = () => Promise.resolve(notSupported);
220
- const styles = StyleSheet.create({
221
- container: { overflow: "hidden" },
222
- image: {
223
- position: "absolute",
224
- top: 0,
225
- left: 0,
226
- width: "100%",
227
- height: "100%"
228
- }
229
- });
340
+ const styles = StyleSheet.create({ container: { overflow: "hidden" } });
230
341
  const FastImageWithRef = FastImageComponent;
231
342
  const FastImageBackground = forwardRef(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ React.createElement(View, {
232
343
  style,
@@ -49,6 +49,7 @@ export type CacheControl = $ReadOnly<{|
49
49
  |}>
50
50
 
51
51
  export type ResizeModes = $Values<ResizeMode>
52
+ export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
52
53
  export type Priorities = $Values<Priority>
53
54
  export type CacheControls = $Values<CacheControl>
54
55
 
@@ -123,6 +124,8 @@ export type FastImageProps = $ReadOnly<{|
123
124
  defaultSource?: ?number,
124
125
 
125
126
  tintColor?: number | string,
127
+ objectFit?: ?ObjectFit,
128
+ // Deprecated: use objectFit.
126
129
  resizeMode?: ?ResizeModes,
127
130
  fallback?: ?boolean,
128
131
  recyclingKey?: ?string,
@@ -0,0 +1,3 @@
1
+ import type { ImageStyle, ObjectFit, ResizeMode } from './index';
2
+ export declare function fromStyle<K extends 'tintColor' | 'objectFit'>(style: unknown, key: K): ImageStyle[K];
3
+ export declare function resolveObjectFit(objectFit: ObjectFit | null | undefined, style: unknown, resizeMode: ResizeMode): ObjectFit | 'repeat' | 'center';
@@ -25,6 +25,14 @@
25
25
  // loaded this way was decoded from, for onLoad, or zero for other images.
26
26
  + (CGSize) sourceSizeOfImage: (UIImage*)image;
27
27
 
28
+ // SDWebImage puts an image it decoded smaller from the disk cache, for a view
29
+ // that downsamples, into the memory cache under the full-size image's key too
30
+ // (SDWebImageManager writes what its original cache query finds back there),
31
+ // where a full-size load (a view that doesn't downsample, or a preload) would
32
+ // find it: a blurry image, or a preload's size. Drops it there, so that load
33
+ // decodes the file from the disk cache. Call it before a full-size load.
34
+ + (void) forgetSmallerFullSizeImageForURL: (NSURL*)url context: (SDWebImageContext*)context;
35
+
28
36
  // Whether this version of SDWebImage can decode through this class: it
29
37
  // decodes static images through the animated image class since 5.19.
30
38
  + (BOOL) isSupported;
@@ -5,6 +5,8 @@
5
5
  #import <SDWebImage/UIImage+Metadata.h>
6
6
  #import <SDWebImage/NSData+ImageContentType.h>
7
7
  #import <SDWebImage/SDImageCodersManager.h>
8
+ #import <SDWebImage/SDImageCache.h>
9
+ #import <SDWebImage/SDWebImageManager.h>
8
10
 
9
11
  // The decode option that carries the full image's size to the view.
10
12
  static SDImageCoderOption const FFFDecodeSourceSize = @"FFFDecodeSourceSize";
@@ -109,6 +111,20 @@ static CGSize FFFPixelSize(NSData* data) {
109
111
  return [sourceSize isKindOfClass: [FFFSourceSize class]] ? ((FFFSourceSize*) sourceSize).size : CGSizeZero;
110
112
  }
111
113
 
114
+ + (void) forgetSmallerFullSizeImageForURL: (NSURL*)url context: (SDWebImageContext*)context {
115
+ SDWebImageManager* manager = [SDWebImageManager sharedManager];
116
+ if (!url || ![manager.imageCache isKindOfClass: [SDImageCache class]] || ![manager respondsToSelector: @selector(cacheKeyForURL:context:)]) {
117
+ return;
118
+ }
119
+ SDImageCache* cache = (SDImageCache*) manager.imageCache;
120
+ NSString* key = [manager cacheKeyForURL: url context: context];
121
+ UIImage* image = [cache imageFromMemoryCacheForKey: key];
122
+ CGSize fullSize = [self sourceSizeOfImage: image];
123
+ if (image && fullSize.width > 0 && fullSize.height > 0 && !CGSizeEqualToSize(image.size, fullSize)) {
124
+ [cache removeImageFromMemoryForKey: key];
125
+ }
126
+ }
127
+
112
128
  - (instancetype) initWithData: (NSData*)data scale: (CGFloat)scale options: (SDImageCoderOptions*)options {
113
129
  // An SVG isn't decoded smaller: it's a vector image, drawn at any size.
114
130
  // This class can't decode it, and SDWebImage would then give it to the
@@ -8,6 +8,12 @@
8
8
 
9
9
  #import "FFFastImageSource.h"
10
10
 
11
+ // resizeMode: React Native's, and `none` (objectFit="none": the image at its
12
+ // own size, centered, not scaled), which React Native's own resize modes only
13
+ // have from 0.77, with another meaning (top-left).
14
+ typedef RCTResizeMode FFFResizeMode;
15
+ static const FFFResizeMode FFFResizeModeNone = (FFFResizeMode) -2;
16
+
11
17
  @interface FFFastImageView : SDAnimatedImageView
12
18
 
13
19
  @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadStart;
@@ -17,7 +23,7 @@
17
23
  @property (nonatomic, copy) RCTDirectEventBlock onFastImageError;
18
24
  @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoad;
19
25
  @property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadEnd;
20
- @property (nonatomic, assign) RCTResizeMode resizeMode;
26
+ @property (nonatomic, assign) FFFResizeMode resizeMode;
21
27
  @property (nonatomic, strong) FFFastImageSource *source;
22
28
  // Several sources (2 or more) of the same image at different sizes: the view
23
29
  // loads the one whose size is closest to its own.
@@ -224,7 +224,7 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
224
224
  }
225
225
  }
226
226
 
227
- - (void) setResizeMode: (RCTResizeMode)resizeMode {
227
+ - (void) setResizeMode: (FFFResizeMode)resizeMode {
228
228
  if (_resizeMode != resizeMode) {
229
229
  BOOL repeated = _resizeMode == RCTResizeModeRepeat;
230
230
  _resizeMode = resizeMode;
@@ -242,10 +242,13 @@ static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, B
242
242
  // view down to fit, as React Native's Image and Android do (#866); only a
243
243
  // smaller one is shown at its own size. UIViewContentModeCenter alone showed
244
244
  // large images at full size, cropped. So it depends on the image and the
245
- // view's size.
245
+ // view's size. `none` is the image at its own size, centered, whatever its
246
+ // size (cropped by the view).
246
247
  - (void) updateContentMode {
247
248
  UIViewContentMode contentMode = (UIViewContentMode) _resizeMode;
248
- if (_resizeMode == RCTResizeModeRepeat) {
249
+ if (_resizeMode == FFFResizeModeNone) {
250
+ contentMode = UIViewContentModeCenter;
251
+ } else if (_resizeMode == RCTResizeModeRepeat) {
249
252
  // The tiled image (see tiledImage:) fills the view.
250
253
  contentMode = UIViewContentModeScaleToFill;
251
254
  } else if (_resizeMode == RCTResizeModeCenter) {
@@ -803,9 +806,10 @@ NSString *FFFErrorMessage(NSError *error)
803
806
  // Whether images are decoded at about the view's size (downsample, from
804
807
  // SDWebImage 5.19, and always for photo library images, which are large and
805
808
  // usually shown small: Photos makes them at the size asked for, on any
806
- // version). Not for `repeat`, which tiles the image at its own size.
809
+ // version). Not for `repeat`, which tiles the image at its own size, or
810
+ // `none`, which shows it at its own size.
807
811
  - (BOOL) downsamples {
808
- if (_resizeMode == RCTResizeModeRepeat) {
812
+ if (_resizeMode == RCTResizeModeRepeat || _resizeMode == FFFResizeModeNone) {
809
813
  return NO;
810
814
  }
811
815
  return [_source isPhotoLibrary] || (_downsample && [FFFDownsampledImage isSupported]);
@@ -1009,6 +1013,7 @@ NSString *FFFErrorMessage(NSError *error)
1009
1013
  // Only on disk, also when it comes from there.
1010
1014
  context[SDWebImageContextStoreCacheType] = @(SDImageCacheTypeDisk);
1011
1015
  }
1016
+ [FFFDownsampledImage forgetSmallerFullSizeImageForURL: source.url context: context];
1012
1017
  return context;
1013
1018
  }
1014
1019
  [FFFDownsampledImage addToContext: context box: box cover: cover];
@@ -103,7 +103,7 @@ RCT_EXPORT_MODULE(FastImageView)
103
103
  RCT_EXPORT_VIEW_PROPERTY(source, FFFastImageSource)
104
104
  RCT_EXPORT_VIEW_PROPERTY(sources, FFFastImageSourceArray)
105
105
  RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage)
106
- RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
106
+ RCT_EXPORT_VIEW_PROPERTY(resizeMode, FFFResizeMode)
107
107
  RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
108
108
  RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
109
109
  RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
@@ -288,6 +288,8 @@ RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources
288
288
  [results addObject:[NSNull null]];
289
289
  SDWebImageOptions options = FFFPreloadOptions(source);
290
290
  SDWebImageMutableContext *context = FFFPreloadContext(source);
291
+ // It looks in the memory cache first, for the full-size image.
292
+ [FFFDownsampledImage forgetSmallerFullSizeImageForURL:source.url context:context];
291
293
  if (!source.memoryCache) {
292
294
  // Not decoded (only its header is read, for the size), and not
293
295
  // kept in memory, also when it comes from the disk cache.
@@ -1,9 +1,12 @@
1
1
  #import <React/RCTConvert.h>
2
2
 
3
+ #import "FFFastImageView.h"
4
+
3
5
  @class FFFastImageSource;
4
6
 
5
7
  @interface RCTConvert (FFFastImage)
6
8
 
9
+ + (FFFResizeMode)FFFResizeMode:(id)json;
7
10
  + (FFFastImageSource *)FFFastImageSource:(id)json;
8
11
  + (NSArray<FFFastImageSource *> *)FFFastImageSourceArray:(id)json;
9
12
 
@@ -3,6 +3,17 @@
3
3
 
4
4
  @implementation RCTConvert (FFFastImage)
5
5
 
6
+ RCT_ENUM_CONVERTER(FFFResizeMode, (@{
7
+ @"cover": @(RCTResizeModeCover),
8
+ @"contain": @(RCTResizeModeContain),
9
+ @"stretch": @(RCTResizeModeStretch),
10
+ @"center": @(RCTResizeModeCenter),
11
+ // objectFit="scale-down": as center on iOS.
12
+ @"scale-down": @(RCTResizeModeCenter),
13
+ @"repeat": @(RCTResizeModeRepeat),
14
+ @"none": @(FFFResizeModeNone),
15
+ }), RCTResizeModeCover, integerValue);
16
+
6
17
  RCT_ENUM_CONVERTER(FFFPriority, (@{
7
18
  @"low": @(FFFPriorityLow),
8
19
  @"normal": @(FFFPriorityNormal),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-fast-image",
3
- "version": "9.2.2",
3
+ "version": "9.3.1",
4
4
  "description": "FastImage, performant React Native image component.",
5
5
  "keywords": [
6
6
  "cache",