This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) Facebook, Inc. and its 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
* @format
*/
'use strict';
export type PackagerAsset = {
+__packager_asset: boolean,
+fileSystemLocation: string,
+httpServerLocation: string,
+width: ?number,
+height: ?number,
+scales: Array<number>,
+hash: string,
+name: string,
+type: string,
...
};
const assets: Array<PackagerAsset> = [];
function registerAsset(asset: PackagerAsset): number {
// `push` returns new array length, so the first asset will
// get id 1 (not 0) to make the value truthy
return assets.push(asset);
}
function getAssetByID(assetId: number): PackagerAsset {
return assets[assetId - 1];
}
module.exports = {registerAsset, getAssetByID};

View File

@ -0,0 +1,176 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
export type ResolvedAssetSource = {|
+__packager_asset: boolean,
+width: ?number,
+height: ?number,
+uri: string,
+scale: number,
|};
import type {PackagerAsset} from './AssetRegistry';
const PixelRatio = require('../Utilities/PixelRatio');
const Platform = require('../Utilities/Platform');
const assetPathUtils = require('./assetPathUtils');
const invariant = require('invariant');
/**
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
*/
function getScaledAssetPath(asset): string {
const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
const assetDir = assetPathUtils.getBasePath(asset);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
/**
* Returns a path like 'drawable-mdpi/icon.png'
*/
function getAssetPathInDrawableFolder(asset): string {
const scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
const drawbleFolder = assetPathUtils.getAndroidResourceFolderName(
asset,
scale,
);
const fileName = assetPathUtils.getAndroidResourceIdentifier(asset);
return drawbleFolder + '/' + fileName + '.' + asset.type;
}
class AssetSourceResolver {
serverUrl: ?string;
// where the jsbundle is being run from
jsbundleUrl: ?string;
// the asset to resolve
asset: PackagerAsset;
constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
this.serverUrl = serverUrl;
this.jsbundleUrl = jsbundleUrl;
this.asset = asset;
}
isLoadedFromServer(): boolean {
return !!this.serverUrl;
}
isLoadedFromFileSystem(): boolean {
return !!(this.jsbundleUrl && this.jsbundleUrl.startsWith('file://'));
}
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetURLNearBundle();
}
}
/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
assetServerURL(): ResolvedAssetSource {
invariant(!!this.serverUrl, 'need server to load from');
return this.fromSource(
this.serverUrl +
getScaledAssetPath(this.asset) +
'?platform=' +
Platform.OS +
'&hash=' +
this.asset.hash,
);
}
/**
* Resolves to just the scaled asset filename
* E.g. 'assets/AwesomeModule/icon@2x.png'
*/
scaledAssetPath(): ResolvedAssetSource {
return this.fromSource(getScaledAssetPath(this.asset));
}
/**
* Resolves to where the bundle is running from, with a scaled asset filename
* E.g. 'file:///sdcard/bundle/assets/AwesomeModule/icon@2x.png'
*/
scaledAssetURLNearBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl || 'file://';
return this.fromSource(
// Assets can have relative paths outside of the project root.
// When bundling them we replace `../` with `_` to make sure they
// don't end up outside of the expected assets directory.
path + getScaledAssetPath(this.asset).replace(/\.\.\//g, '_'),
);
}
/**
* The default location of assets bundled with the app, located by
* resource identifier
* The Android resource system picks the correct scale.
* E.g. 'assets_awesomemodule_icon'
*/
resourceIdentifierWithoutScale(): ResolvedAssetSource {
invariant(
Platform.OS === 'android',
'resource identifiers work on Android',
);
return this.fromSource(
assetPathUtils.getAndroidResourceIdentifier(this.asset),
);
}
/**
* If the jsbundle is running from a sideload location, this resolves assets
* relative to its location
* E.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png'
*/
drawableFolderInBundle(): ResolvedAssetSource {
const path = this.jsbundleUrl || 'file://';
return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
}
fromSource(source: string): ResolvedAssetSource {
return {
__packager_asset: true,
width: this.asset.width,
height: this.asset.height,
uri: source,
scale: AssetSourceResolver.pickScale(this.asset.scales, PixelRatio.get()),
};
}
static pickScale(scales: Array<number>, deviceScale: number): number {
// Packager guarantees that `scales` array is sorted
for (let i = 0; i < scales.length; i++) {
if (scales[i] >= deviceScale) {
return scales[i];
}
}
// If nothing matches, device scale is larger than any available
// scales, so we return the biggest one. Unless the array is empty,
// in which case we default to 1
return scales[scales.length - 1] || 1;
}
}
module.exports = AssetSourceResolver;

View File

@ -0,0 +1,407 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const DeprecatedImageStylePropTypes = require('../DeprecatedPropTypes/DeprecatedImageStylePropTypes');
const DeprecatedStyleSheetPropType = require('../DeprecatedPropTypes/DeprecatedStyleSheetPropType');
const DeprecatedViewPropTypes = require('../DeprecatedPropTypes/DeprecatedViewPropTypes');
import ImageViewNativeComponent from './ImageViewNativeComponent';
const PropTypes = require('prop-types');
const React = require('react');
const ReactNative = require('../Renderer/shims/ReactNative'); // eslint-disable-line no-unused-vars
const StyleSheet = require('../StyleSheet/StyleSheet');
const TextAncestor = require('../Text/TextAncestor');
const ImageAnalyticsTagContext = require('./ImageAnalyticsTagContext').default;
const flattenStyle = require('../StyleSheet/flattenStyle');
const resolveAssetSource = require('./resolveAssetSource');
import NativeImageLoaderAndroid from './NativeImageLoaderAndroid';
const TextInlineImageNativeComponent = require('./TextInlineImageNativeComponent');
import type {ImageProps as ImagePropsType} from './ImageProps';
let _requestId = 1;
function generateRequestId() {
return _requestId++;
}
const ImageProps = {
...DeprecatedViewPropTypes,
style: (DeprecatedStyleSheetPropType(
DeprecatedImageStylePropTypes,
): ReactPropsCheckType),
/**
* See https://reactnative.dev/docs/image.html#source
*/
source: (PropTypes.oneOfType([
PropTypes.shape({
uri: PropTypes.string,
headers: PropTypes.objectOf(PropTypes.string),
}),
// Opaque type returned by require('./image.jpg')
PropTypes.number,
// Multiple sources
PropTypes.arrayOf(
PropTypes.shape({
uri: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
headers: PropTypes.objectOf(PropTypes.string),
}),
),
]): React$PropType$Primitive<
| {
headers?: {[string]: string, ...},
uri?: string,
...
}
| number
| Array<{
headers?: {[string]: string, ...},
height?: number,
uri?: string,
width?: number,
...
}>,
>),
/**
* blurRadius: the blur radius of the blur filter added to the image
*
* See https://reactnative.dev/docs/image.html#blurradius
*/
blurRadius: PropTypes.number,
/**
* See https://reactnative.dev/docs/image.html#defaultsource
*/
defaultSource: PropTypes.number,
/**
* See https://reactnative.dev/docs/image.html#loadingindicatorsource
*/
loadingIndicatorSource: (PropTypes.oneOfType([
PropTypes.shape({
uri: PropTypes.string,
}),
// Opaque type returned by require('./image.jpg')
PropTypes.number,
]): React$PropType$Primitive<{uri?: string, ...} | number>),
progressiveRenderingEnabled: PropTypes.bool,
fadeDuration: PropTypes.number,
/**
* Analytics Tag used by this Image
*/
internal_analyticTag: PropTypes.string,
/**
* Invoked on load start
*/
onLoadStart: PropTypes.func,
/**
* Invoked on load error
*/
onError: PropTypes.func,
/**
* Invoked when load completes successfully
*/
onLoad: PropTypes.func,
/**
* Invoked when load either succeeds or fails
*/
onLoadEnd: PropTypes.func,
/**
* Used to locate this view in end-to-end tests.
*/
testID: PropTypes.string,
/**
* The mechanism that should be used to resize the image when the image's dimensions
* differ from the image view's dimensions. Defaults to `auto`.
*
* See https://reactnative.dev/docs/image.html#resizemethod
*/
resizeMethod: (PropTypes.oneOf([
'auto',
'resize',
'scale',
]): React$PropType$Primitive<'auto' | 'resize' | 'scale'>),
/**
* Determines how to resize the image when the frame doesn't match the raw
* image dimensions.
*
* See https://reactnative.dev/docs/image.html#resizemode
*/
resizeMode: (PropTypes.oneOf([
'cover',
'contain',
'stretch',
'repeat',
'center',
]): React$PropType$Primitive<
'cover' | 'contain' | 'stretch' | 'repeat' | 'center',
>),
};
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
*
* See https://reactnative.dev/docs/image.html#getsize
*/
function getSize(
url: string,
success: (width: number, height: number) => void,
failure?: (error: any) => void,
): any {
return NativeImageLoaderAndroid.getSize(url)
.then(function(sizes) {
success(sizes.width, sizes.height);
})
.catch(
failure ||
function() {
console.warn('Failed to get size for image: ' + url);
},
);
}
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
* with the ability to provide the headers for the request
*
* See https://reactnative.dev/docs/image.html#getsizewithheaders
*/
function getSizeWithHeaders(
url: string,
headers: {[string]: string, ...},
success: (width: number, height: number) => void,
failure?: (error: any) => void,
): any {
return NativeImageLoaderAndroid.getSizeWithHeaders(url, headers)
.then(function(sizes) {
success(sizes.width, sizes.height);
})
.catch(
failure ||
function() {
console.warn('Failed to get size for image: ' + url);
},
);
}
function prefetch(url: string, callback: ?Function): any {
const requestId = generateRequestId();
callback && callback(requestId);
return NativeImageLoaderAndroid.prefetchImage(url, requestId);
}
function abortPrefetch(requestId: number) {
NativeImageLoaderAndroid.abortRequest(requestId);
}
/**
* Perform cache interrogation.
*
* See https://reactnative.dev/docs/image.html#querycache
*/
async function queryCache(
urls: Array<string>,
): Promise<{[string]: 'memory' | 'disk' | 'disk/memory', ...}> {
return await NativeImageLoaderAndroid.queryCache(urls);
}
type ImageComponentStatics = $ReadOnly<{|
getSize: typeof getSize,
getSizeWithHeaders: typeof getSizeWithHeaders,
prefetch: typeof prefetch,
abortPrefetch: typeof abortPrefetch,
queryCache: typeof queryCache,
resolveAssetSource: typeof resolveAssetSource,
propTypes: typeof ImageProps,
|}>;
/**
* A React component for displaying different types of images,
* including network images, static resources, temporary local images, and
* images from local disk, such as the camera roll.
*
* See https://reactnative.dev/docs/image.html
*/
let Image = (props: ImagePropsType, forwardedRef) => {
let source = resolveAssetSource(props.source);
const defaultSource = resolveAssetSource(props.defaultSource);
const loadingIndicatorSource = resolveAssetSource(
props.loadingIndicatorSource,
);
if (source && source.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.',
);
}
if (props.defaultSource && props.loadingIndicatorSource) {
throw new Error(
'The <Image> component cannot have defaultSource and loadingIndicatorSource at the same time. Please use either defaultSource or loadingIndicatorSource.',
);
}
if (source && !source.uri && !Array.isArray(source)) {
source = null;
}
let style;
let sources;
if (source?.uri != null) {
const {width, height} = source;
style = flattenStyle([{width, height}, styles.base, props.style]);
sources = [{uri: source.uri}];
} else {
style = flattenStyle([styles.base, props.style]);
sources = source;
}
const {onLoadStart, onLoad, onLoadEnd, onError} = props;
const nativeProps = {
...props,
style,
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError),
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,
defaultSrc: defaultSource ? defaultSource.uri : null,
loadingIndicatorSrc: loadingIndicatorSource
? loadingIndicatorSource.uri
: null,
ref: forwardedRef,
};
return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
const nativePropsWithAnalytics =
analyticTag !== null
? {
...nativeProps,
internal_analyticTag: analyticTag,
}
: nativeProps;
return (
<TextAncestor.Consumer>
{hasTextAncestor =>
hasTextAncestor ? (
<TextInlineImageNativeComponent {...nativePropsWithAnalytics} />
) : (
<ImageViewNativeComponent {...nativePropsWithAnalytics} />
)
}
</TextAncestor.Consumer>
);
}}
</ImageAnalyticsTagContext.Consumer>
);
};
Image = React.forwardRef<
ImagePropsType,
| React.ElementRef<typeof TextInlineImageNativeComponent>
| React.ElementRef<typeof ImageViewNativeComponent>,
>(Image);
Image.displayName = 'Image';
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
*
* See https://reactnative.dev/docs/image.html#getsize
*/
/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.getSize = getSize;
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
* with the ability to provide the headers for the request
*
* See https://reactnative.dev/docs/image.html#getsizewithheaders
*/
/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.getSizeWithHeaders = getSizeWithHeaders;
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache
*
* See https://reactnative.dev/docs/image.html#prefetch
*/
/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.prefetch = prefetch;
/**
* Abort prefetch request.
*
* See https://reactnative.dev/docs/image.html#abortprefetch
*/
/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.abortPrefetch = abortPrefetch;
/**
* Perform cache interrogation.
*
* See https://reactnative.dev/docs/image.html#querycache
*/
/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.queryCache = queryCache;
/**
* Resolves an asset reference into an object.
*
* See https://reactnative.dev/docs/image.html#resolveassetsource
*/
/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.resolveAssetSource = resolveAssetSource;
/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.propTypes = ImageProps;
const styles = StyleSheet.create({
base: {
overflow: 'hidden',
},
});
module.exports = ((Image: any): React.AbstractComponent<
ImagePropsType,
| React.ElementRef<typeof TextInlineImageNativeComponent>
| React.ElementRef<typeof ImageViewNativeComponent>,
> &
ImageComponentStatics);

211
node_modules/react-native/Libraries/Image/Image.ios.js generated vendored Normal file
View File

@ -0,0 +1,211 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const DeprecatedImagePropType = require('../DeprecatedPropTypes/DeprecatedImagePropType');
const React = require('react');
const ReactNative = require('../Renderer/shims/ReactNative'); // eslint-disable-line no-unused-vars
const StyleSheet = require('../StyleSheet/StyleSheet');
const flattenStyle = require('../StyleSheet/flattenStyle');
const resolveAssetSource = require('./resolveAssetSource');
import type {ImageProps as ImagePropsType} from './ImageProps';
import type {ImageStyleProp} from '../StyleSheet/StyleSheet';
import NativeImageLoaderIOS from './NativeImageLoaderIOS';
import ImageViewNativeComponent from './ImageViewNativeComponent';
function getSize(
uri: string,
success: (width: number, height: number) => void,
failure?: (error: any) => void,
) {
NativeImageLoaderIOS.getSize(uri)
.then(([width, height]) => success(width, height))
.catch(
failure ||
function() {
console.warn('Failed to get size for image ' + uri);
},
);
}
function getSizeWithHeaders(
uri: string,
headers: {[string]: string, ...},
success: (width: number, height: number) => void,
failure?: (error: any) => void,
): any {
return NativeImageLoaderIOS.getSizeWithHeaders(uri, headers)
.then(function(sizes) {
success(sizes.width, sizes.height);
})
.catch(
failure ||
function() {
console.warn('Failed to get size for image: ' + uri);
},
);
}
function prefetch(url: string): any {
return NativeImageLoaderIOS.prefetchImage(url);
}
async function queryCache(
urls: Array<string>,
): Promise<{[string]: 'memory' | 'disk' | 'disk/memory', ...}> {
return await NativeImageLoaderIOS.queryCache(urls);
}
type ImageComponentStatics = $ReadOnly<{|
getSize: typeof getSize,
getSizeWithHeaders: typeof getSizeWithHeaders,
prefetch: typeof prefetch,
queryCache: typeof queryCache,
resolveAssetSource: typeof resolveAssetSource,
propTypes: typeof DeprecatedImagePropType,
|}>;
/**
* A React component for displaying different types of images,
* including network images, static resources, temporary local images, and
* images from local disk, such as the camera roll.
*
* See https://reactnative.dev/docs/image.html
*/
let Image = (props: ImagePropsType, forwardedRef) => {
const source = resolveAssetSource(props.source) || {
uri: undefined,
width: undefined,
height: undefined,
};
let sources;
let style: ImageStyleProp;
if (Array.isArray(source)) {
// $FlowFixMe flattenStyle is not strong enough
style = flattenStyle([styles.base, props.style]) || {};
sources = source;
} else {
const {width, height, uri} = source;
// $FlowFixMe flattenStyle is not strong enough
style = flattenStyle([{width, height}, styles.base, props.style]) || {};
sources = [source];
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
}
const resizeMode = props.resizeMode || style.resizeMode || 'cover';
const 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.',
);
}
return (
<ImageViewNativeComponent
{...props}
ref={forwardedRef}
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
source={sources}
/>
);
};
Image = React.forwardRef<
ImagePropsType,
React.ElementRef<typeof ImageViewNativeComponent>,
>(Image);
Image.displayName = 'Image';
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it.
*
* See https://reactnative.dev/docs/image.html#getsize
*/
/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.getSize = getSize;
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
* with the ability to provide the headers for the request.
*
* See https://reactnative.dev/docs/image.html#getsizewithheaders
*/
/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.getSizeWithHeaders = getSizeWithHeaders;
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache.
*
* See https://reactnative.dev/docs/image.html#prefetch
*/
/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.prefetch = prefetch;
/**
* Performs cache interrogation.
*
* See https://reactnative.dev/docs/image.html#querycache
*/
/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.queryCache = queryCache;
/**
* Resolves an asset reference into an object.
*
* See https://reactnative.dev/docs/image.html#resolveassetsource
*/
/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.resolveAssetSource = resolveAssetSource;
/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an
* error found when Flow v0.89 was deployed. To see the error, delete this
* comment and run Flow. */
Image.propTypes = DeprecatedImagePropType;
const styles = StyleSheet.create({
base: {
overflow: 'hidden',
},
});
module.exports = ((Image: any): React.AbstractComponent<
ImagePropsType,
React.ElementRef<typeof ImageViewNativeComponent>,
> &
ImageComponentStatics);

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its 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
* @format
*/
'use strict';
import * as React from 'react';
type ContextType = ?string;
const Context: React.Context<ContextType> = React.createContext<ContextType>(
null,
);
export default Context;

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const Image = require('./Image');
const React = require('react');
const StyleSheet = require('../StyleSheet/StyleSheet');
const View = require('../Components/View/View');
/**
* Very simple drop-in replacement for <Image> which supports nesting views.
*
* ```ReactNativeWebPlayer
* import React, { Component } from 'react';
* import { AppRegistry, View, ImageBackground, Text } from 'react-native';
*
* class DisplayAnImageBackground extends Component {
* render() {
* return (
* <ImageBackground
* style={{width: 50, height: 50}}
* source={{uri: 'https://reactnative.dev/img/opengraph.png'}}
* >
* <Text>React</Text>
* </ImageBackground>
* );
* }
* }
*
* // App registration and rendering
* AppRegistry.registerComponent('DisplayAnImageBackground', () => DisplayAnImageBackground);
* ```
*/
class ImageBackground extends React.Component<$FlowFixMeProps> {
setNativeProps(props: Object) {
// Work-around flow
const viewRef = this._viewRef;
if (viewRef) {
viewRef.setNativeProps(props);
}
}
_viewRef: ?React.ElementRef<typeof View> = null;
_captureRef = ref => {
this._viewRef = ref;
};
render(): React.Node {
const {children, style, imageStyle, imageRef, ...props} = this.props;
return (
<View
accessibilityIgnoresInvertColors={true}
style={style}
ref={this._captureRef}>
<Image
{...props}
style={[
StyleSheet.absoluteFill,
{
// Temporary Workaround:
// Current (imperfect yet) implementation of <Image> overwrites width and height styles
// (which is not quite correct), and these styles conflict with explicitly set styles
// of <ImageBackground> and with our internal layout model here.
// So, we have to proxy/reapply these styles explicitly for actual <Image> component.
// This workaround should be removed after implementing proper support of
// intrinsic content size of the <Image>.
width: style.width,
height: style.height,
},
imageStyle,
]}
ref={imageRef}
/>
{children}
</View>
);
}
}
module.exports = ImageBackground;

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import NativeImagePickerIOS from './NativeImagePickerIOS';
import invariant from 'invariant';
const ImagePickerIOS = {
canRecordVideos: function(callback: (result: boolean) => void): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
return NativeImagePickerIOS.canRecordVideos(callback);
},
canUseCamera: function(callback: (result: boolean) => void): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
return NativeImagePickerIOS.canUseCamera(callback);
},
openCameraDialog: function(
config: $ReadOnly<{|
unmirrorFrontFacingCamera?: boolean,
videoMode?: boolean,
|}>,
successCallback: (imageURL: string, height: number, width: number) => void,
cancelCallback: () => void,
): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
var newConfig = {
videoMode: true,
unmirrorFrontFacingCamera: false,
};
if (config.videoMode != null) {
newConfig.videoMode = config.videoMode;
}
if (config.unmirrorFrontFacingCamera != null) {
newConfig.unmirrorFrontFacingCamera = config.unmirrorFrontFacingCamera;
}
return NativeImagePickerIOS.openCameraDialog(
newConfig,
successCallback,
cancelCallback,
);
},
openSelectDialog: function(
config: $ReadOnly<{|
showImages?: boolean,
showVideos?: boolean,
|}>,
successCallback: (imageURL: string, height: number, width: number) => void,
cancelCallback: () => void,
): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
var newConfig = {
showImages: true,
showVideos: false,
};
if (config.showImages != null) {
newConfig.showImages = config.showImages;
}
if (config.showVideos != null) {
newConfig.showVideos = config.showVideos;
}
return NativeImagePickerIOS.openSelectDialog(
newConfig,
successCallback,
cancelCallback,
);
},
/**
* In iOS 13, the video URLs returned by the Image Picker are invalidated when
* the picker is dismissed, unless reference to it is held. This API allows
* the application to signal when it's finished with the video so that the
* reference can be cleaned up.
* It is safe to call this method for urlsthat aren't video URLs;
* it will be a no-op.
*/
removePendingVideo: function(url: string): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
NativeImagePickerIOS.removePendingVideo(url);
},
/**
* WARNING: In most cases, removePendingVideo should be used instead because
* clearAllPendingVideos could clear out pending videos made by other callers.
*/
clearAllPendingVideos: function(): void {
invariant(NativeImagePickerIOS, 'ImagePickerIOS is not available');
NativeImagePickerIOS.clearAllPendingVideos();
},
};
module.exports = ImagePickerIOS;

174
node_modules/react-native/Libraries/Image/ImageProps.js generated vendored Normal file
View File

@ -0,0 +1,174 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {SyntheticEvent, LayoutEvent} from '../Types/CoreEventTypes';
import type {EdgeInsetsProp} from '../StyleSheet/EdgeInsetsPropType';
import type {ImageSource} from './ImageSource';
import type {ViewStyleProp, ImageStyleProp} from '../StyleSheet/StyleSheet';
import type {ViewProps} from '../Components/View/ViewPropTypes';
export type ImageLoadEvent = SyntheticEvent<
$ReadOnly<{|
source: $ReadOnly<{|
width: number,
height: number,
url: string,
|}>,
uri?: string, // Only on Android
|}>,
>;
type IOSImageProps = $ReadOnly<{|
/**
* A static image to display while loading the image source.
*
* See https://reactnative.dev/docs/image.html#defaultsource
*/
defaultSource?: ?ImageSource,
/**
* Invoked when a partial load of the image is complete.
*
* See https://reactnative.dev/docs/image.html#onpartialload
*/
onPartialLoad?: ?() => void,
/**
* Invoked on download progress with `{nativeEvent: {loaded, total}}`.
*
* See https://reactnative.dev/docs/image.html#onprogress
*/
onProgress?: ?(
event: SyntheticEvent<$ReadOnly<{|loaded: number, total: number|}>>,
) => void,
|}>;
type AndroidImageProps = $ReadOnly<{|
loadingIndicatorSource?: ?(number | $ReadOnly<{|uri: string|}>),
progressiveRenderingEnabled?: ?boolean,
fadeDuration?: ?number,
|}>;
export type ImageProps = {|
...$Diff<ViewProps, $ReadOnly<{|style: ?ViewStyleProp|}>>,
...IOSImageProps,
...AndroidImageProps,
/**
* When true, indicates the image is an accessibility element.
*
* See https://reactnative.dev/docs/image.html#accessible
*/
accessible?: ?boolean,
/**
* Internal prop to set an "Analytics Tag" that can will be set on the Image
*/
internal_analyticTag?: ?string,
/**
* The text that's read by the screen reader when the user interacts with
* the image.
*
* See https://reactnative.dev/docs/image.html#accessibilitylabel
*/
accessibilityLabel?: ?Stringish,
/**
* blurRadius: the blur radius of the blur filter added to the image
*
* See https://reactnative.dev/docs/image.html#blurradius
*/
blurRadius?: ?number,
/**
* See https://reactnative.dev/docs/image.html#capinsets
*/
capInsets?: ?EdgeInsetsProp,
/**
* Invoked on load error with `{nativeEvent: {error}}`.
*
* See https://reactnative.dev/docs/image.html#onerror
*/
onError?: ?(
event: SyntheticEvent<
$ReadOnly<{|
error: string,
|}>,
>,
) => void,
/**
* Invoked on mount and layout changes with
* `{nativeEvent: {layout: {x, y, width, height}}}`.
*
* See https://reactnative.dev/docs/image.html#onlayout
*/
onLayout?: ?(event: LayoutEvent) => mixed,
/**
* Invoked when load completes successfully.
*
* See https://reactnative.dev/docs/image.html#onload
*/
onLoad?: ?(event: ImageLoadEvent) => void,
/**
* Invoked when load either succeeds or fails.
*
* See https://reactnative.dev/docs/image.html#onloadend
*/
onLoadEnd?: ?() => void,
/**
* Invoked on load start.
*
* See https://reactnative.dev/docs/image.html#onloadstart
*/
onLoadStart?: ?() => void,
/**
* See https://reactnative.dev/docs/image.html#resizemethod
*/
resizeMethod?: ?('auto' | 'resize' | 'scale'),
/**
* The image source (either a remote URL or a local file resource).
*
* See https://reactnative.dev/docs/image.html#source
*/
source?: ?ImageSource,
/**
* See https://reactnative.dev/docs/image.html#style
*/
style?: ?ImageStyleProp,
/**
* Determines how to resize the image when the frame doesn't match the raw
* image dimensions.
*
* See https://reactnative.dev/docs/image.html#resizemode
*/
resizeMode?: ?('cover' | 'contain' | 'stretch' | 'repeat' | 'center'),
/**
* A unique identifier for this element to be used in UI Automation
* testing scripts.
*
* See https://reactnative.dev/docs/image.html#testid
*/
testID?: ?string,
src?: empty,
children?: empty,
|};

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) Facebook, Inc. and its 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
* @format
*/
'use strict';
/**
* ImageResizeMode defines valid values for different image resizing modes set
* via the `resizeMode` style property on `<Image>`.
*/
export type ImageResizeMode =
// Resize by scaling down such that it is completely visible, if bigger than
// the area of the view. The image will not be scaled up.
| 'center'
// Resize such that it will be completely visible, contained within the frame
// of the View.
| 'contain'
// Resize such that the entire area of the view is covered by the image,
// potentially clipping parts of the image.
| 'cover'
// Resize by repeating to cover the frame of the View. The image will keep its
// size and aspect ratio.
| 'repeat'
// Resize by stretching it to fill the entire frame of the view without
// clipping. This may change the aspect ratio of the image, distorting it.
| 'stretch';

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
// This is to sync with ImageSourcePropTypes.js.
// We explicitly don't want this to be strict so that we can pass in objects
// that might have more keys. This also has to be inexact to support taking
// instances of classes like FBIcon.
// https://fburl.com/8lynhvtw
export type ImageURISource = $ReadOnly<{
/**
* `uri` is a string representing the resource identifier for the image, which
* could be an http address, a local file path, or the name of a static image
* resource (which should be wrapped in the `require('./path/to/image.png')`
* function).
*/
uri?: ?string,
/**
* `bundle` is the iOS asset bundle which the image is included in. This
* will default to [NSBundle mainBundle] if not set.
* @platform ios
*/
bundle?: ?string,
/**
* `method` is the HTTP Method to use. Defaults to GET if not specified.
*/
method?: ?string,
/**
* `headers` is an object representing the HTTP headers to send along with the
* request for a remote image.
*/
headers?: ?Object,
/**
* `body` is the HTTP body to send with the request. This must be a valid
* UTF-8 string, and will be sent exactly as specified, with no
* additional encoding (e.g. URL-escaping or base64) applied.
*/
body?: ?string,
/**
* `cache` determines how the requests handles potentially cached
* responses.
*
* - `default`: Use the native platforms default strategy. `useProtocolCachePolicy` on iOS.
*
* - `reload`: The data for the URL will be loaded from the originating source.
* No existing cache data should be used to satisfy a URL load request.
*
* - `force-cache`: The existing cached data will be used to satisfy the request,
* regardless of its age or expiration date. If there is no existing data in the cache
* corresponding the request, the data is loaded from the originating source.
*
* - `only-if-cached`: The existing cache data will be used to satisfy a request, regardless of
* its age or expiration date. If there is no existing data in the cache corresponding
* to a URL load request, no attempt is made to load the data from the originating source,
* and the load is considered to have failed.
*
* @platform ios
*/
cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached'),
/**
* `width` and `height` can be specified if known at build time, in which case
* these will be used to set the default `<Image/>` component dimensions.
*/
width?: ?number,
height?: ?number,
/**
* `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if
* unspecified, meaning that one image pixel equates to one display point / DIP.
*/
scale?: ?number,
...
}>;
// We have to export any because of an issue in Flow with objects that come from Relay:
// https://fburl.com/8ljo5tmr
// https://fb.facebook.com/groups/flow/permalink/1824103160971624/
export type ImageSource = ImageURISource | number | Array<ImageURISource>;

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
const requireNativeComponent = require('../ReactNative/requireNativeComponent');
import type {DangerouslyImpreciseStyle} from '../StyleSheet/StyleSheet';
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
import type {ImageProps} from './ImageProps';
import type {ViewProps} from '../Components/View/ViewPropTypes';
import type {ImageStyleProp} from '../StyleSheet/StyleSheet';
import type {ColorValue} from '../StyleSheet/StyleSheetTypes';
import ImageViewViewConfig from './ImageViewViewConfig';
const ReactNativeViewConfigRegistry = require('../Renderer/shims/ReactNativeViewConfigRegistry');
type NativeProps = $ReadOnly<{|
...ImageProps,
...ViewProps,
style?: ImageStyleProp | DangerouslyImpreciseStyle,
// iOS native props
tintColor?: ColorValue,
// Android native props
shouldNotifyLoadEvents?: boolean,
src?: ?ResolvedAssetSource | $ReadOnlyArray<{uri: string, ...}>,
headers?: ?string,
defaultSrc?: ?string,
loadingIndicatorSrc?: ?string,
|}>;
let ImageViewNativeComponent;
if (global.RN$Bridgeless) {
ReactNativeViewConfigRegistry.register('RCTImageView', () => {
return ImageViewViewConfig;
});
ImageViewNativeComponent = 'RCTImageView';
} else {
ImageViewNativeComponent = requireNativeComponent<NativeProps>(
'RCTImageView',
);
}
// flowlint-next-line unclear-type:off
export default ((ImageViewNativeComponent: any): HostComponent<NativeProps>);

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import ReactNativeViewViewConfig from '../Components/View/ReactNativeViewViewConfig';
import type {ReactNativeBaseComponentViewConfig} from '../Renderer/shims/ReactNativeTypes';
const ImageViewViewConfig = {
uiViewClassName: 'RCTImageView',
bubblingEventTypes: {},
directEventTypes: {
topLoadStart: {
registrationName: 'onLoadStart',
},
topProgress: {
registrationName: 'onProgress',
},
topError: {
registrationName: 'onError',
},
topPartialLoad: {
registrationName: 'onPartialLoad',
},
topLoad: {
registrationName: 'onLoad',
},
topLoadEnd: {
registrationName: 'onLoadEnd',
},
},
validAttributes: {
...ReactNativeViewViewConfig.validAttributes,
blurRadius: true,
// flowlint-next-line unclear-type:off
capInsets: {diff: (require('../Utilities/differ/insetsDiffer'): any)},
defaultSource: {
process: require('./resolveAssetSource'),
},
defaultSrc: true,
fadeDuration: true,
headers: true,
loadingIndicatorSrc: true,
onError: true,
onLoad: true,
onLoadEnd: true,
onLoadStart: true,
onPartialLoad: true,
onProgress: true,
overlayColor: {process: require('../StyleSheet/processColor')},
progressiveRenderingEnabled: true,
resizeMethod: true,
resizeMode: true,
shouldNotifyLoadEvents: true,
source: true,
src: true,
tintColor: {process: require('../StyleSheet/processColor')},
},
};
module.exports = (ImageViewViewConfig: ReactNativeBaseComponentViewConfig<>);

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {TurboModule} from '../TurboModule/RCTExport';
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
export interface Spec extends TurboModule {
+abortRequest: (requestId: number) => void;
+getConstants: () => {||};
+getSize: (
uri: string,
) => Promise<
$ReadOnly<{
width: number,
height: number,
...
}>,
>;
+getSizeWithHeaders: (
uri: string,
headers: Object,
) => Promise<{
width: number,
height: number,
...
}>;
+prefetchImage: (uri: string, requestId: number) => Promise<boolean>;
+queryCache: (uris: Array<string>) => Promise<Object>;
}
export default (TurboModuleRegistry.getEnforcing<Spec>('ImageLoader'): Spec);

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {TurboModule} from '../TurboModule/RCTExport';
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
export interface Spec extends TurboModule {
+getConstants: () => {||};
// Return [width, height] of image uri
+getSize: (uri: string) => Promise<$ReadOnlyArray<number>>;
+getSizeWithHeaders: (
uri: string,
headers: Object,
) => Promise<{
width: number,
height: number,
...
}>;
+prefetchImage: (uri: string) => Promise<boolean>;
+queryCache: (uris: Array<string>) => Promise<Object>;
}
export default (TurboModuleRegistry.getEnforcing<Spec>('ImageLoader'): Spec);

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {TurboModule} from '../TurboModule/RCTExport';
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
export interface Spec extends TurboModule {
+getConstants: () => {||};
+canRecordVideos: (callback: (result: boolean) => void) => void;
+canUseCamera: (callback: (result: boolean) => void) => void;
+openCameraDialog: (
config: {|
unmirrorFrontFacingCamera: boolean,
videoMode: boolean,
|},
successCallback: (imageURL: string, height: number, width: number) => void,
cancelCallback: () => void,
) => void;
+openSelectDialog: (
config: {|
showImages: boolean,
showVideos: boolean,
|},
successCallback: (imageURL: string, height: number, width: number) => void,
cancelCallback: () => void,
) => void;
+clearAllPendingVideos: () => void;
+removePendingVideo: (url: string) => void;
}
export default (TurboModuleRegistry.get<Spec>('ImagePickerIOS'): ?Spec);

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
@protocol RCTAnimatedImage <NSObject>
@property (nonatomic, assign, readonly) NSUInteger animatedImageFrameCount;
@property (nonatomic, assign, readonly) NSUInteger animatedImageLoopCount;
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@end
@interface RCTAnimatedImage : UIImage <RCTAnimatedImage>
@end

View File

@ -0,0 +1,169 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <ImageIO/ImageIO.h>
#import <React/RCTAnimatedImage.h>
@interface RCTGIFCoderFrame : NSObject
@property (nonatomic, assign) NSUInteger index;
@property (nonatomic, assign) NSTimeInterval duration;
@end
@implementation RCTGIFCoderFrame
@end
@implementation RCTAnimatedImage {
CGImageSourceRef _imageSource;
CGFloat _scale;
NSUInteger _loopCount;
NSUInteger _frameCount;
NSArray<RCTGIFCoderFrame *> *_frames;
}
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale
{
if (self = [super init]) {
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!imageSource) {
return nil;
}
BOOL framesValid = [self scanAndCheckFramesValidWithSource:imageSource];
if (!framesValid) {
CFRelease(imageSource);
return nil;
}
_imageSource = imageSource;
// grab image at the first index
UIImage *image = [self animatedImageFrameAtIndex:0];
if (!image) {
return nil;
}
self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (BOOL)scanAndCheckFramesValidWithSource:(CGImageSourceRef)imageSource
{
if (!imageSource) {
return NO;
}
NSUInteger frameCount = CGImageSourceGetCount(imageSource);
NSUInteger loopCount = [self imageLoopCountWithSource:imageSource];
NSMutableArray<RCTGIFCoderFrame *> *frames = [NSMutableArray array];
for (size_t i = 0; i < frameCount; i++) {
RCTGIFCoderFrame *frame = [[RCTGIFCoderFrame alloc] init];
frame.index = i;
frame.duration = [self frameDurationAtIndex:i source:imageSource];
[frames addObject:frame];
}
_frameCount = frameCount;
_loopCount = loopCount;
_frames = [frames copy];
return YES;
}
- (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source
{
NSUInteger loopCount = 1;
NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil);
NSDictionary *gifProperties = imageProperties[(__bridge NSString *)kCGImagePropertyGIFDictionary];
if (gifProperties) {
NSNumber *gifLoopCount = gifProperties[(__bridge NSString *)kCGImagePropertyGIFLoopCount];
if (gifLoopCount != nil) {
loopCount = gifLoopCount.unsignedIntegerValue;
// A loop count of 1 means it should repeat twice, 2 means, thrice, etc.
if (loopCount != 0) {
loopCount++;
}
}
}
return loopCount;
}
- (float)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source
{
float frameDuration = 0.1f;
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
if (!cfFrameProperties) {
return frameDuration;
}
NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
if (delayTimeUnclampedProp != nil && [delayTimeUnclampedProp floatValue] != 0.0f) {
frameDuration = [delayTimeUnclampedProp floatValue];
} else {
NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
if (delayTimeProp != nil) {
frameDuration = [delayTimeProp floatValue];
}
}
CFRelease(cfFrameProperties);
return frameDuration;
}
- (NSUInteger)animatedImageLoopCount
{
return _loopCount;
}
- (NSUInteger)animatedImageFrameCount
{
return _frameCount;
}
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index
{
if (index >= _frameCount) {
return 0;
}
return _frames[index].duration;
}
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index
{
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
if (!imageRef) {
return nil;
}
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
return image;
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification
{
if (_imageSource) {
for (size_t i = 0; i < _frameCount; i++) {
CGImageSourceRemoveCacheAtIndex(_imageSource, i);
}
}
}
- (void)dealloc
{
if (_imageSource) {
CFRelease(_imageSource);
_imageSource = NULL;
}
}
@end

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
@protocol RCTDisplayRefreshable
- (void)displayDidRefresh:(CADisplayLink *)displayLink;
@end
@interface RCTDisplayWeakRefreshable : NSObject
@property (nonatomic, weak) id<RCTDisplayRefreshable> refreshable;
+ (CADisplayLink *)displayLinkWithWeakRefreshable:(id<RCTDisplayRefreshable>)refreshable;
@end

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTDisplayWeakRefreshable.h"
@implementation RCTDisplayWeakRefreshable
+ (CADisplayLink *)displayLinkWithWeakRefreshable:(id<RCTDisplayRefreshable>)refreshable {
RCTDisplayWeakRefreshable *target = [[RCTDisplayWeakRefreshable alloc] initWithRefreshable:refreshable];
return [CADisplayLink displayLinkWithTarget:target selector:@selector(displayDidRefresh:)];
}
- (instancetype)initWithRefreshable:(id<RCTDisplayRefreshable>)refreshable
{
if (self = [super init]) {
_refreshable = refreshable;
}
return self;
}
- (void)displayDidRefresh:(CADisplayLink *)displayLink {
[_refreshable displayDidRefresh:displayLink];
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageDataDecoder.h>
@interface RCTGIFImageDecoder : NSObject <RCTImageDataDecoder>
@end

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTGIFImageDecoder.h>
#import <ImageIO/ImageIO.h>
#import <QuartzCore/QuartzCore.h>
#import <React/RCTAnimatedImage.h>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTTurboModule.h>
#import "RCTImagePlugins.h"
@interface RCTGIFImageDecoder() <RCTTurboModule>
@end
@implementation RCTGIFImageDecoder
RCT_EXPORT_MODULE()
- (BOOL)canDecodeImageData:(NSData *)imageData
{
char header[7] = {};
[imageData getBytes:header length:6];
return !strcmp(header, "GIF87a") || !strcmp(header, "GIF89a");
}
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
{
RCTAnimatedImage *image = [[RCTAnimatedImage alloc] initWithData:imageData scale:scale];
if (!image) {
completionHandler(nil, nil);
return ^{};
}
completionHandler(nil, image);
return ^{};
}
@end
Class RCTGIFImageDecoderCls() {
return RCTGIFImageDecoder.class;
}

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Accelerate/Accelerate.h>
#import <UIKit/UIKit.h>
#import <React/RCTDefines.h>
RCT_EXTERN UIImage *RCTBlurredImageWithRadius(UIImage *inputImage, CGFloat radius);

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageBlurUtils.h>
UIImage *RCTBlurredImageWithRadius(UIImage *inputImage, CGFloat radius)
{
CGImageRef imageRef = inputImage.CGImage;
CGFloat imageScale = inputImage.scale;
UIImageOrientation imageOrientation = inputImage.imageOrientation;
// Image must be nonzero size
if (CGImageGetWidth(imageRef) * CGImageGetHeight(imageRef) == 0) {
return inputImage;
}
//convert to ARGB if it isn't
if (CGImageGetBitsPerPixel(imageRef) != 32 ||
CGImageGetBitsPerComponent(imageRef) != 8 ||
!((CGImageGetBitmapInfo(imageRef) & kCGBitmapAlphaInfoMask))) {
UIGraphicsBeginImageContextWithOptions(inputImage.size, NO, inputImage.scale);
[inputImage drawAtPoint:CGPointZero];
imageRef = UIGraphicsGetImageFromCurrentImageContext().CGImage;
UIGraphicsEndImageContext();
}
vImage_Buffer buffer1, buffer2;
buffer1.width = buffer2.width = CGImageGetWidth(imageRef);
buffer1.height = buffer2.height = CGImageGetHeight(imageRef);
buffer1.rowBytes = buffer2.rowBytes = CGImageGetBytesPerRow(imageRef);
size_t bytes = buffer1.rowBytes * buffer1.height;
buffer1.data = malloc(bytes);
buffer2.data = malloc(bytes);
if (!buffer1.data || !buffer2.data) {
// CWE - 391 : Unchecked error condition
// https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html
// https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c
abort();
}
// A description of how to compute the box kernel width from the Gaussian
// radius (aka standard deviation) appears in the SVG spec:
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
uint32_t boxSize = floor((radius * imageScale * 3 * sqrt(2 * M_PI) / 4 + 0.5) / 2);
boxSize |= 1; // Ensure boxSize is odd
//create temp buffer
void *tempBuffer = malloc((size_t)vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, NULL, 0, 0, boxSize, boxSize,
NULL, kvImageEdgeExtend + kvImageGetTempBufferSize));
if (!tempBuffer) {
// CWE - 391 : Unchecked error condition
// https://www.cvedetails.com/cwe-details/391/Unchecked-Error-Condition.html
// https://eli.thegreenplace.net/2009/10/30/handling-out-of-memory-conditions-in-c
abort();
}
//copy image data
CFDataRef dataSource = CGDataProviderCopyData(CGImageGetDataProvider(imageRef));
memcpy(buffer1.data, CFDataGetBytePtr(dataSource), bytes);
CFRelease(dataSource);
//perform blur
vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&buffer2, &buffer1, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
//free buffers
free(buffer2.data);
free(tempBuffer);
//create image context from buffer
CGContextRef ctx = CGBitmapContextCreate(buffer1.data, buffer1.width, buffer1.height,
8, buffer1.rowBytes, CGImageGetColorSpace(imageRef),
CGImageGetBitmapInfo(imageRef));
//create image from context
imageRef = CGBitmapContextCreateImage(ctx);
UIImage *outputImage = [UIImage imageWithCGImage:imageRef scale:imageScale orientation:imageOrientation];
CGImageRelease(imageRef);
CGContextRelease(ctx);
free(buffer1.data);
return outputImage;
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <React/RCTResizeMode.h>
@interface UIImage (React)
/**
* Memory bytes of the image with the default calculation of static image or GIF. Custom calculations of decoded bytes can be assigned manually.
*/
@property (nonatomic, assign) NSInteger reactDecodedImageBytes;
@end
/**
* Provides an interface to use for providing a image caching strategy.
*/
@protocol RCTImageCache <NSObject>
- (UIImage *)imageForUrl:(NSString *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode;
- (void)addImageToCache:(UIImage *)image
URL:(NSString *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
response:(NSURLResponse *)response;
@end
@interface RCTImageCache : NSObject <RCTImageCache>
@end

View File

@ -0,0 +1,163 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageCache.h>
#import <objc/runtime.h>
#import <ImageIO/ImageIO.h>
#import <React/RCTConvert.h>
#import <React/RCTNetworking.h>
#import <React/RCTUtils.h>
#import <React/RCTResizeMode.h>
#import <React/RCTImageUtils.h>
static const NSUInteger RCTMaxCachableDecodedImageSizeInBytes = 2097152; // 2 MB
static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale,
RCTResizeMode resizeMode)
{
return [NSString stringWithFormat:@"%@|%g|%g|%g|%lld",
imageTag, size.width, size.height, scale, (long long)resizeMode];
}
@implementation RCTImageCache
{
NSOperationQueue *_imageDecodeQueue;
NSCache *_decodedImageCache;
NSMutableDictionary *_cacheStaleTimes;
}
- (instancetype)init
{
if (self = [super init]) {
_decodedImageCache = [NSCache new];
_decodedImageCache.totalCostLimit = 20 * 1024 * 1024; // 20 MB
_cacheStaleTimes = [[NSMutableDictionary alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearCache)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearCache)
name:UIApplicationWillResignActiveNotification
object:nil];
}
return self;
}
- (void)clearCache
{
[_decodedImageCache removeAllObjects];
@synchronized(_cacheStaleTimes) {
[_cacheStaleTimes removeAllObjects];
}
}
- (void)addImageToCache:(UIImage *)image
forKey:(NSString *)cacheKey
{
if (!image) {
return;
}
NSInteger bytes = image.reactDecodedImageBytes;
if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) {
[self->_decodedImageCache setObject:image
forKey:cacheKey
cost:bytes];
}
}
- (UIImage *)imageForUrl:(NSString *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
{
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
@synchronized(_cacheStaleTimes) {
id staleTime = _cacheStaleTimes[cacheKey];
if (staleTime) {
if ([[NSDate new] compare:(NSDate *)staleTime] == NSOrderedDescending) {
// cached image has expired, clear it out to make room for others
[_cacheStaleTimes removeObjectForKey:cacheKey];
[_decodedImageCache removeObjectForKey:cacheKey];
return nil;
}
}
}
return [_decodedImageCache objectForKey:cacheKey];
}
- (void)addImageToCache:(UIImage *)image
URL:(NSString *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
response:(NSURLResponse *)response
{
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSString *cacheKey = RCTCacheKeyForImage(url, size, scale, resizeMode);
BOOL shouldCache = YES;
NSString *responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"];
NSDate *originalDate = [self dateWithHeaderString:responseDate];
NSString *cacheControl = ((NSHTTPURLResponse *)response).allHeaderFields[@"Cache-Control"];
NSDate *staleTime;
NSArray<NSString *> *components = [cacheControl componentsSeparatedByString:@","];
for (NSString *component in components) {
if ([component containsString:@"no-cache"] || [component containsString:@"no-store"] || [component hasSuffix:@"max-age=0"]) {
shouldCache = NO;
break;
} else {
NSRange range = [component rangeOfString:@"max-age="];
if (range.location != NSNotFound) {
NSInteger seconds = [[component substringFromIndex:range.location + range.length] integerValue];
staleTime = [originalDate dateByAddingTimeInterval:(NSTimeInterval)seconds];
}
}
}
if (shouldCache) {
if (!staleTime && originalDate) {
NSString *expires = ((NSHTTPURLResponse *)response).allHeaderFields[@"Expires"];
NSString *lastModified = ((NSHTTPURLResponse *)response).allHeaderFields[@"Last-Modified"];
if (expires) {
staleTime = [self dateWithHeaderString:expires];
} else if (lastModified) {
NSDate *lastModifiedDate = [self dateWithHeaderString:lastModified];
if (lastModifiedDate) {
NSTimeInterval interval = [originalDate timeIntervalSinceDate:lastModifiedDate] / 10;
staleTime = [originalDate dateByAddingTimeInterval:interval];
}
}
}
if (staleTime) {
@synchronized(_cacheStaleTimes) {
_cacheStaleTimes[cacheKey] = staleTime;
}
}
return [self addImageToCache:image forKey:cacheKey];
}
}
}
- (NSDate *)dateWithHeaderString:(NSString *)headerDateString {
static NSDateFormatter *formatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
formatter = [[NSDateFormatter alloc] init];
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
formatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
});
return [formatter dateFromString:headerDateString];
}
@end

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTResizeMode.h>
#import <React/RCTURLRequestHandler.h>
#import <React/RCTImageURLLoader.h>
/**
* Provides the interface needed to register an image decoder. Image decoders
* are also bridge modules, so should be registered using RCT_EXPORT_MODULE().
*/
@protocol RCTImageDataDecoder <RCTBridgeModule>
/**
* Indicates whether this handler is capable of decoding the specified data.
* Typically the handler would examine some sort of header data to determine
* this.
*/
- (BOOL)canDecodeImageData:(NSData *)imageData;
/**
* Decode an image from the data object. The method should call the
* completionHandler when the decoding operation has finished. The method
* should also return a cancellation block, if applicable.
*
* If you provide a custom image decoder, you most implement scheduling yourself,
* to avoid decoding large amounts of images at the same time.
*/
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;
@optional
/**
* If more than one RCTImageDataDecoder responds YES to `-canDecodeImageData:`
* then `decoderPriority` is used to determine which one to use. The decoder
* with the highest priority will be selected. Default priority is zero.
* If two or more valid decoders have the same priority, the selection order is
* undefined.
*/
- (float)decoderPriority;
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBridgeModule.h>
@interface RCTImageEditingManager : NSObject <RCTBridgeModule>
@end

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageEditingManager.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConvert.h>
#import <React/RCTImageLoader.h>
#import <React/RCTImageStoreManager.h>
#import <React/RCTImageUtils.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import <UIKit/UIKit.h>
#import "RCTImagePlugins.h"
@interface RCTImageEditingManager() <NativeImageEditorSpec>
@end
@implementation RCTImageEditingManager
RCT_EXPORT_MODULE()
@synthesize bridge = _bridge;
/**
* Crops an image and adds the result to the image store.
*
* @param imageRequest An image URL
* @param cropData Dictionary with `offset`, `size` and `displaySize`.
* `offset` and `size` are relative to the full-resolution image size.
* `displaySize` is an optimization - if specified, the image will
* be scaled down to `displaySize` rather than `size`.
* All units are in px (not points).
*/
RCT_EXPORT_METHOD(cropImage:(NSURLRequest *)imageRequest
cropData:(JS::NativeImageEditor::Options &)cropData
successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseSenderBlock)errorCallback)
{
CGRect rect = {
[RCTConvert CGPoint:@{
@"x": @(cropData.offset().x()),
@"y": @(cropData.offset().y()),
}],
[RCTConvert CGSize:@{
@"width": @(cropData.size().width()),
@"height": @(cropData.size().height()),
}]
};
// We must keep a copy of cropData so that we can access data from it at a later time
JS::NativeImageEditor::Options cropDataCopy = cropData;
[[_bridge moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES]
loadImageWithURLRequest:imageRequest callback:^(NSError *error, UIImage *image) {
if (error) {
errorCallback(@[RCTJSErrorFromNSError(error)]);
return;
}
// Crop image
CGSize targetSize = rect.size;
CGRect targetRect = {{-rect.origin.x, -rect.origin.y}, image.size};
CGAffineTransform transform = RCTTransformFromTargetRect(image.size, targetRect);
UIImage *croppedImage = RCTTransformImage(image, targetSize, image.scale, transform);
// Scale image
if (cropDataCopy.displaySize()) {
targetSize = [RCTConvert CGSize:@{@"width": @(cropDataCopy.displaySize()->width()), @"height": @(cropDataCopy.displaySize()->height())}]; // in pixels
RCTResizeMode resizeMode = [RCTConvert RCTResizeMode:cropDataCopy.resizeMode() ?: @"contain"];
targetRect = RCTTargetRect(croppedImage.size, targetSize, 1, resizeMode);
transform = RCTTransformFromTargetRect(croppedImage.size, targetRect);
croppedImage = RCTTransformImage(croppedImage, targetSize, image.scale, transform);
}
// Store image
[self->_bridge.imageStoreManager storeImage:croppedImage withBlock:^(NSString *croppedImageTag) {
if (!croppedImageTag) {
NSString *errorMessage = @"Error storing cropped image in RCTImageStoreManager";
RCTLogWarn(@"%@", errorMessage);
errorCallback(@[RCTJSErrorFromNSError(RCTErrorWithMessage(errorMessage))]);
return;
}
successCallback(@[croppedImageTag]);
}];
}];
}
- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
nativeInvoker:(std::shared_ptr<facebook::react::CallInvoker>)nativeInvoker
perfLogger:(id<RCTTurboModulePerformanceLogger>)perfLogger
{
return std::make_shared<facebook::react::NativeImageEditorSpecJSI>(self, jsInvoker, nativeInvoker, perfLogger);
}
@end
Class RCTImageEditingManagerCls() {
return RCTImageEditingManager.class;
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTDefines.h>
#import <React/RCTResizeMode.h>
#import <React/RCTURLRequestHandler.h>
#import <React/RCTImageDataDecoder.h>
#import <React/RCTImageURLLoader.h>
#import <React/RCTImageCache.h>
#import <React/RCTImageLoaderProtocol.h>
@interface RCTImageLoader : NSObject <RCTBridgeModule, RCTImageLoaderProtocol>
- (instancetype)init;
- (instancetype)initWithRedirectDelegate:(id<RCTImageRedirectProtocol>)redirectDelegate NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithRedirectDelegate:(id<RCTImageRedirectProtocol>)redirectDelegate
loadersProvider:(NSArray<id<RCTImageURLLoader>> * (^)(void))getLoaders
decodersProvider:(NSArray<id<RCTImageDataDecoder>> * (^)(void))getDecoders;
@end
/**
* DEPRECATED!! DO NOT USE
* Instead use `[_bridge moduleForClass:[RCTImageLoader class]]`
*/
@interface RCTBridge (RCTImageLoader)
@property (nonatomic, readonly) RCTImageLoader *imageLoader;
@end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,134 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTResizeMode.h>
#import <React/RCTURLRequestHandler.h>
#import <React/RCTImageDataDecoder.h>
#import <React/RCTImageURLLoader.h>
#import <React/RCTImageCache.h>
/**
* If available, RCTImageRedirectProtocol is invoked before loading an asset.
* Implementation should return either a new URL or nil when redirection is
* not needed.
*/
@protocol RCTImageRedirectProtocol
- (NSURL *)redirectAssetsURL:(NSURL *)URL;
@end
/**
* Image Downloading priority.
* Use PriorityImmediate to download images at the highest priority.
* Use PriorityPrefetch to prefetch images at a lower priority.
* The priority logic is up to each @RCTImageLoaderProtocol implementation
*/
typedef NS_ENUM(NSUInteger, RCTImageLoaderPriority) {
RCTImageLoaderPriorityImmediate,
RCTImageLoaderPriorityPrefetch
};
@protocol RCTImageLoaderProtocol<RCTURLRequestHandler>
/**
* The maximum number of concurrent image loading tasks. Loading and decoding
* images can consume a lot of memory, so setting this to a higher value may
* cause memory to spike. If you are seeing out-of-memory crashes, try reducing
* this value.
*/
@property (nonatomic, assign) NSUInteger maxConcurrentLoadingTasks;
/**
* The maximum number of concurrent image decoding tasks. Decoding large
* images can be especially CPU and memory intensive, so if your are decoding a
* lot of large images in your app, you may wish to adjust this value.
*/
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingTasks;
/**
* Decoding large images can use a lot of memory, and potentially cause the app
* to crash. This value allows you to throttle the amount of memory used by the
* decoder independently of the number of concurrent threads. This means you can
* still decode a lot of small images in parallel, without allowing the decoder
* to try to decompress multiple huge images at once. Note that this value is
* only a hint, and not an indicator of the total memory used by the app.
*/
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingBytes;
/**
* Loads the specified image at the highest available resolution.
* Can be called from any thread, will call back on an unspecified thread.
*/
- (nullable RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
callback:(RCTImageLoaderCompletionBlock)callback;
/**
* As above, but includes download `priority`.
*/
- (nullable RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
priority:(RCTImageLoaderPriority)priority
callback:(RCTImageLoaderCompletionBlock)callback;
/**
* As above, but includes target `size`, `scale` and `resizeMode`, which are used to
* select the optimal dimensions for the loaded image. The `clipped` option
* controls whether the image will be clipped to fit the specified size exactly,
* or if the original aspect ratio should be retained.
* `partialLoadBlock` is meant for custom image loaders that do not ship with the core RN library.
* It is meant to be called repeatedly while loading the image as higher quality versions are decoded,
* for instance with progressive JPEGs.
*/
- (nullable RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
size:(CGSize)size
scale:(CGFloat)scale
clipped:(BOOL)clipped
resizeMode:(RCTResizeMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;
/**
* Finds an appropriate image decoder and passes the target `size`, `scale` and
* `resizeMode` for optimal image decoding. The `clipped` option controls
* whether the image will be clipped to fit the specified size exactly, or
* if the original aspect ratio should be retained. Can be called from any
* thread, will call callback on an unspecified thread.
*/
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
size:(CGSize)size
scale:(CGFloat)scale
clipped:(BOOL)clipped
resizeMode:(RCTResizeMode)resizeMode
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;
/**
* Get image size, in pixels. This method will do the least work possible to get
* the information, and won't decode the image if it doesn't have to.
*/
- (RCTImageLoaderCancellationBlock)getImageSizeForURLRequest:(NSURLRequest *)imageURLRequest
block:(void(^)(NSError *error, CGSize size))completionBlock;
/**
* Determines whether given image URLs are cached locally. The `requests` array is expected
* to contain objects convertible to NSURLRequest. The return value maps URLs to strings:
* "disk" for images known to be cached in non-volatile storage, "memory" for images known
* to be cached in memory. Dictionary items corresponding to images that are not known to be
* cached are simply missing.
*/
- (NSDictionary *)getImageCacheStatus:(NSArray *)requests;
/**
* Allows developers to set their own caching implementation for
* decoded images as long as it conforms to the RCTImageCache
* protocol. This method should be called in bridgeDidInitializeModule.
*/
- (void)setImageCache:(id<RCTImageCache>)cache;
@end

View File

@ -0,0 +1,53 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTImageURLLoaderWithAttribution.h>
RCT_EXTERN BOOL RCTImageLoadingInstrumentationEnabled(void);
RCT_EXTERN BOOL RCTImageLoadingPerfInstrumentationEnabled(void);
RCT_EXTERN void RCTEnableImageLoadingInstrumentation(BOOL enabled);
RCT_EXTERN void RCTEnableImageLoadingPerfInstrumentation(BOOL enabled);
@protocol RCTImageLoaderWithAttributionProtocol<RCTImageLoaderProtocol>
// TODO (T61325135): Remove C++ checks
#ifdef __cplusplus
/**
* Same as the variant in RCTImageURLLoaderProtocol, but allows passing attribution
* information that each image URL loader can process.
*/
- (RCTImageURLLoaderRequest *)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
size:(CGSize)size
scale:(CGFloat)scale
clipped:(BOOL)clipped
resizeMode:(RCTResizeMode)resizeMode
priority: (RCTImageLoaderPriority)priority
attribution:(const facebook::react::ImageURLLoaderAttribution &)attribution
progressBlock:(RCTImageLoaderProgressBlock)progressBlock
partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;
#endif
/**
* Image instrumentation - notify that the image content (UIImage) has been set on the native view.
*/
- (void)trackURLImageContentDidSetForRequest:(RCTImageURLLoaderRequest *)loaderRequest;
/**
* Image instrumentation - start tracking the on-screen visibility of the native image view.
*/
- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(UIView *)imageView;
/**
* Image instrumentation - notify that the native image view was destroyed.
*/
- (void)trackURLImageDidDestroy:(RCTImageURLLoaderRequest *)loaderRequest;
@end

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated by an internal plugin build system
*/
#ifdef RN_DISABLE_OSS_PLUGIN_HEADER
// FB Internal: FBRCTImagePlugins.h is autogenerated by the build system.
#import <React/FBRCTImagePlugins.h>
#else
// OSS-compatibility layer
#import <Foundation/Foundation.h>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wreturn-type-c-linkage"
#ifdef __cplusplus
extern "C" {
#endif
// RCTTurboModuleManagerDelegate should call this to resolve module classes.
Class RCTImageClassProvider(const char *name);
// Lookup functions
Class RCTGIFImageDecoderCls(void) __attribute__((used));
Class RCTImageEditingManagerCls(void) __attribute__((used));
Class RCTImageLoaderCls(void) __attribute__((used));
Class RCTImageStoreManagerCls(void) __attribute__((used));
Class RCTLocalAssetImageLoaderCls(void) __attribute__((used));
#ifdef __cplusplus
}
#endif
#pragma GCC diagnostic pop
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @generated by an internal plugin build system
*/
#ifndef RN_DISABLE_OSS_PLUGIN_HEADER
// OSS-compatibility layer
#import "RCTImagePlugins.h"
#import <string>
#import <unordered_map>
Class RCTImageClassProvider(const char *name) {
static std::unordered_map<std::string, Class (*)(void)> sCoreModuleClassMap = {
{"GIFImageDecoder", RCTGIFImageDecoderCls},
{"ImageEditingManager", RCTImageEditingManagerCls},
{"ImageLoader", RCTImageLoaderCls},
{"ImageStoreManager", RCTImageStoreManagerCls},
{"LocalAssetImageLoader", RCTLocalAssetImageLoaderCls},
};
auto p = sCoreModuleClassMap.find(name);
if (p != sCoreModuleClassMap.end()) {
auto classFunc = p->second;
return classFunc();
}
return nil;
}
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTShadowView.h>
@interface RCTImageShadowView : RCTShadowView
@end

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageShadowView.h>
#import <React/RCTLog.h>
@implementation RCTImageShadowView
- (BOOL)isYogaLeafNode
{
return YES;
}
- (BOOL)canHaveSubviews
{
return NO;
}
@end

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTURLRequestHandler.h>
@interface RCTImageStoreManager : NSObject <RCTURLRequestHandler>
/**
* Set and get cached image data asynchronously. It is safe to call these from any
* thread. The callbacks will be called on an unspecified thread.
*/
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)(void))block;
- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block;
- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block;
/**
* Convenience method to store an image directly (image is converted to data
* internally, so any metadata such as scale or orientation will be lost).
*/
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block;
@end
@interface RCTImageStoreManager (Deprecated)
/**
* These methods are deprecated - use the data-based alternatives instead.
*/
- (NSString *)storeImage:(UIImage *)image __deprecated;
- (UIImage *)imageForTag:(NSString *)imageTag __deprecated;
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block __deprecated;
@end
@interface RCTBridge (RCTImageStoreManager)
@property (nonatomic, readonly) RCTImageStoreManager *imageStoreManager;
@end

View File

@ -0,0 +1,258 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageStoreManager.h>
#import <atomic>
#import <memory>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/UTType.h>
#import <React/RCTAssert.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import <React/RCTImageUtils.h>
#import "RCTImagePlugins.h"
static NSString *const RCTImageStoreURLScheme = @"rct-image-store";
@interface RCTImageStoreManager() <NativeImageStoreSpec>
@end
@implementation RCTImageStoreManager
{
NSMutableDictionary<NSString *, NSData *> *_store;
NSUInteger _id;
}
@synthesize methodQueue = _methodQueue;
RCT_EXPORT_MODULE()
- (float)handlerPriority
{
return 1;
}
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)(void))block
{
dispatch_async(_methodQueue, ^{
[self removeImageForTag:imageTag];
if (block) {
block();
}
});
}
- (NSString *)_storeImageData:(NSData *)imageData
{
RCTAssertThread(_methodQueue, @"Must be called on RCTImageStoreManager thread");
if (!_store) {
_store = [NSMutableDictionary new];
_id = 0;
}
NSString *imageTag = [NSString stringWithFormat:@"%@://%tu", RCTImageStoreURLScheme, _id++];
_store[imageTag] = imageData;
return imageTag;
}
- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block
{
RCTAssertParam(block);
dispatch_async(_methodQueue, ^{
block([self _storeImageData:imageData]);
});
}
- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block
{
RCTAssertParam(block);
dispatch_async(_methodQueue, ^{
block(self->_store[imageTag]);
});
}
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
{
RCTAssertParam(block);
dispatch_async(_methodQueue, ^{
NSString *imageTag = [self _storeImageData:RCTGetImageData(image, 0.75)];
dispatch_async(dispatch_get_main_queue(), ^{
block(imageTag);
});
});
}
RCT_EXPORT_METHOD(removeImageForTag:(NSString *)imageTag)
{
[_store removeObjectForKey:imageTag];
}
RCT_EXPORT_METHOD(hasImageForTag:(NSString *)imageTag
callback:(RCTResponseSenderBlock)callback)
{
callback(@[@(_store[imageTag] != nil)]);
}
// TODO (#5906496): Name could be more explicit - something like getBase64EncodedDataForTag:?
RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag
successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseSenderBlock)errorCallback)
{
NSData *imageData = _store[imageTag];
if (!imageData) {
errorCallback(@[RCTJSErrorFromNSError(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]))]);
return;
}
// Dispatching to a background thread to perform base64 encoding
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
successCallback(@[[imageData base64EncodedStringWithOptions:0]]);
});
}
RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseSenderBlock)errorCallback)
{
// Dispatching to a background thread to perform base64 decoding
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
if (imageData) {
dispatch_async(self->_methodQueue, ^{
successCallback(@[[self _storeImageData:imageData]]);
});
} else {
errorCallback(@[RCTJSErrorFromNSError(RCTErrorWithMessage(@"Failed to add image from base64String"))]);
}
});
}
#pragma mark - RCTURLRequestHandler
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
return [request.URL.scheme caseInsensitiveCompare:RCTImageStoreURLScheme] == NSOrderedSame;
}
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
{
__block auto cancelled = std::make_shared<std::atomic<bool>>(false);
void (^cancellationBlock)(void) = ^{
cancelled->store(true);
};
// Dispatch async to give caller time to cancel the request
dispatch_async(_methodQueue, ^{
if (cancelled->load()) {
return;
}
NSString *imageTag = request.URL.absoluteString;
NSData *imageData = self->_store[imageTag];
if (!imageData) {
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]);
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (!sourceRef) {
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Unable to decode data for imageTag: %@", imageTag]);
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}
CFStringRef UTI = CGImageSourceGetType(sourceRef);
CFRelease(sourceRef);
NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
MIMEType:MIMEType
expectedContentLength:imageData.length
textEncodingName:nil];
CFRelease(UTI);
[delegate URLRequest:cancellationBlock didReceiveResponse:response];
[delegate URLRequest:cancellationBlock didReceiveData:imageData];
[delegate URLRequest:cancellationBlock didCompleteWithError:nil];
});
return cancellationBlock;
}
- (void)cancelRequest:(id)requestToken
{
if (requestToken) {
((void (^)(void))requestToken)();
}
}
@end
@implementation RCTImageStoreManager (Deprecated)
- (NSString *)storeImage:(UIImage *)image
{
RCTAssertMainQueue();
RCTLogWarn(@"RCTImageStoreManager.storeImage() is deprecated and has poor performance. Use an alternative method instead.");
__block NSString *imageTag;
dispatch_sync(_methodQueue, ^{
imageTag = [self _storeImageData:RCTGetImageData(image, 0.75)];
});
return imageTag;
}
- (UIImage *)imageForTag:(NSString *)imageTag
{
RCTAssertMainQueue();
RCTLogWarn(@"RCTImageStoreManager.imageForTag() is deprecated and has poor performance. Use an alternative method instead.");
__block NSData *imageData;
dispatch_sync(_methodQueue, ^{
imageData = self->_store[imageTag];
});
return [UIImage imageWithData:imageData];
}
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
{
RCTAssertParam(block);
dispatch_async(_methodQueue, ^{
NSData *imageData = self->_store[imageTag];
dispatch_async(dispatch_get_main_queue(), ^{
// imageWithData: is not thread-safe, so we can't do this on methodQueue
block([UIImage imageWithData:imageData]);
});
});
}
- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModuleWithJsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
nativeInvoker:(std::shared_ptr<facebook::react::CallInvoker>)nativeInvoker
perfLogger:(id<RCTTurboModulePerformanceLogger>)perfLogger
{
return std::make_shared<facebook::react::NativeImageStoreSpecJSI>(self, jsInvoker, nativeInvoker, perfLogger);
}
@end
@implementation RCTBridge (RCTImageStoreManager)
- (RCTImageStoreManager *)imageStoreManager
{
return [self moduleForClass:[RCTImageStoreManager class]];
}
@end
Class RCTImageStoreManagerCls(void) {
return RCTImageStoreManager.class;
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTResizeMode.h>
typedef void (^RCTImageLoaderProgressBlock)(int64_t progress, int64_t total);
typedef void (^RCTImageLoaderPartialLoadBlock)(UIImage *image);
typedef void (^RCTImageLoaderCompletionBlock)(NSError *error, UIImage *image);
typedef dispatch_block_t RCTImageLoaderCancellationBlock;
/**
* Provides the interface needed to register an image loader. Image data
* loaders are also bridge modules, so should be registered using
* RCT_EXPORT_MODULE().
*/
@protocol RCTImageURLLoader <RCTBridgeModule>
/**
* Indicates whether this data loader is capable of processing the specified
* request URL. Typically the handler would examine the scheme/protocol of the
* URL to determine this.
*/
- (BOOL)canLoadImageURL:(NSURL *)requestURL;
/**
* Send a network request to load the request URL. The method should call the
* progressHandler (if applicable) and the completionHandler when the request
* has finished. The method should also return a cancellation block, if
* applicable.
*/
- (nullable RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;
@optional
/**
* If more than one RCTImageURLLoader responds YES to `-canLoadImageURL:`
* then `loaderPriority` is used to determine which one to use. The loader
* with the highest priority will be selected. Default priority is zero. If
* two or more valid loaders have the same priority, the selection order is
* undefined.
*/
- (float)loaderPriority;
/**
* If the loader must be called on the serial url cache queue, and whether the completion
* block should be dispatched off the main thread. If this is NO, the loader will be
* called from the main queue. Defaults to YES.
*
* Use with care: disabling scheduling will reduce RCTImageLoader's ability to throttle
* network requests.
*/
- (BOOL)requiresScheduling;
/**
* If images loaded by the loader should be cached in the decoded image cache.
* Defaults to YES.
*/
- (BOOL)shouldCacheLoadedImages;
@end

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageURLLoader.h>
#import <React/RCTImageLoaderProtocol.h>
// TODO (T61325135): Remove C++ checks
#ifdef __cplusplus
namespace facebook {
namespace react {
struct ImageURLLoaderAttribution {
int32_t nativeViewTag = 0;
int32_t surfaceId = 0;
};
} // namespace react
} // namespace facebook
#endif
@interface RCTImageURLLoaderRequest : NSObject
@property (nonatomic, strong, readonly) NSString *requestId;
@property (nonatomic, strong, readonly) NSURL *imageURL;
@property (nonatomic, copy, readonly) RCTImageLoaderCancellationBlock cancellationBlock;
- (instancetype)initWithRequestId:(NSString *)requestId imageURL:(NSURL *)imageURL cancellationBlock:(RCTImageLoaderCancellationBlock)cancellationBlock;
- (void)cancel;
@end
/**
* Same as the RCTImageURLLoader interface, but allows passing in optional `attribution` information.
* This is useful for per-app logging and other instrumentation.
*/
@protocol RCTImageURLLoaderWithAttribution <RCTImageURLLoader>
// TODO (T61325135): Remove C++ checks
#ifdef __cplusplus
/**
* Same as the RCTImageURLLoader variant above, but allows optional `attribution` information.
* Caller may also specify a preferred requestId for tracking purpose.
*/
- (RCTImageURLLoaderRequest *)loadImageForURL:(NSURL *)imageURL
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
requestId:(NSString *)requestId
priority: (RCTImageLoaderPriority)priority
attribution:(const facebook::react::ImageURLLoaderAttribution &)attribution
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;
#endif
/**
* Image instrumentation - notify that the image content (UIImage) has been set on the native view.
*/
- (void)trackURLImageContentDidSetForRequest:(RCTImageURLLoaderRequest *)loaderRequest;
/**
* Image instrumentation - start tracking the on-screen visibility of the native image view.
*/
- (void)trackURLImageVisibilityForRequest:(RCTImageURLLoaderRequest *)loaderRequest imageView:(UIView *)imageView;
/**
* Image instrumentation - notify that the native image view was destroyed.
*/
- (void)trackURLImageDidDestroy:(RCTImageURLLoaderRequest *)loaderRequest;
@end

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTImageURLLoaderWithAttribution.h"
@implementation RCTImageURLLoaderRequest
- (instancetype)initWithRequestId:(NSString *)requestId imageURL:(NSURL *)imageURL cancellationBlock:(RCTImageLoaderCancellationBlock)cancellationBlock
{
if (self = [super init]) {
_requestId = requestId;
_imageURL = imageURL;
_cancellationBlock = cancellationBlock;
}
return self;
}
- (void)cancel
{
if (_cancellationBlock) {
_cancellationBlock();
}
}
@end

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTDefines.h>
#import <React/RCTResizeMode.h>
NS_ASSUME_NONNULL_BEGIN
/**
* This function takes an source size (typically from an image), a target size
* and scale that it will be drawn at (typically in a CGContext) and then
* calculates the rectangle to draw the image into so that it will be sized and
* positioned correctly according to the specified resizeMode.
*/
RCT_EXTERN CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize,
CGFloat destScale, RCTResizeMode resizeMode);
/**
* This function takes a source size (typically from an image), a target rect
* that it will be drawn into (typically relative to a CGContext), and works out
* the transform needed to draw the image at the correct scale and position.
*/
RCT_EXTERN CGAffineTransform RCTTransformFromTargetRect(CGSize sourceSize,
CGRect targetRect);
/**
* This function takes an input content size & scale (typically from an image),
* a target size & scale at which it will be displayed (typically in a
* UIImageView) and then calculates the optimal size at which to redraw the
* image so that it will be displayed correctly with the specified resizeMode.
*/
RCT_EXTERN CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale,
CGSize destSize, CGFloat destScale,
RCTResizeMode resizeMode, BOOL allowUpscaling);
/**
* This function takes an input content size & scale (typically from an image),
* a target size & scale that it will be displayed at, and determines if the
* source will need to be upscaled to fit (which may result in pixelization).
*/
RCT_EXTERN BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale,
CGSize destSize, CGFloat destScale,
RCTResizeMode resizeMode);
/**
* This function takes the source data for an image and decodes it at the
* specified size. If the original image is smaller than the destination size,
* the resultant image's scale will be decreased to compensate, so the
* width/height of the returned image is guaranteed to be >= destSize.
* Pass a destSize of CGSizeZero to decode the image at its original size.
*/
RCT_EXTERN UIImage *__nullable RCTDecodeImageWithData(NSData *data,
CGSize destSize,
CGFloat destScale,
RCTResizeMode resizeMode);
/**
* This function takes the source data for an image and decodes just the
* metadata, without decompressing the image itself.
*/
RCT_EXTERN NSDictionary<NSString *, id> *__nullable RCTGetImageMetadata(NSData *data);
/**
* Convert an image back into data. Images with an alpha channel will be
* converted to lossless PNG data. Images without alpha will be converted to
* JPEG. The `quality` argument controls the compression ratio of the JPEG
* conversion, with 1.0 being maximum quality. It has no effect for images
* using PNG compression.
*/
RCT_EXTERN NSData *__nullable RCTGetImageData(UIImage *image, float quality);
/**
* This function transforms an image. `destSize` is the size of the final image,
* and `destScale` is its scale. The `transform` argument controls how the
* source image will be mapped to the destination image.
*/
RCT_EXTERN UIImage *__nullable RCTTransformImage(UIImage *image,
CGSize destSize,
CGFloat destScale,
CGAffineTransform transform);
/*
* Return YES if image has an alpha component
*/
RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image);
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,392 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageUtils.h>
#import <tgmath.h>
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
{
return ceil(value * scale) / scale;
}
static CGFloat RCTFloorValue(CGFloat value, CGFloat scale)
{
return floor(value * scale) / scale;
}
static CGSize RCTCeilSize(CGSize size, CGFloat scale)
{
return (CGSize){
RCTCeilValue(size.width, scale),
RCTCeilValue(size.height, scale)
};
}
static CGImagePropertyOrientation CGImagePropertyOrientationFromUIImageOrientation(UIImageOrientation imageOrientation)
{
// see https://stackoverflow.com/a/6699649/496389
switch (imageOrientation) {
case UIImageOrientationUp: return kCGImagePropertyOrientationUp;
case UIImageOrientationDown: return kCGImagePropertyOrientationDown;
case UIImageOrientationLeft: return kCGImagePropertyOrientationLeft;
case UIImageOrientationRight: return kCGImagePropertyOrientationRight;
case UIImageOrientationUpMirrored: return kCGImagePropertyOrientationUpMirrored;
case UIImageOrientationDownMirrored: return kCGImagePropertyOrientationDownMirrored;
case UIImageOrientationLeftMirrored: return kCGImagePropertyOrientationLeftMirrored;
case UIImageOrientationRightMirrored: return kCGImagePropertyOrientationRightMirrored;
default: return kCGImagePropertyOrientationUp;
}
}
CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize,
CGFloat destScale, RCTResizeMode resizeMode)
{
if (CGSizeEqualToSize(destSize, CGSizeZero)) {
// Assume we require the largest size available
return (CGRect){CGPointZero, sourceSize};
}
CGFloat aspect = sourceSize.width / sourceSize.height;
// If only one dimension in destSize is non-zero (for example, an Image
// with `flex: 1` whose height is indeterminate), calculate the unknown
// dimension based on the aspect ratio of sourceSize
if (destSize.width == 0) {
destSize.width = destSize.height * aspect;
}
if (destSize.height == 0) {
destSize.height = destSize.width / aspect;
}
// Calculate target aspect ratio if needed
CGFloat targetAspect = 0.0;
if (resizeMode != RCTResizeModeCenter &&
resizeMode != RCTResizeModeStretch) {
targetAspect = destSize.width / destSize.height;
if (aspect == targetAspect) {
resizeMode = RCTResizeModeStretch;
}
}
switch (resizeMode) {
case RCTResizeModeStretch:
case RCTResizeModeRepeat:
return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)};
case RCTResizeModeContain:
if (targetAspect <= aspect) { // target is taller than content
sourceSize.width = destSize.width;
sourceSize.height = sourceSize.width / aspect;
} else { // target is wider than content
sourceSize.height = destSize.height;
sourceSize.width = sourceSize.height * aspect;
}
return (CGRect){
{
RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale),
RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale),
},
RCTCeilSize(sourceSize, destScale)
};
case RCTResizeModeCover:
if (targetAspect <= aspect) { // target is taller than content
sourceSize.height = destSize.height;
sourceSize.width = sourceSize.height * aspect;
destSize.width = destSize.height * targetAspect;
return (CGRect){
{RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale), 0},
RCTCeilSize(sourceSize, destScale)
};
} else { // target is wider than content
sourceSize.width = destSize.width;
sourceSize.height = sourceSize.width / aspect;
destSize.height = destSize.width / targetAspect;
return (CGRect){
{0, RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale)},
RCTCeilSize(sourceSize, destScale)
};
}
case RCTResizeModeCenter:
// Make sure the image is not clipped by the target.
if (sourceSize.height > destSize.height) {
sourceSize.width = destSize.width;
sourceSize.height = sourceSize.width / aspect;
}
if (sourceSize.width > destSize.width) {
sourceSize.height = destSize.height;
sourceSize.width = sourceSize.height * aspect;
}
return (CGRect){
{
RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale),
RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale),
},
RCTCeilSize(sourceSize, destScale)
};
}
}
CGAffineTransform RCTTransformFromTargetRect(CGSize sourceSize, CGRect targetRect)
{
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform,
targetRect.origin.x,
targetRect.origin.y);
transform = CGAffineTransformScale(transform,
targetRect.size.width / sourceSize.width,
targetRect.size.height / sourceSize.height);
return transform;
}
CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale,
CGSize destSize, CGFloat destScale,
RCTResizeMode resizeMode,
BOOL allowUpscaling)
{
switch (resizeMode) {
case RCTResizeModeCenter:
return RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size;
case RCTResizeModeStretch:
if (!allowUpscaling) {
CGFloat scale = sourceScale / destScale;
destSize.width = MIN(sourceSize.width * scale, destSize.width);
destSize.height = MIN(sourceSize.height * scale, destSize.height);
}
return RCTCeilSize(destSize, destScale);
default: {
// Get target size
CGSize size = RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size;
if (!allowUpscaling) {
// return sourceSize if target size is larger
if (sourceSize.width * sourceScale < size.width * destScale) {
return sourceSize;
}
}
return size;
}
}
}
BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale,
CGSize destSize, CGFloat destScale,
RCTResizeMode resizeMode)
{
if (CGSizeEqualToSize(destSize, CGSizeZero)) {
// Assume we require the largest size available
return YES;
}
// Precompensate for scale
CGFloat scale = sourceScale / destScale;
sourceSize.width *= scale;
sourceSize.height *= scale;
// Calculate aspect ratios if needed (don't bother if resizeMode == stretch)
CGFloat aspect = 0.0, targetAspect = 0.0;
if (resizeMode != UIViewContentModeScaleToFill) {
aspect = sourceSize.width / sourceSize.height;
targetAspect = destSize.width / destSize.height;
if (aspect == targetAspect) {
resizeMode = RCTResizeModeStretch;
}
}
switch (resizeMode) {
case RCTResizeModeStretch:
return destSize.width > sourceSize.width || destSize.height > sourceSize.height;
case RCTResizeModeContain:
if (targetAspect <= aspect) { // target is taller than content
return destSize.width > sourceSize.width;
} else { // target is wider than content
return destSize.height > sourceSize.height;
}
case RCTResizeModeCover:
if (targetAspect <= aspect) { // target is taller than content
return destSize.height > sourceSize.height;
} else { // target is wider than content
return destSize.width > sourceSize.width;
}
case RCTResizeModeRepeat:
case RCTResizeModeCenter:
return NO;
}
}
UIImage *__nullable RCTDecodeImageWithData(NSData *data,
CGSize destSize,
CGFloat destScale,
RCTResizeMode resizeMode)
{
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!sourceRef) {
return nil;
}
// Get original image size
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
if (!imageProperties) {
CFRelease(sourceRef);
return nil;
}
NSNumber *width = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth);
NSNumber *height = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight);
CGSize sourceSize = {width.doubleValue, height.doubleValue};
CFRelease(imageProperties);
if (CGSizeEqualToSize(destSize, CGSizeZero)) {
destSize = sourceSize;
if (!destScale) {
destScale = 1;
}
} else if (!destScale) {
destScale = RCTScreenScale();
}
if (resizeMode == UIViewContentModeScaleToFill) {
// Decoder cannot change aspect ratio, so RCTResizeModeStretch is equivalent
// to RCTResizeModeCover for our purposes
resizeMode = RCTResizeModeCover;
}
// Calculate target size
CGSize targetSize = RCTTargetSize(sourceSize, 1, destSize, destScale, resizeMode, NO);
CGSize targetPixelSize = RCTSizeInPixels(targetSize, destScale);
CGFloat maxPixelSize = fmax(fmin(sourceSize.width, targetPixelSize.width),
fmin(sourceSize.height, targetPixelSize.height));
NSDictionary<NSString *, NSNumber *> *options = @{
(id)kCGImageSourceShouldAllowFloat: @YES,
(id)kCGImageSourceCreateThumbnailWithTransform: @YES,
(id)kCGImageSourceCreateThumbnailFromImageAlways: @YES,
(id)kCGImageSourceThumbnailMaxPixelSize: @(maxPixelSize),
};
// Get thumbnail
CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
CFRelease(sourceRef);
if (!imageRef) {
return nil;
}
// Return image
UIImage *image = [UIImage imageWithCGImage:imageRef
scale:destScale
orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
return image;
}
NSDictionary<NSString *, id> *__nullable RCTGetImageMetadata(NSData *data)
{
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!sourceRef) {
return nil;
}
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
CFRelease(sourceRef);
return (__bridge_transfer id)imageProperties;
}
NSData *__nullable RCTGetImageData(UIImage *image, float quality)
{
CGImageRef cgImage = image.CGImage;
if (!cgImage) {
return NULL;
}
NSMutableDictionary *properties = [[NSMutableDictionary alloc] initWithDictionary:@{
(id)kCGImagePropertyOrientation : @(CGImagePropertyOrientationFromUIImageOrientation(image.imageOrientation))
}];
CGImageDestinationRef destination;
CFMutableDataRef imageData = CFDataCreateMutable(NULL, 0);
if (RCTImageHasAlpha(cgImage)) {
// get png data
destination = CGImageDestinationCreateWithData(imageData, kUTTypePNG, 1, NULL);
} else {
// get jpeg data
destination = CGImageDestinationCreateWithData(imageData, kUTTypeJPEG, 1, NULL);
[properties setValue:@(quality) forKey:(id)kCGImageDestinationLossyCompressionQuality];
}
if (!destination) {
CFRelease(imageData);
return NULL;
}
CGImageDestinationAddImage(destination, cgImage, (__bridge CFDictionaryRef)properties);
if (!CGImageDestinationFinalize(destination)) {
CFRelease(imageData);
imageData = NULL;
}
CFRelease(destination);
return (__bridge_transfer NSData *)imageData;
}
UIImage *__nullable RCTTransformImage(UIImage *image,
CGSize destSize,
CGFloat destScale,
CGAffineTransform transform)
{
if (destSize.width <= 0 | destSize.height <= 0 || destScale <= 0) {
return nil;
}
BOOL opaque = !RCTImageHasAlpha(image.CGImage);
UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale);
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextConcatCTM(currentContext, transform);
[image drawAtPoint:CGPointZero];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
BOOL RCTImageHasAlpha(CGImageRef image)
{
switch (CGImageGetAlphaInfo(image)) {
case kCGImageAlphaNone:
case kCGImageAlphaNoneSkipLast:
case kCGImageAlphaNoneSkipFirst:
return NO;
default:
return YES;
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
#import <React/RCTResizeMode.h>
@class RCTBridge;
@class RCTImageSource;
@interface RCTImageView : RCTView
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
@property (nonatomic, assign) UIEdgeInsets capInsets;
@property (nonatomic, strong) UIImage *defaultImage;
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
@property (nonatomic, copy) NSArray<RCTImageSource *> *imageSources;
@property (nonatomic, assign) CGFloat blurRadius;
@property (nonatomic, assign) RCTResizeMode resizeMode;
@end

View File

@ -0,0 +1,475 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageView.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTImageBlurUtils.h>
#import <React/RCTImageSource.h>
#import <React/RCTImageUtils.h>
#import <React/RCTImageLoaderWithAttributionProtocol.h>
#import <React/RCTUIImageViewAnimated.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
/**
* Determines whether an image of `currentSize` should be reloaded for display
* at `idealSize`.
*/
static BOOL RCTShouldReloadImageForSizeChange(CGSize currentSize, CGSize idealSize)
{
static const CGFloat upscaleThreshold = 1.2;
static const CGFloat downscaleThreshold = 0.5;
CGFloat widthMultiplier = idealSize.width / currentSize.width;
CGFloat heightMultiplier = idealSize.height / currentSize.height;
return widthMultiplier > upscaleThreshold || widthMultiplier < downscaleThreshold ||
heightMultiplier > upscaleThreshold || heightMultiplier < downscaleThreshold;
}
/**
* See RCTConvert (ImageSource). We want to send down the source as a similar
* JSON parameter.
*/
static NSDictionary *onLoadParamsForSource(RCTImageSource *source)
{
NSDictionary *dict = @{
@"width": @(source.size.width),
@"height": @(source.size.height),
@"url": source.request.URL.absoluteString,
};
return @{ @"source": dict };
}
@interface RCTImageView ()
@property (nonatomic, copy) RCTDirectEventBlock onLoadStart;
@property (nonatomic, copy) RCTDirectEventBlock onProgress;
@property (nonatomic, copy) RCTDirectEventBlock onError;
@property (nonatomic, copy) RCTDirectEventBlock onPartialLoad;
@property (nonatomic, copy) RCTDirectEventBlock onLoad;
@property (nonatomic, copy) RCTDirectEventBlock onLoadEnd;
@end
@implementation RCTImageView
{
// Weak reference back to the bridge, for image loading
__weak RCTBridge *_bridge;
// The image source that's currently displayed
RCTImageSource *_imageSource;
// The image source that's being loaded from the network
RCTImageSource *_pendingImageSource;
// Size of the image loaded / being loaded, so we can determine when to issue a reload to accommodate a changing size.
CGSize _targetSize;
// A block that can be invoked to cancel the most recent call to -reloadImage, if any
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
// Whether the latest change of props requires the image to be reloaded
BOOL _needsReload;
RCTUIImageViewAnimated *_imageView;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super initWithFrame:CGRectZero])) {
_bridge = bridge;
_imageView = [[RCTUIImageViewAnimated alloc] init];
_imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:_imageView];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(clearImageIfDetached)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[center addObserver:self
selector:@selector(clearImageIfDetached)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
[center addObserver:self
selector:@selector(clearImageIfDetached)
name:UISceneDidEnterBackgroundNotification
object:nil];
}
#endif
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
- (void)updateWithImage:(UIImage *)image
{
if (!image) {
_imageView.image = nil;
return;
}
// Apply rendering mode
if (_renderingMode != image.renderingMode) {
image = [image imageWithRenderingMode:_renderingMode];
}
if (_resizeMode == RCTResizeModeRepeat) {
image = [image resizableImageWithCapInsets:_capInsets resizingMode:UIImageResizingModeTile];
} else if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, _capInsets)) {
// Applying capInsets of 0 will switch the "resizingMode" of the image to "tile" which is undesired
image = [image resizableImageWithCapInsets:_capInsets resizingMode:UIImageResizingModeStretch];
}
// Apply trilinear filtering to smooth out mis-sized images
_imageView.layer.minificationFilter = kCAFilterTrilinear;
_imageView.layer.magnificationFilter = kCAFilterTrilinear;
_imageView.image = image;
}
- (void)setImage:(UIImage *)image
{
image = image ?: _defaultImage;
if (image != self.image) {
[self updateWithImage:image];
}
}
- (UIImage *)image {
return _imageView.image;
}
- (void)setBlurRadius:(CGFloat)blurRadius
{
if (blurRadius != _blurRadius) {
_blurRadius = blurRadius;
_needsReload = YES;
}
}
- (void)setCapInsets:(UIEdgeInsets)capInsets
{
if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, capInsets)) {
if (UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero) ||
UIEdgeInsetsEqualToEdgeInsets(capInsets, UIEdgeInsetsZero)) {
_capInsets = capInsets;
// Need to reload image when enabling or disabling capInsets
_needsReload = YES;
} else {
_capInsets = capInsets;
[self updateWithImage:self.image];
}
}
}
- (void)setRenderingMode:(UIImageRenderingMode)renderingMode
{
if (_renderingMode != renderingMode) {
_renderingMode = renderingMode;
[self updateWithImage:self.image];
}
}
- (void)setImageSources:(NSArray<RCTImageSource *> *)imageSources
{
if (![imageSources isEqual:_imageSources]) {
_imageSources = [imageSources copy];
_needsReload = YES;
}
}
- (void)setResizeMode:(RCTResizeMode)resizeMode
{
if (_resizeMode != resizeMode) {
_resizeMode = resizeMode;
if (_resizeMode == RCTResizeModeRepeat) {
// Repeat resize mode is handled by the UIImage. Use scale to fill
// so the repeated image fills the UIImageView.
_imageView.contentMode = UIViewContentModeScaleToFill;
} else {
_imageView.contentMode = (UIViewContentMode)resizeMode;
}
if ([self shouldReloadImageSourceAfterResize]) {
_needsReload = YES;
}
}
}
- (void)cancelImageLoad
{
RCTImageLoaderCancellationBlock previousCancellationBlock = _reloadImageCancellationBlock;
if (previousCancellationBlock) {
previousCancellationBlock();
_reloadImageCancellationBlock = nil;
}
_pendingImageSource = nil;
}
- (void)clearImage
{
[self cancelImageLoad];
self.image = nil;
_imageSource = nil;
}
- (void)clearImageIfDetached
{
if (!self.window) {
[self clearImage];
}
}
- (BOOL)hasMultipleSources
{
return _imageSources.count > 1;
}
- (RCTImageSource *)imageSourceForSize:(CGSize)size
{
if (![self hasMultipleSources]) {
return _imageSources.firstObject;
}
// Need to wait for layout pass before deciding.
if (CGSizeEqualToSize(size, CGSizeZero)) {
return nil;
}
const CGFloat scale = RCTScreenScale();
const CGFloat targetImagePixels = size.width * size.height * scale * scale;
RCTImageSource *bestSource = nil;
CGFloat bestFit = CGFLOAT_MAX;
for (RCTImageSource *source in _imageSources) {
CGSize imgSize = source.size;
const CGFloat imagePixels =
imgSize.width * imgSize.height * source.scale * source.scale;
const CGFloat fit = ABS(1 - (imagePixels / targetImagePixels));
if (fit < bestFit) {
bestFit = fit;
bestSource = source;
}
}
return bestSource;
}
- (BOOL)shouldReloadImageSourceAfterResize
{
// If capInsets are set, image doesn't need reloading when resized
return UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero);
}
- (BOOL)shouldChangeImageSource
{
// We need to reload if the desired image source is different from the current image
// source AND the image load that's pending
RCTImageSource *desiredImageSource = [self imageSourceForSize:self.frame.size];
return ![desiredImageSource isEqual:_imageSource] &&
![desiredImageSource isEqual:_pendingImageSource];
}
- (void)reloadImage
{
[self cancelImageLoad];
_needsReload = NO;
RCTImageSource *source = [self imageSourceForSize:self.frame.size];
_pendingImageSource = source;
if (source && self.frame.size.width > 0 && self.frame.size.height > 0) {
if (_onLoadStart) {
_onLoadStart(nil);
}
RCTImageLoaderProgressBlock progressHandler = nil;
if (_onProgress) {
progressHandler = ^(int64_t loaded, int64_t total) {
self->_onProgress(@{
@"loaded": @((double)loaded),
@"total": @((double)total),
});
};
}
__weak RCTImageView *weakSelf = self;
RCTImageLoaderPartialLoadBlock partialLoadHandler = ^(UIImage *image) {
[weakSelf imageLoaderLoadedImage:image error:nil forImageSource:source partial:YES];
};
CGSize imageSize = self.bounds.size;
CGFloat imageScale = RCTScreenScale();
if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero)) {
// Don't resize images that use capInsets
imageSize = CGSizeZero;
imageScale = source.scale;
}
RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *loadedImage) {
[weakSelf imageLoaderLoadedImage:loadedImage error:error forImageSource:source partial:NO];
};
id<RCTImageLoaderWithAttributionProtocol> imageLoader = [_bridge moduleForName:@"ImageLoader"
lazilyLoadIfNecessary:YES];
RCTImageURLLoaderRequest *loaderRequest = [imageLoader loadImageWithURLRequest:source.request
size:imageSize
scale:imageScale
clipped:NO
resizeMode:_resizeMode
priority:RCTImageLoaderPriorityImmediate
attribution:{
.nativeViewTag = [self.reactTag intValue],
.surfaceId = [self.rootTag intValue],
}
progressBlock:progressHandler
partialLoadBlock:partialLoadHandler
completionBlock:completionHandler];
_reloadImageCancellationBlock = loaderRequest.cancellationBlock;
} else {
[self clearImage];
}
}
- (void)imageLoaderLoadedImage:(UIImage *)loadedImage error:(NSError *)error forImageSource:(RCTImageSource *)source partial:(BOOL)isPartialLoad
{
if (![source isEqual:_pendingImageSource]) {
// Bail out if source has changed since we started loading
return;
}
if (error) {
RCTExecuteOnMainQueue(^{
self.image = nil;
});
if (_onError) {
_onError(@{ @"error": error.localizedDescription });
}
if (_onLoadEnd) {
_onLoadEnd(nil);
}
return;
}
void (^setImageBlock)(UIImage *) = ^(UIImage *image) {
if (!isPartialLoad) {
self->_imageSource = source;
self->_pendingImageSource = nil;
}
self.image = image;
if (isPartialLoad) {
if (self->_onPartialLoad) {
self->_onPartialLoad(nil);
}
} else {
if (self->_onLoad) {
RCTImageSource *sourceLoaded = [source imageSourceWithSize:image.size scale:source.scale];
self->_onLoad(onLoadParamsForSource(sourceLoaded));
}
if (self->_onLoadEnd) {
self->_onLoadEnd(nil);
}
}
};
if (_blurRadius > __FLT_EPSILON__) {
// Blur on a background thread to avoid blocking interaction
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *blurredImage = RCTBlurredImageWithRadius(loadedImage, self->_blurRadius);
RCTExecuteOnMainQueue(^{
setImageBlock(blurredImage);
});
});
} else {
// No blur, so try to set the image on the main thread synchronously to minimize image
// flashing. (For instance, if this view gets attached to a window, then -didMoveToWindow
// calls -reloadImage, and we want to set the image synchronously if possible so that the
// image property is set in the same CATransaction that attaches this view to the window.)
RCTExecuteOnMainQueue(^{
setImageBlock(loadedImage);
});
}
}
- (void)reactSetFrame:(CGRect)frame
{
[super reactSetFrame:frame];
// If we didn't load an image yet, or the new frame triggers a different image source
// to be loaded, reload to swap to the proper image source.
if ([self shouldChangeImageSource]) {
_targetSize = frame.size;
[self reloadImage];
} else if ([self shouldReloadImageSourceAfterResize]) {
CGSize imageSize = self.image.size;
CGFloat imageScale = self.image.scale;
CGSize idealSize = RCTTargetSize(imageSize, imageScale, frame.size, RCTScreenScale(),
(RCTResizeMode)self.contentMode, YES);
// Don't reload if the current image or target image size is close enough
if (!RCTShouldReloadImageForSizeChange(imageSize, idealSize) ||
!RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) {
return;
}
// Don't reload if the current image size is the maximum size of either the pending image source or image source
CGSize imageSourceSize = (_imageSource ? _imageSource : _pendingImageSource).size;
if (imageSize.width * imageScale == imageSourceSize.width * _imageSource.scale &&
imageSize.height * imageScale == imageSourceSize.height * _imageSource.scale) {
return;
}
RCTLogInfo(@"Reloading image %@ as size %@", _imageSource.request.URL.absoluteString, NSStringFromCGSize(idealSize));
// If the existing image or an image being loaded are not the right
// size, reload the asset in case there is a better size available.
_targetSize = idealSize;
[self reloadImage];
}
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
if (_needsReload) {
[self reloadImage];
}
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (!self.window) {
// Cancel loading the image if we've moved offscreen. In addition to helping
// prioritise image requests that are actually on-screen, this removes
// requests that have gotten "stuck" from the queue, unblocking other images
// from loading.
[self cancelImageLoad];
} else if ([self shouldChangeImageSource]) {
[self reloadImage];
}
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewManager.h>
@interface RCTImageViewManager : RCTViewManager
@end

View File

@ -0,0 +1,111 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageViewManager.h>
#import <UIKit/UIKit.h>
#import <React/RCTConvert.h>
#import <React/RCTImageSource.h>
#import <React/RCTImageShadowView.h>
#import <React/RCTImageView.h>
#import <React/RCTImageLoaderProtocol.h>
@implementation RCTImageViewManager
RCT_EXPORT_MODULE()
- (RCTShadowView *)shadowView
{
return [RCTImageShadowView new];
}
- (UIView *)view
{
return [[RCTImageView alloc] initWithBridge:self.bridge];
}
RCT_EXPORT_VIEW_PROPERTY(blurRadius, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets)
RCT_REMAP_VIEW_PROPERTY(defaultSource, defaultImage, UIImage)
RCT_EXPORT_VIEW_PROPERTY(onLoadStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPartialLoad, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
RCT_REMAP_VIEW_PROPERTY(source, imageSources, NSArray<RCTImageSource *>);
RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
{
// Default tintColor isn't nil - it's inherited from the superView - but we
// want to treat a null json value for `tintColor` as meaning 'disable tint',
// so we toggle `renderingMode` here instead of in `-[RCTImageView setTintColor:]`
view.tintColor = [RCTConvert UIColor:json] ?: defaultView.tintColor;
view.renderingMode = json ? UIImageRenderingModeAlwaysTemplate : defaultView.renderingMode;
}
RCT_EXPORT_METHOD(getSize:(NSURLRequest *)request
successBlock:(RCTResponseSenderBlock)successBlock
errorBlock:(RCTResponseErrorBlock)errorBlock)
{
[[self.bridge moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES]
getImageSizeForURLRequest:request
block:^(NSError *error, CGSize size) {
if (error) {
errorBlock(error);
} else {
successBlock(@[@(size.width), @(size.height)]);
}
}];
}
RCT_EXPORT_METHOD(getSizeWithHeaders:(RCTImageSource *)source
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
[[self.bridge moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES]
getImageSizeForURLRequest:source.request
block:^(NSError *error, CGSize size) {
if (error) {
reject(@"E_GET_SIZE_FAILURE", nil, error);
return;
}
resolve(@{@"width":@(size.width),@"height":@(size.height)});
}];
}
RCT_EXPORT_METHOD(prefetchImage:(NSURLRequest *)request
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
if (!request) {
reject(@"E_INVALID_URI", @"Cannot prefetch an image for an empty URI", nil);
return;
}
id<RCTImageLoaderProtocol> imageLoader = (id<RCTImageLoaderProtocol>)[self.bridge
moduleForName:@"ImageLoader" lazilyLoadIfNecessary:YES];
[imageLoader loadImageWithURLRequest:request
priority:RCTImageLoaderPriorityPrefetch
callback:^(NSError *error, UIImage *image) {
if (error) {
reject(@"E_PREFETCH_FAILURE", nil, error);
return;
}
resolve(@YES);
}];
}
RCT_EXPORT_METHOD(queryCache:(NSArray *)requests
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
resolve([[self.bridge moduleForName:@"ImageLoader"] getImageCacheStatus:requests]);
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageURLLoader.h>
@interface RCTLocalAssetImageLoader : NSObject <RCTImageURLLoader>
@end

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTLocalAssetImageLoader.h>
#import <atomic>
#import <memory>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTTurboModule.h>
#import "RCTImagePlugins.h"
@interface RCTLocalAssetImageLoader() <RCTTurboModule>
@end
@implementation RCTLocalAssetImageLoader
RCT_EXPORT_MODULE()
- (BOOL)canLoadImageURL:(NSURL *)requestURL
{
return RCTIsLocalAssetURL(requestURL);
}
- (BOOL)requiresScheduling
{
// Don't schedule this loader on the URL queue so we can load the
// local assets synchronously to avoid flickers.
return NO;
}
- (BOOL)shouldCacheLoadedImages
{
// UIImage imageNamed handles the caching automatically so we don't want
// to add it to the image cache.
return NO;
}
- (nullable RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
{
UIImage *image = RCTImageFromLocalAssetURL(imageURL);
if (image) {
if (progressHandler) {
progressHandler(1, 1);
}
completionHandler(nil, image);
} else {
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
RCTLogWarn(@"%@", message);
completionHandler(RCTErrorWithMessage(message), nil);
}
return nil;
}
@end
Class RCTLocalAssetImageLoaderCls(void) {
return RCTLocalAssetImageLoader.class;
}

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTConvert.h>
typedef NS_ENUM(NSInteger, RCTResizeMode) {
RCTResizeModeCover = UIViewContentModeScaleAspectFill,
RCTResizeModeContain = UIViewContentModeScaleAspectFit,
RCTResizeModeStretch = UIViewContentModeScaleToFill,
RCTResizeModeCenter = UIViewContentModeCenter,
RCTResizeModeRepeat = -1, // Use negative values to avoid conflicts with iOS enum values.
};
@interface RCTConvert(RCTResizeMode)
+ (RCTResizeMode)RCTResizeMode:(id)json;
@end

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTResizeMode.h>
@implementation RCTConvert(RCTResizeMode)
RCT_ENUM_CONVERTER(RCTResizeMode, (@{
@"cover": @(RCTResizeModeCover),
@"contain": @(RCTResizeModeContain),
@"stretch": @(RCTResizeModeStretch),
@"center": @(RCTResizeModeCenter),
@"repeat": @(RCTResizeModeRepeat),
}), RCTResizeModeStretch, integerValue)
@end

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTAnimatedImage.h>
#import <React/RCTDefines.h>
@interface RCTUIImageViewAnimated : UIImageView
@end

View File

@ -0,0 +1,335 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTUIImageViewAnimated.h>
#import <React/RCTDisplayWeakRefreshable.h>
#import <mach/mach.h>
#import <objc/runtime.h>
static NSUInteger RCTDeviceTotalMemory() {
return (NSUInteger)[[NSProcessInfo processInfo] physicalMemory];
}
static NSUInteger RCTDeviceFreeMemory() {
mach_port_t host_port = mach_host_self();
mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t);
vm_size_t page_size;
vm_statistics_data_t vm_stat;
kern_return_t kern;
kern = host_page_size(host_port, &page_size);
if (kern != KERN_SUCCESS) return 0;
kern = host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size);
if (kern != KERN_SUCCESS) return 0;
return (vm_stat.free_count - vm_stat.speculative_count) * page_size;
}
@interface RCTUIImageViewAnimated () <CALayerDelegate, RCTDisplayRefreshable>
@property (nonatomic, assign) NSUInteger maxBufferSize;
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
@property (nonatomic, assign) NSUInteger totalFrameCount;
@property (nonatomic, assign) NSUInteger totalLoopCount;
@property (nonatomic, strong) UIImage<RCTAnimatedImage> *animatedImage;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *frameBuffer;
@property (nonatomic, assign) NSTimeInterval currentTime;
@property (nonatomic, assign) BOOL bufferMiss;
@property (nonatomic, assign) NSUInteger maxBufferCount;
@property (nonatomic, strong) NSOperationQueue *fetchQueue;
@property (nonatomic, strong) dispatch_semaphore_t lock;
@property (nonatomic, assign) CGFloat animatedImageScale;
@property (nonatomic, strong) CADisplayLink *displayLink;
@end
@implementation RCTUIImageViewAnimated
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.lock = dispatch_semaphore_create(1);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
return self;
}
- (void)resetAnimatedImage
{
self.animatedImage = nil;
self.totalFrameCount = 0;
self.totalLoopCount = 0;
self.currentFrame = nil;
self.currentFrameIndex = 0;
self.currentLoopCount = 0;
self.currentTime = 0;
self.bufferMiss = NO;
self.maxBufferCount = 0;
self.animatedImageScale = 1;
[_fetchQueue cancelAllOperations];
_fetchQueue = nil;
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
[_frameBuffer removeAllObjects];
_frameBuffer = nil;
dispatch_semaphore_signal(self.lock);
}
- (void)setImage:(UIImage *)image
{
if (self.image == image) {
return;
}
[self stop];
[self resetAnimatedImage];
if ([image respondsToSelector:@selector(animatedImageFrameAtIndex:)]) {
NSUInteger animatedImageFrameCount = ((UIImage<RCTAnimatedImage> *)image).animatedImageFrameCount;
// In case frame count is 0, there is no reason to continue.
if (animatedImageFrameCount == 0) {
return;
}
self.animatedImage = (UIImage<RCTAnimatedImage> *)image;
self.totalFrameCount = animatedImageFrameCount;
// Get the current frame and loop count.
self.totalLoopCount = self.animatedImage.animatedImageLoopCount;
self.animatedImageScale = image.scale;
self.currentFrame = image;
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame;
dispatch_semaphore_signal(self.lock);
// Calculate max buffer size
[self calculateMaxBufferCount];
if ([self paused]) {
[self start];
}
[self.layer setNeedsDisplay];
} else {
super.image = image;
}
}
#pragma mark - Private
- (NSOperationQueue *)fetchQueue
{
if (!_fetchQueue) {
_fetchQueue = [[NSOperationQueue alloc] init];
_fetchQueue.maxConcurrentOperationCount = 1;
}
return _fetchQueue;
}
- (NSMutableDictionary<NSNumber *,UIImage *> *)frameBuffer
{
if (!_frameBuffer) {
_frameBuffer = [NSMutableDictionary dictionary];
}
return _frameBuffer;
}
- (CADisplayLink *)displayLink
{
// We only need a displayLink in the case of animated images, so short-circuit this code and don't create one for most of the use cases.
// Since this class is used for all RCTImageView's, this is especially important.
if (!_animatedImage) {
return nil;
}
if (!_displayLink) {
_displayLink = [RCTDisplayWeakRefreshable displayLinkWithWeakRefreshable:self];
NSString *runLoopMode = [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode;
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode];
}
return _displayLink;
}
#pragma mark - Animation
- (void)start
{
self.displayLink.paused = NO;
}
- (void)stop
{
self.displayLink.paused = YES;
}
- (BOOL)paused
{
return self.displayLink.isPaused;
}
- (void)displayDidRefresh:(CADisplayLink *)displayLink
{
#if TARGET_OS_UIKITFORMAC
// TODO: `displayLink.frameInterval` is not available on UIKitForMac
NSTimeInterval duration = displayLink.duration;
#else
NSTimeInterval duration = displayLink.duration * displayLink.frameInterval;
#endif
NSUInteger totalFrameCount = self.totalFrameCount;
NSUInteger currentFrameIndex = self.currentFrameIndex;
NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount;
// Check if we have the frame buffer firstly to improve performance
if (!self.bufferMiss) {
// Then check if timestamp is reached
self.currentTime += duration;
NSTimeInterval currentDuration = [self.animatedImage animatedImageDurationAtIndex:currentFrameIndex];
if (self.currentTime < currentDuration) {
// Current frame timestamp not reached, return
return;
}
self.currentTime -= currentDuration;
NSTimeInterval nextDuration = [self.animatedImage animatedImageDurationAtIndex:nextFrameIndex];
if (self.currentTime > nextDuration) {
// Do not skip frame
self.currentTime = nextDuration;
}
}
// Update the current frame
UIImage *currentFrame;
UIImage *fetchFrame;
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
currentFrame = self.frameBuffer[@(currentFrameIndex)];
fetchFrame = currentFrame ? self.frameBuffer[@(nextFrameIndex)] : nil;
dispatch_semaphore_signal(self.lock);
BOOL bufferFull = NO;
if (currentFrame) {
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
// Remove the frame buffer if need
if (self.frameBuffer.count > self.maxBufferCount) {
self.frameBuffer[@(currentFrameIndex)] = nil;
}
// Check whether we can stop fetch
if (self.frameBuffer.count == totalFrameCount) {
bufferFull = YES;
}
dispatch_semaphore_signal(self.lock);
self.currentFrame = currentFrame;
self.currentFrameIndex = nextFrameIndex;
self.bufferMiss = NO;
[self.layer setNeedsDisplay];
} else {
self.bufferMiss = YES;
}
// Update the loop count when last frame rendered
if (nextFrameIndex == 0 && !self.bufferMiss) {
// Update the loop count
self.currentLoopCount++;
// if reached the max loop count, stop animating, 0 means loop indefinitely
NSUInteger maxLoopCount = self.totalLoopCount;
if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) {
[self stop];
return;
}
}
// Check if we should prefetch next frame or current frame
NSUInteger fetchFrameIndex;
if (self.bufferMiss) {
// When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
fetchFrameIndex = currentFrameIndex;
} else {
// Or, most cases, the decode speed is faster than render speed, we fetch next frame
fetchFrameIndex = nextFrameIndex;
}
if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) {
// Prefetch next frame in background queue
UIImage<RCTAnimatedImage> *animatedImage = self.animatedImage;
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
UIImage *frame = [animatedImage animatedImageFrameAtIndex:fetchFrameIndex];
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
self.frameBuffer[@(fetchFrameIndex)] = frame;
dispatch_semaphore_signal(self.lock);
}];
[self.fetchQueue addOperation:operation];
}
}
#pragma mark - CALayerDelegate
- (void)displayLayer:(CALayer *)layer
{
if (_currentFrame) {
layer.contentsScale = self.animatedImageScale;
layer.contents = (__bridge id)_currentFrame.CGImage;
} else {
[super displayLayer:layer];
}
}
#pragma mark - Util
- (void)calculateMaxBufferCount
{
NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage);
if (bytes == 0) bytes = 1024;
NSUInteger max = 0;
if (self.maxBufferSize > 0) {
max = self.maxBufferSize;
} else {
// Calculate based on current memory, these factors are by experience
NSUInteger total = RCTDeviceTotalMemory();
NSUInteger free = RCTDeviceFreeMemory();
max = MIN(total * 0.2, free * 0.6);
}
NSUInteger maxBufferCount = (double)max / (double)bytes;
if (!maxBufferCount) {
// At least 1 frame
maxBufferCount = 1;
}
self.maxBufferCount = maxBufferCount;
}
#pragma mark - Lifecycle
- (void)dealloc
{
// Removes the display link from all run loop modes.
[_displayLink invalidate];
_displayLink = nil;
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification
{
[_fetchQueue cancelAllOperations];
[_fetchQueue addOperationWithBlock:^{
NSNumber *currentFrameIndex = @(self.currentFrameIndex);
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
NSArray *keys = self.frameBuffer.allKeys;
// only keep the next frame for later rendering
for (NSNumber * key in keys) {
if (![key isEqualToNumber:currentFrameIndex]) {
[self.frameBuffer removeObjectForKey:key];
}
}
dispatch_semaphore_signal(self.lock);
}];
}
@end

View File

@ -0,0 +1,49 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip
else
source[:tag] = "v#{version}"
end
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
folly_version = '2020.01.13.00'
Pod::Spec.new do |s|
s.name = "React-RCTImage"
s.version = version
s.summary = "A React component for displaying different types of images."
s.homepage = "https://reactnative.dev/"
s.documentation_url = "https://reactnative.dev/docs/image"
s.license = package["license"]
s.author = "Facebook, Inc. and its affiliates"
s.platforms = { :ios => "10.0", :tvos => "10.0" }
s.compiler_flags = folly_compiler_flags + ' -Wno-nullability-completeness'
s.source = source
s.source_files = "*.{m,mm}"
s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
s.header_dir = "RCTImage"
s.pod_target_xcconfig = {
"USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++14",
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Folly\""
}
s.dependency "Folly", folly_version
s.dependency "FBReactNativeSpec", version
s.dependency "RCTTypeSafety", version
s.dependency "ReactCommon/turbomodule/core", version
s.dependency "React-jsi", version
s.dependency "React-Core/RCTImageHeaders", version
s.dependency "React-RCTNetwork", version
end

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/
'use strict';
// This is a stub for flow to make it understand require('./icon.png')
// See metro/src/Bundler/index.js
const AssetRegistry = require('./AssetRegistry');
module.exports = (AssetRegistry.registerAsset({
__packager_asset: true,
fileSystemLocation: '/full/path/to/directory',
httpServerLocation: '/assets/full/path/to/directory',
width: 100,
height: 100,
scales: [1, 2, 3],
hash: 'nonsense',
name: 'icon',
type: 'png',
}): number);

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
const requireNativeComponent = require('../ReactNative/requireNativeComponent');
import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
const TextInlineImage: HostComponent<mixed> = requireNativeComponent<mixed>(
'RCTTextInlineImage',
);
module.exports = TextInlineImage;

View File

@ -0,0 +1,91 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/
'use strict';
import type {PackagerAsset} from './AssetRegistry';
const androidScaleSuffix = {
'0.75': 'ldpi',
'1': 'mdpi',
'1.5': 'hdpi',
'2': 'xhdpi',
'3': 'xxhdpi',
'4': 'xxxhdpi',
};
/**
* FIXME: using number to represent discrete scale numbers is fragile in essence because of
* floating point numbers imprecision.
*/
function getAndroidAssetSuffix(scale: number): string {
if (scale.toString() in androidScaleSuffix) {
return androidScaleSuffix[scale.toString()];
}
throw new Error('no such scale ' + scale.toString());
}
// See https://developer.android.com/guide/topics/resources/drawable-resource.html
const drawableFileTypes = new Set([
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
'xml',
]);
function getAndroidResourceFolderName(
asset: PackagerAsset,
scale: number,
): string | $TEMPORARY$string<'raw'> {
if (!drawableFileTypes.has(asset.type)) {
return 'raw';
}
var suffix = getAndroidAssetSuffix(scale);
if (!suffix) {
throw new Error(
"Don't know which android drawable suffix to use for scale: " +
scale +
'\nAsset: ' +
JSON.stringify(asset, null, '\t') +
'\nPossible scales are:' +
JSON.stringify(androidScaleSuffix, null, '\t'),
);
}
const androidFolder = 'drawable-' + suffix;
return androidFolder;
}
function getAndroidResourceIdentifier(asset: PackagerAsset): string {
var folderPath = getBasePath(asset);
return (folderPath + '/' + asset.name)
.toLowerCase()
.replace(/\//g, '_') // Encode folder structure in file name
.replace(/([^a-z0-9_])/g, '') // Remove illegal chars
.replace(/^assets_/, ''); // Remove "assets_" prefix
}
function getBasePath(asset: PackagerAsset): string {
var basePath = asset.httpServerLocation;
if (basePath[0] === '/') {
basePath = basePath.substr(1);
}
return basePath;
}
module.exports = {
getAndroidAssetSuffix: getAndroidAssetSuffix,
getAndroidResourceFolderName: getAndroidResourceFolderName,
getAndroidResourceIdentifier: getAndroidResourceIdentifier,
getBasePath: getBasePath,
};

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) Facebook, Inc. and its 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 Platform from '../Utilities/Platform';
import type {ImageURISource} from './ImageSource';
type NativeImageSourceSpec = $ReadOnly<{|
android?: string,
ios?: string,
default?: string,
// For more details on width and height, see
// https://reactnative.dev/docs/images.html#why-not-automatically-size-everything
height: number,
width: number,
|}>;
/**
* In hybrid apps, use `nativeImageSource` to access images that are already
* available on the native side, for example in Xcode Asset Catalogs or
* Android's drawable folder.
*
* However, keep in mind that React Native Packager does not guarantee that the
* image exists. If the image is missing you'll get an empty box. When adding
* new images your app needs to be recompiled.
*
* Prefer Static Image Resources system which provides more guarantees,
* automates measurements and allows adding new images without rebuilding the
* native app. For more details visit:
*
* https://reactnative.dev/docs/images.html
*
*/
function nativeImageSource(spec: NativeImageSourceSpec): ImageURISource {
let uri = Platform.select({
android: spec.android,
default: spec.default,
ios: spec.ios,
});
if (uri == null) {
console.warn(
'nativeImageSource(...): No image name supplied for `%s`:\n%s',
Platform.OS,
JSON.stringify(spec, null, 2),
);
uri = '';
}
return {
deprecated: true,
height: spec.height,
uri,
width: spec.width,
};
}
module.exports = nativeImageSource;

View File

@ -0,0 +1,109 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
// Resolves an asset into a `source` for `Image`.
'use strict';
const AssetRegistry = require('./AssetRegistry');
const AssetSourceResolver = require('./AssetSourceResolver');
import type {ResolvedAssetSource} from './AssetSourceResolver';
let _customSourceTransformer, _serverURL, _scriptURL;
let _sourceCodeScriptURL: ?string;
function getSourceCodeScriptURL(): ?string {
if (_sourceCodeScriptURL) {
return _sourceCodeScriptURL;
}
let sourceCode =
global.nativeExtensions && global.nativeExtensions.SourceCode;
if (!sourceCode) {
sourceCode = require('../NativeModules/specs/NativeSourceCode').default;
}
_sourceCodeScriptURL = sourceCode.getConstants().scriptURL;
return _sourceCodeScriptURL;
}
function getDevServerURL(): ?string {
if (_serverURL === undefined) {
const sourceCodeScriptURL = getSourceCodeScriptURL();
const match =
sourceCodeScriptURL && sourceCodeScriptURL.match(/^https?:\/\/.*?\//);
if (match) {
// jsBundle was loaded from network
_serverURL = match[0];
} else {
// jsBundle was loaded from file
_serverURL = null;
}
}
return _serverURL;
}
function _coerceLocalScriptURL(scriptURL: ?string): ?string {
if (scriptURL) {
if (scriptURL.startsWith('assets://')) {
// android: running from within assets, no offline path to use
return null;
}
scriptURL = scriptURL.substring(0, scriptURL.lastIndexOf('/') + 1);
if (!scriptURL.includes('://')) {
// Add file protocol in case we have an absolute file path and not a URL.
// This shouldn't really be necessary. scriptURL should be a URL.
scriptURL = 'file://' + scriptURL;
}
}
return scriptURL;
}
function getScriptURL(): ?string {
if (_scriptURL === undefined) {
_scriptURL = _coerceLocalScriptURL(getSourceCodeScriptURL());
}
return _scriptURL;
}
function setCustomSourceTransformer(
transformer: (resolver: AssetSourceResolver) => ResolvedAssetSource,
): void {
_customSourceTransformer = transformer;
}
/**
* `source` is either a number (opaque type returned by require('./foo.png'))
* or an `ImageSource` like { uri: '<http location || file path>' }
*/
function resolveAssetSource(source: any): ?ResolvedAssetSource {
if (typeof source === 'object') {
return source;
}
const asset = AssetRegistry.getAssetByID(source);
if (!asset) {
return null;
}
const resolver = new AssetSourceResolver(
getDevServerURL(),
getScriptURL(),
asset,
);
if (_customSourceTransformer) {
return _customSourceTransformer(resolver);
}
return resolver.defaultAsset();
}
module.exports = resolveAssetSource;
module.exports.pickScale = AssetSourceResolver.pickScale;
module.exports.setCustomSourceTransformer = setCustomSourceTransformer;