Skip to content

Commit 3e5bb60

Browse files
author
Julian Hundeloh
committed
fix: support image source objects
1 parent 250ee3c commit 3e5bb60

File tree

5 files changed

+179
-66
lines changed

5 files changed

+179
-66
lines changed

packages/react-native-web/src/exports/Image/ImageUriCache.js

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,60 @@
77
* @flow
88
*/
99

10-
const dataUriPattern = /^data:/;
11-
1210
export default class ImageUriCache {
1311
static _maximumEntries: number = 256;
1412
static _entries = {};
1513

16-
static has(uri: string) {
14+
static has(cacheId: string) {
15+
const entries = ImageUriCache._entries;
16+
return Boolean(entries[cacheId]);
17+
}
18+
19+
static get(cacheId: string) {
1720
const entries = ImageUriCache._entries;
18-
const isDataUri = dataUriPattern.test(uri);
19-
return isDataUri || Boolean(entries[uri]);
21+
return entries[cacheId];
2022
}
2123

22-
static add(uri: string) {
24+
static add(cacheId: string, displayImageUri: string) {
2325
const entries = ImageUriCache._entries;
2426
const lastUsedTimestamp = Date.now();
25-
if (entries[uri]) {
26-
entries[uri].lastUsedTimestamp = lastUsedTimestamp;
27-
entries[uri].refCount += 1;
27+
if (entries[cacheId]) {
28+
entries[cacheId].lastUsedTimestamp = lastUsedTimestamp;
29+
entries[cacheId].refCount += 1;
2830
} else {
29-
entries[uri] = {
31+
entries[cacheId] = {
3032
lastUsedTimestamp,
31-
refCount: 1
33+
refCount: 1,
34+
displayImageUri
3235
};
3336
}
3437
}
3538

36-
static remove(uri: string) {
39+
static remove(cacheId: string) {
3740
const entries = ImageUriCache._entries;
38-
if (entries[uri]) {
39-
entries[uri].refCount -= 1;
41+
if (entries[cacheId]) {
42+
entries[cacheId].refCount -= 1;
4043
}
4144
// Free up entries when the cache is "full"
4245
ImageUriCache._cleanUpIfNeeded();
4346
}
4447

4548
static _cleanUpIfNeeded() {
4649
const entries = ImageUriCache._entries;
47-
const imageUris = Object.keys(entries);
50+
const cacheIds = Object.keys(entries);
4851

49-
if (imageUris.length + 1 > ImageUriCache._maximumEntries) {
52+
if (cacheIds.length + 1 > ImageUriCache._maximumEntries) {
5053
let leastRecentlyUsedKey;
5154
let leastRecentlyUsedEntry;
5255

53-
imageUris.forEach(uri => {
54-
const entry = entries[uri];
56+
cacheIds.forEach(cacheId => {
57+
const entry = entries[cacheId];
5558
if (
5659
(!leastRecentlyUsedEntry ||
5760
entry.lastUsedTimestamp < leastRecentlyUsedEntry.lastUsedTimestamp) &&
5861
entry.refCount === 0
5962
) {
60-
leastRecentlyUsedKey = uri;
63+
leastRecentlyUsedKey = cacheId;
6164
leastRecentlyUsedEntry = entry;
6265
}
6366
});

packages/react-native-web/src/exports/Image/index.js

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -38,41 +38,57 @@ const getImageState = (uri, shouldDisplaySource) => {
3838
};
3939

4040
const resolveAssetDimensions = source => {
41-
if (typeof source === 'number') {
42-
const { height, width } = getAssetByID(source);
43-
return { height, width };
44-
} else if (typeof source === 'object') {
45-
const { height, width } = source;
46-
return { height, width };
47-
}
41+
return {
42+
height: source.height,
43+
width: source.width
44+
};
4845
};
4946

5047
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
51-
const resolveAssetUri = source => {
52-
let uri = '';
48+
const resolveAssetSource = source => {
49+
let resolvedSource = {
50+
method: 'GET',
51+
uri: '',
52+
headers: {},
53+
width: undefined,
54+
height: undefined
55+
};
5356
if (typeof source === 'number') {
5457
// get the URI from the packager
5558
const asset = getAssetByID(source);
5659
const scale = asset.scales[0];
5760
const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
58-
uri = asset ? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}` : '';
61+
resolvedSource.uri = asset
62+
? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}`
63+
: '';
64+
resolvedSource.width = asset.width;
65+
resolvedSource.height = asset.height;
5966
} else if (typeof source === 'string') {
60-
uri = source;
61-
} else if (source && typeof source.uri === 'string') {
62-
uri = source.uri;
67+
resolvedSource.uri = source;
68+
} else if (typeof source === 'object') {
69+
resolvedSource = {
70+
...resolvedSource,
71+
...source
72+
};
6373
}
6474

65-
if (uri) {
66-
const match = uri.match(svgDataUriPattern);
75+
if (resolvedSource.uri) {
76+
const match = resolvedSource.uri.match(svgDataUriPattern);
6777
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
6878
if (match) {
6979
const [, prefix, svg] = match;
7080
const encodedSvg = encodeURIComponent(svg);
71-
return `${prefix}${encodedSvg}`;
81+
resolvedSource.uri = `${prefix}${encodedSvg}`;
7282
}
7383
}
7484

75-
return uri;
85+
return resolvedSource;
86+
};
87+
const getCacheId = source => {
88+
return JSON.stringify(resolveAssetSource(source));
89+
};
90+
const getCacheUrl = e => {
91+
return e.path && e.path[0].src;
7692
};
7793

7894
let filterId = 0;
@@ -91,7 +107,8 @@ const createTintColorSVG = (tintColor, id) =>
91107

92108
type State = {
93109
layout: Object,
94-
shouldDisplaySource: boolean
110+
shouldDisplaySource: boolean,
111+
displayImageUri: string
95112
};
96113

97114
class Image extends Component<*, State> {
@@ -130,10 +147,10 @@ class Image extends Component<*, State> {
130147
}
131148

132149
static prefetch(uri) {
133-
return ImageLoader.prefetch(uri).then(() => {
150+
return ImageLoader.prefetch(uri).then(e => {
134151
// Add the uri to the cache so it can be immediately displayed when used
135152
// but also immediately remove it to correctly reflect that it has no active references
136-
ImageUriCache.add(uri);
153+
ImageUriCache.add(uri, getCacheUrl(e));
137154
ImageUriCache.remove(uri);
138155
});
139156
}
@@ -157,10 +174,19 @@ class Image extends Component<*, State> {
157174
constructor(props, context) {
158175
super(props, context);
159176
// If an image has been loaded before, render it immediately
160-
const uri = resolveAssetUri(props.source);
161-
const shouldDisplaySource = ImageUriCache.has(uri);
162-
this.state = { layout: {}, shouldDisplaySource };
163-
this._imageState = getImageState(uri, shouldDisplaySource);
177+
const cacheId = getCacheId(props.source);
178+
const resolvedSource = resolveAssetSource(props.source);
179+
const resolvedDefaultSource = resolveAssetSource(props.defaultSource);
180+
const cachedSource = ImageUriCache.get(cacheId);
181+
const shouldDisplaySource = !!cachedSource;
182+
this.state = {
183+
layout: {},
184+
shouldDisplaySource,
185+
displayImageUri: shouldDisplaySource
186+
? cachedSource.uri
187+
: resolvedDefaultSource.uri || resolvedSource.uri
188+
};
189+
this._imageState = getImageState(resolvedSource.uri, shouldDisplaySource);
164190
this._filterId = filterId;
165191
filterId++;
166192
}
@@ -175,14 +201,14 @@ class Image extends Component<*, State> {
175201
}
176202

177203
componentDidUpdate(prevProps) {
178-
const prevUri = resolveAssetUri(prevProps.source);
179-
const uri = resolveAssetUri(this.props.source);
204+
const prevCacheId = getCacheId(prevProps.source);
205+
const cacheId = getCacheId(this.props.source);
180206
const hasDefaultSource = this.props.defaultSource != null;
181-
if (prevUri !== uri) {
182-
ImageUriCache.remove(prevUri);
183-
const isPreviouslyLoaded = ImageUriCache.has(uri);
184-
isPreviouslyLoaded && ImageUriCache.add(uri);
185-
this._updateImageState(getImageState(uri, isPreviouslyLoaded), hasDefaultSource);
207+
if (prevCacheId !== cacheId) {
208+
ImageUriCache.remove(prevCacheId);
209+
const isPreviouslyLoaded = ImageUriCache.has(cacheId);
210+
isPreviouslyLoaded && ImageUriCache.add(cacheId);
211+
this._updateImageState(getImageState(cacheId, isPreviouslyLoaded), hasDefaultSource);
186212
} else if (hasDefaultSource && prevProps.defaultSource !== this.props.defaultSource) {
187213
this._updateImageState(this._imageState, hasDefaultSource);
188214
}
@@ -192,14 +218,14 @@ class Image extends Component<*, State> {
192218
}
193219

194220
componentWillUnmount() {
195-
const uri = resolveAssetUri(this.props.source);
196-
ImageUriCache.remove(uri);
221+
const cacheId = getCacheId(this.props.source);
222+
ImageUriCache.remove(cacheId);
197223
this._destroyImageLoader();
198224
this._isMounted = false;
199225
}
200226

201227
render() {
202-
const { shouldDisplaySource } = this.state;
228+
const { displayImageUri, shouldDisplaySource } = this.state;
203229
const {
204230
accessibilityLabel,
205231
accessible,
@@ -233,8 +259,7 @@ class Image extends Component<*, State> {
233259
}
234260
}
235261

236-
const selectedSource = shouldDisplaySource ? source : defaultSource;
237-
const displayImageUri = resolveAssetUri(selectedSource);
262+
const selectedSource = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
238263
const imageSizeStyle = resolveAssetDimensions(selectedSource);
239264
const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
240265
const flatStyle = { ...StyleSheet.flatten(this.props.style) };
@@ -312,8 +337,11 @@ class Image extends Component<*, State> {
312337
_createImageLoader() {
313338
const { source } = this.props;
314339
this._destroyImageLoader();
315-
const uri = resolveAssetUri(source);
316-
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
340+
this._imageRequestId = ImageLoader.load(
341+
resolveAssetSource(source),
342+
this._onLoad,
343+
this._onError
344+
);
317345
this._onLoadStart();
318346
}
319347

@@ -356,7 +384,7 @@ class Image extends Component<*, State> {
356384
if (onError) {
357385
onError({
358386
nativeEvent: {
359-
error: `Failed to load resource ${resolveAssetUri(source)} (404)`
387+
error: `Failed to load resource ${resolveAssetSource(source).uri} (404)`
360388
}
361389
});
362390
}
@@ -366,7 +394,7 @@ class Image extends Component<*, State> {
366394
_onLoad = e => {
367395
const { onLoad, source } = this.props;
368396
const event = { nativeEvent: e };
369-
ImageUriCache.add(resolveAssetUri(source));
397+
ImageUriCache.add(getCacheId(source), getCacheUrl(e));
370398
this._updateImageState(STATUS_LOADED);
371399
if (onLoad) {
372400
onLoad(event);
@@ -394,14 +422,26 @@ class Image extends Component<*, State> {
394422
};
395423

396424
_updateImageState(status: ?string, hasDefaultSource: ?boolean = false) {
425+
const { source } = this.props;
397426
this._imageState = status;
398427
const shouldDisplaySource =
399428
this._imageState === STATUS_LOADED ||
400429
(this._imageState === STATUS_LOADING && !hasDefaultSource);
430+
const cachedId = getCacheId(source);
431+
const { displayImageUri } = ImageUriCache.has(cachedId)
432+
? ImageUriCache.get(cachedId)
433+
: this.state;
434+
401435
// only triggers a re-render when the image is loading and has no default image (to support PJPEG), loaded, or failed
402-
if (shouldDisplaySource !== this.state.shouldDisplaySource) {
436+
if (
437+
shouldDisplaySource !== this.state.shouldDisplaySource ||
438+
displayImageUri !== this.state.displayImageUri
439+
) {
403440
if (this._isMounted) {
404-
this.setState(() => ({ shouldDisplaySource }));
441+
this.setState(() => ({
442+
shouldDisplaySource,
443+
displayImageUri
444+
}));
405445
}
406446
}
407447
}

packages/react-native-web/src/modules/ImageLoader/index.js

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
let id = 0;
1111
const requests = {};
12+
const dataUriPattern = /^data:/;
1213

1314
const ImageLoader = {
1415
abort(requestId: number) {
@@ -21,7 +22,7 @@ const ImageLoader = {
2122
getSize(uri, success, failure) {
2223
let complete = false;
2324
const interval = setInterval(callback, 16);
24-
const requestId = ImageLoader.load(uri, callback, errorCallback);
25+
const requestId = ImageLoader.load({ uri }, callback, errorCallback);
2526

2627
function callback() {
2728
const image = requests[`${requestId}`];
@@ -46,8 +47,11 @@ const ImageLoader = {
4647
clearInterval(interval);
4748
}
4849
},
49-
load(uri, onLoad, onError): number {
50+
load(source, onLoad, onError): number {
51+
const { uri, method, headers, body } = { uri: '', method: 'GET', headers: {}, ...source };
5052
id += 1;
53+
54+
// Create image
5155
const image = new window.Image();
5256
image.onerror = onError;
5357
image.onload = e => {
@@ -62,13 +66,54 @@ const ImageLoader = {
6266
setTimeout(onDecode, 0);
6367
}
6468
};
65-
image.src = uri;
6669
requests[`${id}`] = image;
70+
71+
// If the important source properties are empty, return the image directly
72+
if (!source || !uri) {
73+
return id;
74+
}
75+
76+
// If the image is a dataUri, display it directly via image
77+
const isDataUri = dataUriPattern.test(uri);
78+
if (isDataUri) {
79+
image.src = uri;
80+
return id;
81+
}
82+
83+
// If the image can be retrieved via GET, we can fallback to image loading method
84+
if (method === 'GET') {
85+
image.src = uri;
86+
return id;
87+
}
88+
89+
// Load image via XHR
90+
const request = new window.XMLHttpRequest();
91+
request.open(method, uri);
92+
request.responseType = 'blob';
93+
request.withCredentials = false;
94+
request.onerror = () => {
95+
// Fall back to image (e.g. for CORS issues)
96+
image.src = uri;
97+
};
98+
99+
// Add request headers
100+
for (const [name, value] of Object.entries(headers)) {
101+
request.setRequestHeader(name, value);
102+
}
103+
104+
// When the request finished loading, pass it on to the image
105+
request.onload = () => {
106+
image.src = window.URL.createObjectURL(request.response);
107+
};
108+
109+
// Send the request
110+
request.send(body);
111+
67112
return id;
68113
},
69114
prefetch(uri): Promise {
70115
return new Promise((resolve, reject) => {
71-
ImageLoader.load(uri, resolve, reject);
116+
ImageLoader.load({ uri }, resolve, reject);
72117
});
73118
}
74119
};

0 commit comments

Comments
 (0)