Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions Libraries/Image/Image.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import TextInlineImageNativeComponent from './TextInlineImageNativeComponent';

import type {ImageProps as ImagePropsType} from './ImageProps';
import type {RootTag} from '../Types/RootTagTypes';
import {getImageSourcesFromImageProps} from './ImageSourceUtils';

let _requestId = 1;
function generateRequestId() {
Expand Down Expand Up @@ -126,25 +127,12 @@ export type ImageComponentStatics = $ReadOnly<{|
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
const BaseImage = (props: ImagePropsType, forwardedRef) => {
let source = resolveAssetSource(props.source);
let source = getImageSourcesFromImageProps(props);
const defaultSource = resolveAssetSource(props.defaultSource);
const loadingIndicatorSource = resolveAssetSource(
props.loadingIndicatorSource,
);

if (source) {
const uri = source.uri;
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
}

if (props.src) {
console.warn(
'The <Image> component requires a `source` property rather than `src`.',
);
}

if (props.children) {
throw new Error(
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
Expand All @@ -163,10 +151,13 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => {

let style;
let sources;
if (source?.uri != null) {
const {width, height} = source;
if (!Array.isArray(source) && source?.uri != null) {
const {width = props.width, height = props.height, uri} = source;
style = flattenStyle([{width, height}, styles.base, props.style]);
sources = [{uri: source.uri}];
sources = [{uri: uri, width: width, height: height}];
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
} else {
style = flattenStyle([styles.base, props.style]);
sources = source;
Expand All @@ -180,7 +171,7 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => {
src: sources,
/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
* when making Flow check .android.js files. */
headers: source?.headers,
headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}),
defaultSrc: defaultSource ? defaultSource.uri : null,
loadingIndicatorSrc: loadingIndicatorSource
? loadingIndicatorSource.uri
Expand Down
15 changes: 6 additions & 9 deletions Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import NativeImageLoaderIOS from './NativeImageLoaderIOS';

import ImageViewNativeComponent from './ImageViewNativeComponent';
import type {RootTag} from 'react-native/Libraries/Types/RootTagTypes';
import {getImageSourcesFromImageProps} from './ImageSourceUtils';

function getSize(
uri: string,
Expand Down Expand Up @@ -105,7 +106,7 @@ export type ImageComponentStatics = $ReadOnly<{|
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
const BaseImage = (props: ImagePropsType, forwardedRef) => {
const source = resolveAssetSource(props.source) || {
const source = getImageSourcesFromImageProps(props) || {
uri: undefined,
width: undefined,
height: undefined,
Expand All @@ -117,7 +118,7 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => {
style = flattenStyle([styles.base, props.style]) || {};
sources = source;
} else {
const {width, height, uri} = source;
const {width = props.width, height = props.height, uri} = source;
style = flattenStyle([{width, height}, styles.base, props.style]) || {};
sources = [source];

Expand All @@ -131,24 +132,20 @@ const BaseImage = (props: ImagePropsType, forwardedRef) => {
// $FlowFixMe[prop-missing]
const tintColor = props.tintColor || style.tintColor;

if (props.src != null) {
console.warn(
'The <Image> component requires a `source` property rather than `src`.',
);
}

if (props.children != null) {
throw new Error(
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
);
}

const {src, ...restProps} = props;

return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
return (
<ImageViewNativeComponent
{...props}
{...restProps}
ref={forwardedRef}
style={style}
resizeMode={resizeMode}
Expand Down
54 changes: 53 additions & 1 deletion Libraries/Image/ImageProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ export type ImageProps = {|
*/
capInsets?: ?EdgeInsetsProp,

/**
* Adds the CORS related header to the request.
* Similar to crossorigin from HTML.
*
* See https://reactnative.dev/docs/image#crossorigin
*/
crossOrigin?: ?('anonymous' | 'use-credentials'),

/**
* Height of the image component.
*
* See https://reactnative.dev/docs/image#height
*/
height?: ?number,

/**
* Width of the image component.
*
* See https://reactnative.dev/docs/image#width
*/
width?: ?number,

/**
* Invoked on load error with `{nativeEvent: {error}}`.
*
Expand Down Expand Up @@ -158,6 +180,23 @@ export type ImageProps = {|
*/
style?: ?ImageStyleProp,

/**
* A string indicating which referrer to use when fetching the resource.
* Similar to referrerpolicy from HTML.
*
* See https://reactnative.dev/docs/image#referrerpolicy
*/
referrerPolicy?: ?(
| 'no-referrer'
| 'no-referrer-when-downgrade'
| 'origin'
| 'origin-when-cross-origin'
| 'same-origin'
| 'strict-origin'
| 'strict-origin-when-cross-origin'
| 'unsafe-url'
),

/**
* Determines how to resize the image when the frame doesn't match the raw
* image dimensions.
Expand All @@ -181,7 +220,20 @@ export type ImageProps = {|
*/
tintColor?: ColorValue,

src?: empty,
/**
* A string representing the resource identifier for the image. Similar to
* src from HTML.
*
* See https://reactnative.dev/docs/image#src
*/
src?: ?string,

/**
* Similar to srcset from HTML.
*
* See https://reactnative.dev/docs/image#srcset
*/
srcSet?: ?string,
children?: empty,
|};

Expand Down
81 changes: 81 additions & 0 deletions Libraries/Image/ImageSourceUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

'use strict';

import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageProps} from './ImageProps';

import resolveAssetSource from './resolveAssetSource';

/**
* A function which returns the appropriate value for image source
* by resolving the `source`, `src` and `srcSet` props.
*/
export function getImageSourcesFromImageProps(
imageProps: ImageProps,
): ?ResolvedAssetSource | $ReadOnlyArray<{uri: string, ...}> {
let source = resolveAssetSource(imageProps.source);

let sources;

const {crossOrigin, referrerPolicy, src, srcSet} = imageProps;

const getHeaders = () => {
const headers: {[string]: string} = {};
if (crossOrigin === 'use-credentials') {
headers['Access-Control-Allow-Credentials'] = 'true';
}
if (referrerPolicy != null) {
headers['Referrer-Policy'] = referrerPolicy;
}
return headers;
};

if (srcSet != null) {
const sourceList = [];
const srcSetList = srcSet.split(', ');
// `src` prop should be used with default scale if `srcSet` does not have 1x scale.
let shouldUseSrcForDefaultScale = true;
const {width, height} = imageProps;
srcSetList.forEach(imageSrc => {
const [uri, xScale = '1x'] = imageSrc.split(' ');
const scale = parseInt(xScale.split('x')[0], 10);
if (scale) {
if (scale === 1) {
// 1x scale is provided in `srcSet` prop so ignore the `src` prop if provided.
shouldUseSrcForDefaultScale = false;
}
sourceList.push({headers: getHeaders(), scale, uri, width, height});
}
});

if (shouldUseSrcForDefaultScale && src != null) {
sourceList.push({
headers: getHeaders(),
scale: 1,
uri: src,
width,
height,
});
}
if (sourceList.length === 0) {
console.warn('The provided value for srcSet is not valid.');
}

sources = sourceList;
} else if (src != null) {
const {width, height} = imageProps;
sources = [{uri: src, headers: getHeaders(), width, height}];
} else {
sources = source;
}
return sources;
}
124 changes: 124 additions & 0 deletions Libraries/Image/__tests__/ImageSourceUtils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @emails oncall+react_native
*/

const {getImageSourcesFromImageProps} = require('../ImageSourceUtils');

describe('ImageSourceUtils', () => {
it('source prop provided', () => {
const imageProps = {source: require('./img/img1.png')};
const sources = getImageSourcesFromImageProps(imageProps);

expect(sources).toBeDefined();
});

it('should ignore source when src is provided', () => {
let uri = 'imageURI';
const imageProps = {source: require('./img/img1.png'), src: uri};
const sources = getImageSourcesFromImageProps(imageProps);

expect(sources).toBeDefined();
expect(sources).toHaveLength(1);
expect(sources[0].uri).toBe(uri);
});

it('should ignore source and src when srcSet is provided', () => {
let uri = 'imageURI';

let uri1 = 'uri1';
let scale1 = '1x';

let uri2 = 'uri2';
let scale2 = '2x';

const imageProps = {
source: require('./img/img1.png'),
src: uri,
srcSet: `${uri1} ${scale1}, ${uri2} ${scale2}`,
};
const sources = getImageSourcesFromImageProps(imageProps);

expect(sources).toBeDefined();
expect(sources).toHaveLength(2);
expect(sources[0]).toEqual(expect.objectContaining({uri: uri1, scale: 1}));
expect(sources[1]).toEqual(expect.objectContaining({uri: uri2, scale: 2}));
});

it('should use src as default when 1x scale is not provided in srcSet', () => {
let uri = 'imageURI';

let uri1 = 'uri1';
let scale1 = '3x';

let uri2 = 'uri2';
let scale2 = '2x';

const imageProps = {
src: uri,
srcSet: `${uri1} ${scale1}, ${uri2} ${scale2}`,
};
const sources = getImageSourcesFromImageProps(imageProps);

expect(sources).toBeDefined();
expect(sources).toHaveLength(3);
expect(sources[0]).toEqual(expect.objectContaining({uri: uri1, scale: 3}));
expect(sources[1]).toEqual(expect.objectContaining({uri: uri2, scale: 2}));
expect(sources[2]).toEqual(expect.objectContaining({uri: uri, scale: 1}));
});

it('should use 1x as default scale if only url is provided in srcSet', () => {
let uri1 = 'uri1';
let scale1 = '2x';

let uri2 = 'uri2';

const imageProps = {
srcSet: `${uri1} ${scale1}, ${uri2}`,
};
const sources = getImageSourcesFromImageProps(imageProps);

expect(sources).toBeDefined();
expect(sources).toHaveLength(2);
expect(sources[0]).toEqual(expect.objectContaining({uri: uri1, scale: 2}));
expect(sources[1]).toEqual(expect.objectContaining({uri: uri2, scale: 1}));
});

it('should contain crossorigin headers when provided with src', () => {
let uri = 'imageURI';

const imageProps = {
src: uri,
crossOrigin: 'use-credentials',
};
const sources = getImageSourcesFromImageProps(imageProps);

expect(sources).toBeDefined();
expect(sources).toHaveLength(1);
expect(sources[0]).toHaveProperty('headers', {
['Access-Control-Allow-Credentials']: 'true',
});
});

it('should contain referrerPolicy headers when provided with src', () => {
let uri = 'imageURI';

let referrerPolicy = 'origin-when-cross-origin';
const imageProps = {
src: uri,
referrerPolicy: referrerPolicy,
};
const sources = getImageSourcesFromImageProps(imageProps);

expect(sources).toBeDefined();
expect(sources).toHaveLength(1);
expect(sources[0]).toHaveProperty('headers', {
['Referrer-Policy']: referrerPolicy,
});
});
});
Loading