first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
export {
registerAsset,
getAssetByID,
} from '@react-native/assets-registry/registry';

View File

@@ -0,0 +1,226 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
export type ResolvedAssetSource = {
+__packager_asset: boolean,
+width: ?number,
+height: ?number,
+uri: string,
+scale: number,
};
// From @react-native/assets-registry
type AssetDestPathResolver = 'android' | 'generic';
// From @react-native/assets-registry
type PackagerAsset = $ReadOnly<{
__packager_asset: boolean,
fileSystemLocation: string,
httpServerLocation: string,
width: ?number,
height: ?number,
scales: Array<number>,
hash: string,
name: string,
type: string,
resolver?: AssetDestPathResolver,
...
}>;
const PixelRatio = require('../Utilities/PixelRatio').default;
const Platform = require('../Utilities/Platform').default;
const {pickScale} = require('./AssetUtils');
const {
getAndroidResourceFolderName,
getAndroidResourceIdentifier,
getBasePath,
} = require('@react-native/assets-registry/path-support');
const invariant = require('invariant');
/**
* Returns a path like 'assets/AwesomeModule/icon@2x.png'
*/
function getScaledAssetPath(asset: PackagerAsset): string {
const scale = pickScale(asset.scales, PixelRatio.get());
const scaleSuffix = scale === 1 ? '' : '@' + scale + 'x';
const assetDir = getBasePath(asset);
return assetDir + '/' + asset.name + scaleSuffix + '.' + asset.type;
}
/**
* Returns a path like 'drawable-mdpi/icon.png'
*/
function getAssetPathInDrawableFolder(asset: PackagerAsset): string {
const scale = pickScale(asset.scales, PixelRatio.get());
const drawableFolder = getAndroidResourceFolderName(asset, scale);
const fileName = getAndroidResourceIdentifier(asset);
return drawableFolder + '/' + fileName + '.' + asset.type;
}
/**
* Returns true if the asset can be loaded over the network.
*
* This prevents an issue loading XML assets on Android. XML asset types like
* vector drawables can only be loaded from precompiled source. Android does
* not support loading these over the network, and AAPT precompiles data by
* breaking path data and resource information apart into multiple files,
* stuffing it all into the resource table. As a result, we should only attempt
* to load resources as we would in release builds: by the resource name.
*
* For more information, see:
* https://issuetracker.google.com/issues/62435069
* https://issuetracker.google.com/issues/68293189
*/
function assetSupportsNetworkLoads(asset: PackagerAsset): boolean {
return !(asset.type === 'xml' && Platform.OS === 'android');
}
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 != null &&
this.serverUrl !== '' &&
assetSupportsNetworkLoads(this.asset)
);
}
isLoadedFromFileSystem(): boolean {
return this.jsbundleUrl != null && this.jsbundleUrl?.startsWith('file://');
}
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (this.asset.resolver != null) {
return this.getAssetUsingResolver(this.asset.resolver);
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetURLNearBundle();
}
}
getAssetUsingResolver(resolver: AssetDestPathResolver): ResolvedAssetSource {
switch (resolver) {
case 'android':
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
case 'generic':
return this.scaledAssetURLNearBundle();
default:
throw new Error(
"Don't know how to get asset via provided resolver: " +
resolver +
'\nAsset: ' +
JSON.stringify(this.asset, null, '\t') +
'\nPossible resolvers are:' +
JSON.stringify(['android', 'generic'], null, '\t'),
);
}
}
/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
assetServerURL(): ResolvedAssetSource {
invariant(this.serverUrl != null, '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(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: pickScale(this.asset.scales, PixelRatio.get()),
};
}
static pickScale: (scales: Array<number>, deviceScale?: number) => number =
pickScale;
}
export default AssetSourceResolver;

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import PixelRatio from '../Utilities/PixelRatio';
let cacheBreaker;
let warnIfCacheBreakerUnset = true;
export function pickScale(scales: Array<number>, deviceScale?: number): number {
const requiredDeviceScale = deviceScale ?? PixelRatio.get();
// Packager guarantees that `scales` array is sorted
for (let i = 0; i < scales.length; i++) {
if (scales[i] >= requiredDeviceScale) {
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;
}
export function setUrlCacheBreaker(appendage: string) {
cacheBreaker = appendage;
}
export function getUrlCacheBreaker(): string {
if (cacheBreaker == null) {
if (__DEV__ && warnIfCacheBreakerUnset) {
warnIfCacheBreakerUnset = false;
console.warn(
'AssetUtils.getUrlCacheBreaker: Cache breaker value is unset',
);
}
return '';
}
return cacheBreaker;
}

View File

@@ -0,0 +1,546 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../../src/private/types/HostInstance';
import type {ImageStyleProp} from '../StyleSheet/StyleSheet';
import type {RootTag} from '../Types/RootTagTypes';
import type {ImageProps} from './ImageProps';
import type {ImageSourceHeaders} from './ImageSourceUtils';
import type {AbstractImageAndroid, ImageAndroid} from './ImageTypes.flow';
import * as ReactNativeFeatureFlags from '../../src/private/featureflags/ReactNativeFeatureFlags';
import flattenStyle from '../StyleSheet/flattenStyle';
import StyleSheet from '../StyleSheet/StyleSheet';
import TextAncestorContext from '../Text/TextAncestorContext';
import ImageAnalyticsTagContext from './ImageAnalyticsTagContext';
import {
unstable_getImageComponentDecorator,
useWrapRefWithImageAttachedCallbacks,
} from './ImageInjection';
import {getImageSourcesFromImageProps} from './ImageSourceUtils';
import {convertObjectFitToResizeMode} from './ImageUtils';
import ImageViewNativeComponent from './ImageViewNativeComponent';
import NativeImageLoaderAndroid, {
type ImageSize,
} from './NativeImageLoaderAndroid';
import resolveAssetSource from './resolveAssetSource';
import TextInlineImageNativeComponent from './TextInlineImageNativeComponent';
import * as React from 'react';
import {use} from 'react';
let _requestId = 1;
function generateRequestId() {
return _requestId++;
}
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
*
* See https://reactnative.dev/docs/image#getsize
*/
function getSize(
url: string,
success?: (width: number, height: number) => void,
failure?: (error: mixed) => void,
): void | Promise<ImageSize> {
const promise = NativeImageLoaderAndroid.getSize(url);
if (typeof success !== 'function') {
return promise;
}
promise
.then(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#getsizewithheaders
*/
function getSizeWithHeaders(
url: string,
headers: {[string]: string, ...},
success?: (width: number, height: number) => void,
failure?: (error: mixed) => void,
): void | Promise<ImageSize> {
const promise = NativeImageLoaderAndroid.getSizeWithHeaders(url, headers);
if (typeof success !== 'function') {
return promise;
}
promise
.then(sizes => success(sizes.width, sizes.height))
.catch(
failure ||
function () {
console.warn('Failed to get size for image: ' + url);
},
);
}
function prefetchWithMetadata(
url: string,
queryRootName: string,
rootTag?: ?RootTag,
callback: ?(requestId: number) => void,
): Promise<boolean> {
// TODO: T79192300 Log queryRootName and rootTag
return prefetch(url, callback);
}
function prefetch(
url: string,
callback: ?(requestId: number) => void,
): Promise<boolean> {
const requestId = generateRequestId();
callback && callback(requestId);
return NativeImageLoaderAndroid.prefetchImage(url, requestId);
}
function abortPrefetch(requestId: number): void {
NativeImageLoaderAndroid.abortRequest(requestId);
}
/**
* Perform cache interrogation.
*
* See https://reactnative.dev/docs/image#querycache
*/
async function queryCache(
urls: Array<string>,
): Promise<{[string]: 'memory' | 'disk' | 'disk/memory', ...}> {
return NativeImageLoaderAndroid.queryCache(urls);
}
const EMPTY_IMAGE_SOURCE = {
uri: undefined,
width: undefined,
height: undefined,
};
/**
* 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
*/
let _BaseImage;
if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) {
let BaseImage: AbstractImageAndroid = ({
ref: forwardedRef,
alt,
accessible,
'aria-labelledby': ariaLabelledBy,
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-hidden': ariaHidden,
'aria-label': ariaLabel,
'aria-selected': ariaSelected,
accessibilityLabel,
accessibilityLabelledBy,
accessibilityState,
defaultSource,
loadingIndicatorSource,
children,
source,
src,
style,
crossOrigin,
referrerPolicy,
srcSet,
onLoadStart,
onLoad,
onLoadEnd,
onError,
width,
height,
resizeMode,
...restProps
}: {
ref?: React.RefSetter<HostInstance>,
...ImageProps,
}) => {
let source_ =
getImageSourcesFromImageProps({
crossOrigin,
referrerPolicy,
src,
srcSet,
width,
height,
source,
}) || EMPTY_IMAGE_SOURCE;
const defaultSource_ = resolveAssetSource(defaultSource);
const loadingIndicatorSource_ = resolveAssetSource(loadingIndicatorSource);
if (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.',
);
}
if (defaultSource != null && loadingIndicatorSource != null) {
throw new Error(
'The <Image> component cannot have defaultSource and loadingIndicatorSource at the same time. Please use either defaultSource or loadingIndicatorSource.',
);
}
let style_: ImageStyleProp;
let sources_;
let headers_: ?ImageSourceHeaders;
if (Array.isArray(source_)) {
style_ = [styles.base, style];
sources_ = source_;
headers_ = sources_[0].headers;
} else {
const {uri} = source_;
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
const width_ = source_.width ?? width;
const height_ = source_.height ?? height;
style_ = [{width: width_, height: height_}, styles.base, style];
sources_ = [source_];
}
const nativeProps = restProps as {
...React.PropsOf<ImageViewNativeComponent>,
};
// Both iOS and C++ sides expect to have "source" prop, whereas on Android it's "src"
// (for historical reasons). So in the latter case we populate both "src" and "source",
// in order to have a better alignment between platforms in the future.
// TODO: `src` should be eventually removed from the API on Android.
nativeProps.src = sources_;
nativeProps.source = sources_;
nativeProps.style = style_;
if (headers_ != null) {
nativeProps.headers = headers_;
}
if (onLoadStart != null) {
nativeProps.shouldNotifyLoadEvents = true;
nativeProps.onLoadStart = onLoadStart;
}
if (onLoad != null) {
nativeProps.shouldNotifyLoadEvents = true;
nativeProps.onLoad = onLoad;
}
if (onLoadEnd != null) {
nativeProps.shouldNotifyLoadEvents = true;
nativeProps.onLoadEnd = onLoadEnd;
}
if (onError != null) {
nativeProps.shouldNotifyLoadEvents = true;
nativeProps.onError = onError;
}
if (defaultSource_ != null && defaultSource_.uri != null) {
nativeProps.defaultSource = defaultSource_.uri;
}
if (
loadingIndicatorSource_ != null &&
loadingIndicatorSource_.uri != null
) {
nativeProps.loadingIndicatorSrc = loadingIndicatorSource_.uri;
}
if (ariaLabel != null) {
nativeProps.accessibilityLabel = ariaLabel;
} else if (accessibilityLabel != null) {
nativeProps.accessibilityLabel = accessibilityLabel;
} else if (alt != null) {
nativeProps.accessibilityLabel = alt;
}
if (ariaLabelledBy != null) {
nativeProps.accessibilityLabelledBy = ariaLabelledBy;
} else if (accessibilityLabelledBy != null) {
nativeProps.accessibilityLabelledBy = accessibilityLabelledBy;
}
if (alt != null) {
nativeProps.accessible = true;
} else if (accessible != null) {
nativeProps.accessible = accessible;
}
if (
accessibilityState != null ||
ariaBusy != null ||
ariaChecked != null ||
ariaDisabled != null ||
ariaExpanded != null ||
ariaSelected != null
) {
nativeProps.accessibilityState = {
busy: ariaBusy ?? accessibilityState?.busy,
checked: ariaChecked ?? accessibilityState?.checked,
disabled: ariaDisabled ?? accessibilityState?.disabled,
expanded: ariaExpanded ?? accessibilityState?.expanded,
selected: ariaSelected ?? accessibilityState?.selected,
};
}
if (ariaHidden === true) {
nativeProps.importantForAccessibility = 'no-hide-descendants';
}
const flattenedStyle_ = flattenStyle<ImageStyleProp>(style);
const objectFit_ = convertObjectFitToResizeMode(flattenedStyle_?.objectFit);
const resizeMode_ =
objectFit_ || resizeMode || flattenedStyle_?.resizeMode || 'cover';
nativeProps.resizeMode = resizeMode_;
const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);
const hasTextAncestor = use(TextAncestorContext);
const analyticTag = use(ImageAnalyticsTagContext);
if (analyticTag !== null) {
nativeProps.internal_analyticTag = analyticTag;
}
return hasTextAncestor ? (
<TextInlineImageNativeComponent
// $FlowFixMe[incompatible-type]
style={style_}
resizeMode={resizeMode_}
headers={headers_}
src={sources_}
ref={actualRef}
/>
) : (
<ImageViewNativeComponent {...nativeProps} ref={actualRef} />
);
};
_BaseImage = BaseImage;
} else {
let BaseImage: AbstractImageAndroid = ({
ref: forwardedRef,
...props
}: {
ref?: React.RefSetter<HostInstance>,
...ImageProps,
}) => {
let source = getImageSourcesFromImageProps(props) || {
uri: undefined,
width: undefined,
height: undefined,
};
const defaultSource = resolveAssetSource(props.defaultSource);
const loadingIndicatorSource = resolveAssetSource(
props.loadingIndicatorSource,
);
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.',
);
}
if (props.defaultSource != null && props.loadingIndicatorSource != null) {
throw new Error(
'The <Image> component cannot have defaultSource and loadingIndicatorSource at the same time. Please use either defaultSource or loadingIndicatorSource.',
);
}
let style: ImageStyleProp;
let sources;
if (Array.isArray(source)) {
style = [styles.base, props.style];
sources = source;
} else {
const {uri} = source;
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
const width = source.width ?? props.width;
const height = source.height ?? props.height;
style = [{width, height}, styles.base, props.style];
sources = [source];
}
const {onLoadStart, onLoad, onLoadEnd, onError} = props;
const nativeProps = {
...props,
style,
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError),
// Both iOS and C++ sides expect to have "source" prop, whereas on Android it's "src"
// (for historical reasons). So in the latter case we populate both "src" and "source",
// in order to have a better alignment between platforms in the future.
src: sources,
source: sources,
/* $FlowFixMe[prop-missing](>=0.78.0 site=react_native_android_fb) This issue was found
* when making Flow check .android.js files. */
headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}),
defaultSource: defaultSource ? defaultSource.uri : null,
loadingIndicatorSrc: loadingIndicatorSource
? loadingIndicatorSource.uri
: null,
accessibilityLabel:
props['aria-label'] ?? props.accessibilityLabel ?? props.alt,
accessibilityLabelledBy:
props?.['aria-labelledby'] ?? props?.accessibilityLabelledBy,
accessible: props.alt !== undefined ? true : props.accessible,
accessibilityState: {
busy: props['aria-busy'] ?? props.accessibilityState?.busy,
checked: props['aria-checked'] ?? props.accessibilityState?.checked,
disabled: props['aria-disabled'] ?? props.accessibilityState?.disabled,
expanded: props['aria-expanded'] ?? props.accessibilityState?.expanded,
selected: props['aria-selected'] ?? props.accessibilityState?.selected,
},
importantForAccessibility:
props['aria-hidden'] === true
? ('no-hide-descendants' as const)
: props.importantForAccessibility,
};
const flattenedStyle = flattenStyle<ImageStyleProp>(style);
const objectFit = convertObjectFitToResizeMode(flattenedStyle?.objectFit);
const resizeMode =
objectFit || props.resizeMode || flattenedStyle?.resizeMode || 'cover';
const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);
return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
const nativePropsWithAnalytics =
analyticTag !== null
? {
...nativeProps,
internal_analyticTag: analyticTag,
}
: nativeProps;
return (
<TextAncestorContext.Consumer>
{hasTextAncestor => {
if (hasTextAncestor) {
return (
<TextInlineImageNativeComponent
// $FlowFixMe[incompatible-type]
style={style}
resizeMode={resizeMode}
headers={nativeProps.headers}
src={sources}
ref={actualRef}
/>
);
}
return (
<ImageViewNativeComponent
{...nativePropsWithAnalytics}
resizeMode={resizeMode}
ref={actualRef}
/>
);
}}
</TextAncestorContext.Consumer>
);
}}
</ImageAnalyticsTagContext.Consumer>
);
};
_BaseImage = BaseImage;
}
const imageComponentDecorator = unstable_getImageComponentDecorator();
if (imageComponentDecorator != null) {
_BaseImage = imageComponentDecorator(_BaseImage);
}
// $FlowExpectedError[incompatible-type] Eventually we need to move these functions from statics of the component to exports in the module.
const Image: ImageAndroid = _BaseImage;
Image.displayName = 'Image';
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
*
* See https://reactnative.dev/docs/image#getsize
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
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#getsizewithheaders
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.getSizeWithHeaders = getSizeWithHeaders;
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache
*
* See https://reactnative.dev/docs/image#prefetch
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.prefetch = prefetch;
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache, and adds metadata for queryRootName and rootTag.
*
* See https://reactnative.dev/docs/image#prefetch
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.prefetchWithMetadata = prefetchWithMetadata;
/**
* Abort prefetch request.
*
* See https://reactnative.dev/docs/image#abortprefetch
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.abortPrefetch = abortPrefetch;
/**
* Perform cache interrogation.
*
* See https://reactnative.dev/docs/image#querycache
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.queryCache = queryCache;
/**
* Resolves an asset reference into an object.
*
* See https://reactnative.dev/docs/image#resolveassetsource
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.resolveAssetSource = resolveAssetSource;
const styles = StyleSheet.create({
base: {
overflow: 'hidden',
},
});
export default Image;

389
node_modules/react-native/Libraries/Image/Image.d.ts generated vendored Normal file
View File

@@ -0,0 +1,389 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import * as React from 'react';
import {Constructor} from '../../types/private/Utilities';
import {AccessibilityProps} from '../Components/View/ViewAccessibility';
import {Insets} from '../../types/public/Insets';
import {HostInstance} from '../../types/public/ReactNativeTypes';
import {ColorValue, StyleProp} from '../StyleSheet/StyleSheet';
import {ImageStyle, ViewStyle} from '../StyleSheet/StyleSheetTypes';
import {LayoutChangeEvent, NativeSyntheticEvent} from '../Types/CoreEventTypes';
import {ImageResizeMode} from './ImageResizeMode';
import {ImageRequireSource, ImageURISource} from './ImageSource';
/**
* @deprecated Use `ImageProgressEventIOS` instead.
*/
export interface ImageProgressEventDataIOS {
loaded: number;
total: number;
}
/**
* @see https://reactnative.dev/docs/image#onprogress
*/
export type ImageProgressEventIOS =
NativeSyntheticEvent<ImageProgressEventDataIOS>;
export interface ImagePropsIOS {
/**
* blurRadius: the blur radius of the blur filter added to the image
* @platform ios
*/
blurRadius?: number | undefined;
/**
* When the image is resized, the corners of the size specified by capInsets will stay a fixed size,
* but the center content and borders of the image will be stretched.
* This is useful for creating resizable rounded buttons, shadows, and other resizable assets.
* More info on Apple documentation
*/
capInsets?: Insets | undefined;
/**
* Invoked on download progress with {nativeEvent: {loaded, total}}
*/
onProgress?: ((event: ImageProgressEventIOS) => void) | undefined;
/**
* Invoked when a partial load of the image is complete. The definition of
* what constitutes a "partial load" is loader specific though this is meant
* for progressive JPEG loads.
* @platform ios
*/
onPartialLoad?: (() => void) | undefined;
}
interface ImagePropsAndroid {
/**
* 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`.
*
* - `auto`: Use heuristics to pick between `resize` and `scale`.
*
* - `resize`: A software operation which changes the encoded image in memory before it
* gets decoded. This should be used instead of `scale` when the image is much larger
* than the view.
*
* - `scale`: The image gets drawn downscaled or upscaled. Compared to `resize`, `scale` is
* faster (usually hardware accelerated) and produces higher quality images. This
* should be used if the image is smaller than the view. It should also be used if the
* image is slightly bigger than the view.
*
* - `none`: No sampling is performed and the image is displayed in its full resolution. This
* should only be used in rare circumstances because it is considered unsafe as Android will
* throw a runtime exception when trying to render images that consume too much memory.
*
* More details about `resize` and `scale` can be found at http://frescolib.org/docs/resizing-rotating.html.
*
* @platform android
*/
resizeMethod?: 'auto' | 'resize' | 'scale' | 'none' | undefined;
/**
* Duration of fade in animation in ms. Defaults to 300
*
* @platform android
*/
fadeDuration?: number | undefined;
}
/**
* @see https://reactnative.dev/docs/image#source
*/
export type ImageSourcePropType =
| ImageURISource
| ImageURISource[]
| ImageRequireSource;
/**
* @deprecated Use `ImageLoadEvent` instead.
*/
export interface ImageLoadEventData {
source: {
height: number;
width: number;
uri: string;
};
}
/**
* @see https://reactnative.dev/docs/image#onload
*/
export type ImageLoadEvent = NativeSyntheticEvent<ImageLoadEventData>;
/**
* @deprecated Use `ImageErrorEvent` instead.
*/
export interface ImageErrorEventData {
error: any;
}
/**
* @see https://reactnative.dev/docs/image#onerror
*/
export type ImageErrorEvent = NativeSyntheticEvent<ImageErrorEventData>;
/**
* @see https://reactnative.dev/docs/image#resolveassetsource
*/
export interface ImageResolvedAssetSource {
height: number;
width: number;
scale: number;
uri: string;
}
/**
* @see https://reactnative.dev/docs/image
*/
export interface ImagePropsBase
extends ImagePropsIOS,
ImagePropsAndroid,
AccessibilityProps {
/**
* Used to reference react managed images from native code.
*/
id?: string | undefined;
/**
* onLayout function
*
* Invoked on mount and layout changes with
*
* {nativeEvent: { layout: {x, y, width, height} }}.
*/
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
/**
* Invoked on load error with {nativeEvent: {error}}
*/
onError?: ((error: ImageErrorEvent) => void) | undefined;
/**
* Invoked when load completes successfully
* { source: { uri, height, width } }.
*/
onLoad?: ((event: ImageLoadEvent) => void) | undefined;
/**
* Invoked when load either succeeds or fails
*/
onLoadEnd?: (() => void) | undefined;
/**
* Invoked on load start
*/
onLoadStart?: (() => void) | undefined;
progressiveRenderingEnabled?: boolean | undefined;
borderRadius?: number | undefined;
borderTopLeftRadius?: number | undefined;
borderTopRightRadius?: number | undefined;
borderBottomLeftRadius?: number | undefined;
borderBottomRightRadius?: number | undefined;
/**
* Determines how to resize the image when the frame doesn't match the raw
* image dimensions.
*
* 'cover': Scale the image uniformly (maintain the image's aspect ratio)
* so that both dimensions (width and height) of the image will be equal
* to or larger than the corresponding dimension of the view (minus padding).
*
* 'contain': Scale the image uniformly (maintain the image's aspect ratio)
* so that both dimensions (width and height) of the image will be equal to
* or less than the corresponding dimension of the view (minus padding).
*
* 'stretch': Scale width and height independently, This may change the
* aspect ratio of the src.
*
* 'repeat': Repeat the image to cover the frame of the view.
* The image will keep it's size and aspect ratio. (iOS only)
*
* 'center': Scale the image down so that it is completely visible,
* if bigger than the area of the view.
* The image will not be scaled up.
*
* 'none': Do not resize the image. The image will be displayed at its intrinsic size.
*/
resizeMode?: ImageResizeMode | undefined;
/**
* The image source (either a remote URL or a local file resource).
*
* This prop can also contain several remote URLs, specified together with their width and height and potentially with scale/other URI arguments.
* The native side will then choose the best uri to display based on the measured size of the image container.
* A cache property can be added to control how networked request interacts with the local cache.
*
* The currently supported formats are png, jpg, jpeg, bmp, gif, webp (Android only), psd (iOS only).
*/
source?: ImageSourcePropType | undefined;
/**
* A string representing the resource identifier for the image. Similar to
* src from HTML.
*
* See https://reactnative.dev/docs/image#src
*/
src?: string | undefined;
/**
* Similar to srcset from HTML.
*
* See https://reactnative.dev/docs/image#srcset
*/
srcSet?: string | undefined;
/**
* similarly to `source`, this property represents the resource used to render
* the loading indicator for the image, displayed until image is ready to be
* displayed, typically after when it got downloaded from network.
*/
loadingIndicatorSource?: ImageURISource | undefined;
/**
* A unique identifier for this element to be used in UI Automation testing scripts.
*/
testID?: string | undefined;
/**
* Used to reference react managed images from native code.
*/
nativeID?: string | undefined;
/**
* A static image to display while downloading the final image off the network.
*/
defaultSource?: ImageURISource | ImageRequireSource | undefined;
/**
* The text that's read by the screen reader when the user interacts with
* the image.
*
* See https://reactnative.dev/docs/image#alt
*/
alt?: string | undefined;
/**
* Height of the image component.
*
* See https://reactnative.dev/docs/image#height
*/
height?: number | undefined;
/**
* Width of the image component.
*
* See https://reactnative.dev/docs/image#width
*/
width?: number | undefined;
/**
* Adds the CORS related header to the request.
* Similar to crossorigin from HTML.
*
* See https://reactnative.dev/docs/image#crossorigin
*/
crossOrigin?: 'anonymous' | 'use-credentials' | undefined;
/**
* Changes the color of all the non-transparent pixels to the tintColor.
*
* See https://reactnative.dev/docs/image#tintcolor
*/
tintColor?: ColorValue | undefined;
/**
* A string indicating which referrer to use when fetching the resource.
* Similar to referrerpolicy from HTML.
*
* See https://reactnative.dev/docs/image#referrerpolicy
*/
referrerPolicy?:
| 'no-referrer'
| 'no-referrer-when-downgrade'
| 'origin'
| 'origin-when-cross-origin'
| 'same-origin'
| 'strict-origin'
| 'strict-origin-when-cross-origin'
| 'unsafe-url'
| undefined;
}
export interface ImageProps extends ImagePropsBase {
/**
*
* Style
*/
style?: StyleProp<ImageStyle> | undefined;
}
export interface ImageSize {
width: number;
height: number;
}
declare class ImageComponent extends React.Component<ImageProps> {}
declare const ImageBase: Constructor<HostInstance> & typeof ImageComponent;
export class Image extends ImageBase {
static getSize(uri: string): Promise<ImageSize>;
static getSize(
uri: string,
success: (width: number, height: number) => void,
failure?: (error: any) => void,
): void;
static getSizeWithHeaders(
uri: string,
headers: {[index: string]: string},
): Promise<ImageSize>;
static getSizeWithHeaders(
uri: string,
headers: {[index: string]: string},
success: (width: number, height: number) => void,
failure?: (error: any) => void,
): void;
static prefetch(url: string): Promise<boolean>;
static prefetchWithMetadata(
url: string,
queryRootName: string,
rootTag?: number,
): Promise<boolean>;
static abortPrefetch?(requestId: number): void;
static queryCache?(
urls: string[],
): Promise<{[url: string]: 'memory' | 'disk' | 'disk/memory'}>;
/**
* @see https://reactnative.dev/docs/image#resolveassetsource
*/
static resolveAssetSource(
source: ImageSourcePropType,
): ImageResolvedAssetSource;
}
export interface ImageBackgroundProps extends ImagePropsBase {
children?: React.ReactNode | undefined;
imageStyle?: StyleProp<ImageStyle> | undefined;
style?: StyleProp<ViewStyle> | undefined;
imageRef?(image: Image): void;
}
declare class ImageBackgroundComponent extends React.Component<ImageBackgroundProps> {}
declare const ImageBackgroundBase: Constructor<HostInstance> &
typeof ImageBackgroundComponent;
export class ImageBackground extends ImageBackgroundBase {}

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

@@ -0,0 +1,265 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../../src/private/types/HostInstance';
import type {ImageStyleProp} from '../StyleSheet/StyleSheet';
import type {RootTag} from '../Types/RootTagTypes';
import type {ImageProps} from './ImageProps';
import type {AbstractImageIOS, ImageIOS} from './ImageTypes.flow';
import type {ImageSize} from './NativeImageLoaderAndroid';
import {createRootTag} from '../ReactNative/RootTag';
import flattenStyle from '../StyleSheet/flattenStyle';
import StyleSheet from '../StyleSheet/StyleSheet';
import ImageAnalyticsTagContext from './ImageAnalyticsTagContext';
import {
unstable_getImageComponentDecorator,
useWrapRefWithImageAttachedCallbacks,
} from './ImageInjection';
import {getImageSourcesFromImageProps} from './ImageSourceUtils';
import {convertObjectFitToResizeMode} from './ImageUtils';
import ImageViewNativeComponent from './ImageViewNativeComponent';
import NativeImageLoaderIOS from './NativeImageLoaderIOS';
import resolveAssetSource from './resolveAssetSource';
import * as React from 'react';
function getSize(
uri: string,
success?: (width: number, height: number) => void,
failure?: (error: mixed) => void,
): void | Promise<ImageSize> {
const promise = NativeImageLoaderIOS.getSize(uri).then(([width, height]) => ({
width,
height,
}));
if (typeof success !== 'function') {
return promise;
}
promise
.then(sizes => success(sizes.width, sizes.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: mixed) => void,
): void | Promise<ImageSize> {
const promise = NativeImageLoaderIOS.getSizeWithHeaders(uri, headers);
if (typeof success !== 'function') {
return promise;
}
promise
.then(sizes => success(sizes.width, sizes.height))
.catch(
failure ||
function () {
console.warn('Failed to get size for image: ' + uri);
},
);
}
function prefetchWithMetadata(
url: string,
queryRootName: string,
rootTag?: ?RootTag,
): Promise<boolean> {
if (NativeImageLoaderIOS.prefetchImageWithMetadata) {
// number params like rootTag cannot be nullable before TurboModules is available
return NativeImageLoaderIOS.prefetchImageWithMetadata(
url,
queryRootName,
// NOTE: RootTag type
rootTag != null ? rootTag : createRootTag(0),
);
} else {
return NativeImageLoaderIOS.prefetchImage(url);
}
}
function prefetch(url: string): Promise<boolean> {
return NativeImageLoaderIOS.prefetchImage(url);
}
async function queryCache(
urls: Array<string>,
): Promise<{[string]: 'memory' | 'disk' | 'disk/memory', ...}> {
return NativeImageLoaderIOS.queryCache(urls);
}
/**
* 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
*/
let BaseImage: AbstractImageIOS = ({
ref: forwardedRef,
...props
}: {
ref?: React.RefSetter<HostInstance>,
...ImageProps,
}) => {
const source = getImageSourcesFromImageProps(props) || {
uri: undefined,
width: undefined,
height: undefined,
};
let style: ImageStyleProp;
let sources;
if (Array.isArray(source)) {
style = [styles.base, props.style];
sources = source;
} else {
const {uri} = source;
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
const width = source.width ?? props.width;
const height = source.height ?? props.height;
style = [{width, height}, styles.base, props.style];
sources = [source];
}
const flattenedStyle = flattenStyle<ImageStyleProp>(style);
const objectFit = convertObjectFitToResizeMode(flattenedStyle?.objectFit);
const resizeMode =
objectFit || props.resizeMode || flattenedStyle?.resizeMode || 'cover';
const tintColor = props.tintColor ?? flattenedStyle?.tintColor;
if (props.children != null) {
throw new Error(
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
);
}
const {
'aria-busy': ariaBusy,
'aria-checked': ariaChecked,
'aria-disabled': ariaDisabled,
'aria-expanded': ariaExpanded,
'aria-selected': ariaSelected,
'aria-hidden': ariaHidden,
src,
...restProps
} = props;
const _accessibilityState = {
busy: ariaBusy ?? props.accessibilityState?.busy,
checked: ariaChecked ?? props.accessibilityState?.checked,
disabled: ariaDisabled ?? props.accessibilityState?.disabled,
expanded: ariaExpanded ?? props.accessibilityState?.expanded,
selected: ariaSelected ?? props.accessibilityState?.selected,
};
// In order for `aria-hidden` to work on iOS we must set `accessible` to false (`accessibilityElementsHidden` is not enough).
const accessible =
ariaHidden !== true && (props.alt !== undefined ? true : props.accessible);
const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel;
const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);
return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
return (
<ImageViewNativeComponent
accessibilityState={_accessibilityState}
{...restProps}
accessible={accessible}
accessibilityLabel={accessibilityLabel ?? props.alt}
ref={actualRef}
style={style}
resizeMode={resizeMode}
tintColor={tintColor}
source={sources}
internal_analyticTag={analyticTag}
/>
);
}}
</ImageAnalyticsTagContext.Consumer>
);
};
const imageComponentDecorator = unstable_getImageComponentDecorator();
if (imageComponentDecorator != null) {
BaseImage = imageComponentDecorator(BaseImage);
}
// $FlowExpectedError[incompatible-type] Eventually we need to move these functions from statics of the component to exports in the module.
const Image: ImageIOS = BaseImage;
Image.displayName = 'Image';
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it.
*
* See https://reactnative.dev/docs/image#getsize
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
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#getsizewithheaders
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.getSizeWithHeaders = getSizeWithHeaders;
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache.
*
* See https://reactnative.dev/docs/image#prefetch
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.prefetch = prefetch;
/**
* Prefetches a remote image for later use by downloading it to the disk
* cache, and adds metadata for queryRootName and rootTag.
*
* See https://reactnative.dev/docs/image#prefetch
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.prefetchWithMetadata = prefetchWithMetadata;
/**
* Performs cache interrogation.
*
* See https://reactnative.dev/docs/image#querycache
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.queryCache = queryCache;
/**
* Resolves an asset reference into an object.
*
* See https://reactnative.dev/docs/image#resolveassetsource
*/
// $FlowFixMe[incompatible-use] This property isn't writable but we're actually defining it here for the first time.
Image.resolveAssetSource = resolveAssetSource;
const styles = StyleSheet.create({
base: {
overflow: 'hidden',
},
});
export default Image;

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

@@ -0,0 +1,17 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// NOTE: This file supports backwards compatibility of subpath (deep) imports
// from 'react-native' with platform-specific extensions. It can be deleted
// once we remove the "./*" mapping from package.json "exports".
import Image from './Image';
export default Image;

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {ImageType} from './ImageTypes.flow';
export type {
ImageProgressEventIOS,
ImagePropsIOS,
ImagePropsAndroid,
ImageSourcePropType,
ImageLoadEvent,
ImageErrorEvent,
ImagePropsBase,
ImageProps,
ImageBackgroundProps,
} from './ImageProps';
export type {ImageResolvedAssetSource, ImageSize} from './ImageTypes.flow';
declare export default ImageType;

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import * as React from 'react';
import {createContext} from 'react';
type ContextType = ?string;
const Context: React.Context<ContextType> = createContext<ContextType>(null);
if (__DEV__) {
Context.displayName = 'ImageAnalyticsTagContext';
}
export default Context;

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../../src/private/types/HostInstance';
import type {ImageBackgroundProps} from './ImageProps';
import View from '../Components/View/View';
import flattenStyle from '../StyleSheet/flattenStyle';
import StyleSheet from '../StyleSheet/StyleSheet';
import Image from './Image';
import * as React from 'react';
export type {ImageBackgroundProps} from './ImageProps';
/**
* 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<ImageBackgroundProps> {
setNativeProps(props: {...}) {
// Work-around flow
const viewRef = this._viewRef;
if (viewRef) {
viewRef.setNativeProps(props);
}
}
_viewRef: ?React.ElementRef<typeof View> = null;
_captureRef = (ref: null | HostInstance) => {
this._viewRef = ref;
};
render(): React.Node {
const {
children,
style,
imageStyle,
imageRef,
importantForAccessibility,
...props
} = this.props;
// $FlowFixMe[underconstrained-implicit-instantiation]
const flattenedStyle = flattenStyle(style);
return (
<View
accessibilityIgnoresInvertColors={true}
importantForAccessibility={importantForAccessibility}
style={style}
ref={this._captureRef}>
{/* $FlowFixMe[incompatible-use] */}
<Image
{...props}
importantForAccessibility={importantForAccessibility}
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>.
// $FlowFixMe[prop-missing]
width: flattenedStyle?.width,
// $FlowFixMe[prop-missing]
height: flattenedStyle?.height,
},
imageStyle,
]}
ref={imageRef}
/>
{children}
</View>
);
}
}
export default ImageBackground;

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../..';
import type {AbstractImageAndroid, AbstractImageIOS} from './ImageTypes.flow';
import useMergeRefs from '../Utilities/useMergeRefs';
import * as React from 'react';
import {useRef} from 'react';
type ImageComponentDecorator = (AbstractImageAndroid => AbstractImageAndroid) &
(AbstractImageIOS => AbstractImageIOS);
let injectedImageComponentDecorator: ?ImageComponentDecorator;
export function unstable_setImageComponentDecorator(
imageComponentDecorator: ?ImageComponentDecorator,
): void {
injectedImageComponentDecorator = imageComponentDecorator;
}
export function unstable_getImageComponentDecorator(): ?ImageComponentDecorator {
return injectedImageComponentDecorator;
}
type ImageInstance = HostInstance;
type ImageAttachedCallback = (
imageInstance: ImageInstance,
) => void | (() => void);
const imageAttachedCallbacks = new Set<ImageAttachedCallback>();
export function unstable_registerImageAttachedCallback(
callback: ImageAttachedCallback,
): void {
imageAttachedCallbacks.add(callback);
}
export function unstable_unregisterImageAttachedCallback(
callback: ImageAttachedCallback,
): void {
imageAttachedCallbacks.delete(callback);
}
export function useWrapRefWithImageAttachedCallbacks(
forwardedRef: React.RefSetter<ImageInstance>,
): React.RefSetter<ImageInstance> {
const pendingCleanupCallbacks = useRef<Array<() => void>>([]);
const imageAttachedCallbacksRef =
useRef<?(node: ImageInstance | null) => void>(null);
if (imageAttachedCallbacksRef.current == null) {
imageAttachedCallbacksRef.current = (node: ImageInstance | null): void => {
if (node == null) {
if (pendingCleanupCallbacks.current.length > 0) {
pendingCleanupCallbacks.current.forEach(cb => cb());
pendingCleanupCallbacks.current = [];
}
} else {
imageAttachedCallbacks.forEach(imageAttachedCallback => {
const maybeCleanupCallback = imageAttachedCallback(node);
if (maybeCleanupCallback != null) {
pendingCleanupCallbacks.current.push(maybeCleanupCallback);
}
});
}
};
}
// `useMergeRefs` returns a stable ref if its arguments don't change.
return useMergeRefs<ImageInstance>(
forwardedRef,
imageAttachedCallbacksRef.current,
);
}

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

@@ -0,0 +1,371 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {ViewProps} from '../Components/View/ViewPropTypes';
import type {EdgeInsetsProp} from '../StyleSheet/EdgeInsetsPropType';
import type {
ColorValue,
ImageStyleProp,
ViewStyleProp,
} from '../StyleSheet/StyleSheet';
import type {
LayoutChangeEvent,
NativeSyntheticEvent,
} from '../Types/CoreEventTypes';
import type {ImageResizeMode} from './ImageResizeMode';
import type {ImageSource, ImageURISource} from './ImageSource';
import type {ImageType} from './ImageTypes.flow';
import * as React from 'react';
export type ImageSourcePropType = ImageSource;
type ImageProgressEventDataIOS = {
loaded: number,
total: number,
};
/**
* @see ImagePropsIOS.onProgress
*/
export type ImageProgressEventIOS = NativeSyntheticEvent<
$ReadOnly<ImageProgressEventDataIOS>,
>;
type ImageErrorEventData = {
error: string,
};
export type ImageErrorEvent = NativeSyntheticEvent<
$ReadOnly<ImageErrorEventData>,
>;
type ImageLoadEventData = {
source: {
height: number,
width: number,
uri: string,
},
};
export type ImageLoadEvent = NativeSyntheticEvent<
$ReadOnly<ImageLoadEventData>,
>;
export type ImagePropsIOS = $ReadOnly<{
/**
* A static image to display while loading the image source.
*
* See https://reactnative.dev/docs/image#defaultsource
*/
defaultSource?: ?ImageSource,
/**
* Invoked when a partial load of the image is complete.
*
* See https://reactnative.dev/docs/image#onpartialload
*/
onPartialLoad?: ?() => void,
/**
* Invoked on download progress with `{nativeEvent: {loaded, total}}`.
*
* See https://reactnative.dev/docs/image#onprogress
*/
onProgress?: ?(event: ImageProgressEventIOS) => void,
}>;
export type ImagePropsAndroid = $ReadOnly<{
/**
* similarly to `source`, this property represents the resource used to render
* the loading indicator for the image, displayed until image is ready to be
* displayed, typically after when it got downloaded from network.
*/
loadingIndicatorSource?: ?(number | $ReadOnly<ImageURISource>),
progressiveRenderingEnabled?: ?boolean,
fadeDuration?: ?number,
/**
* 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`.
*
* - `auto`: Use heuristics to pick between `resize` and `scale`.
*
* - `resize`: A software operation which changes the encoded image in memory before it
* gets decoded. This should be used instead of `scale` when the image is much larger
* than the view.
*
* - `scale`: The image gets drawn downscaled or upscaled. Compared to `resize`, `scale` is
* faster (usually hardware accelerated) and produces higher quality images. This
* should be used if the image is smaller than the view. It should also be used if the
* image is slightly bigger than the view.
*
* - `none`: No sampling is performed and the image is displayed in its full resolution. This
* should only be used in rare circumstances because it is considered unsafe as Android will
* throw a runtime exception when trying to render images that consume too much memory.
*
* More details about `resize` and `scale` can be found at http://frescolib.org/docs/resizing-rotating.html.
*
* @platform android
*/
resizeMethod?: ?('auto' | 'resize' | 'scale' | 'none'),
/**
* When the `resizeMethod` is set to `resize`, the destination dimensions are
* multiplied by this value. The `scale` method is used to perform the
* remainder of the resize.
* This is used to produce higher quality images when resizing to small dimensions.
* Defaults to 1.0.
*/
resizeMultiplier?: ?number,
}>;
export type ImagePropsBase = $ReadOnly<{
...Omit<ViewProps, 'style'>,
/**
* When true, indicates the image is an accessibility element.
*
* See https://reactnative.dev/docs/image#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#accessibilitylabel
*/
accessibilityLabel?: ?Stringish,
/**
* Alias for accessibilityLabel
* See https://reactnative.dev/docs/image#accessibilitylabel
*/
'aria-label'?: ?Stringish,
/**
* Represents the nativeID of the associated label. When the assistive technology focuses on the component with this props.
*
* @platform android
*/
'aria-labelledby'?: ?string,
/**
* The text that's read by the screen reader when the user interacts with
* the image.
*
* See https://reactnative.dev/docs/image#alt
*/
alt?: ?Stringish,
/**
* blurRadius: the blur radius of the blur filter added to the image
*
* See https://reactnative.dev/docs/image#blurradius
*/
blurRadius?: ?number,
/**
* See https://reactnative.dev/docs/image#capinsets
*/
capInsets?: ?EdgeInsetsProp,
/**
* Adds the CORS related header to the request.
* Similar to crossorigin from HTML.
*
* See https://reactnative.dev/docs/image#crossorigin
*/
crossOrigin?: ?('anonymous' | 'use-credentials'),
/**
* Height of the image component.
*
* See https://reactnative.dev/docs/image#height
*/
height?: number,
/**
* Width of the image component.
*
* See https://reactnative.dev/docs/image#width
*/
width?: number,
/**
* Invoked on load error with `{nativeEvent: {error}}`.
*
* See https://reactnative.dev/docs/image#onerror
*/
onError?: ?(event: ImageErrorEvent) => void,
/**
* onLayout function
*
* Invoked on mount and layout changes with
*
* {nativeEvent: { layout: {x, y, width, height} }}.
*
* See https://reactnative.dev/docs/image#onlayout
*/
onLayout?: ?(event: LayoutChangeEvent) => mixed,
/**
* Invoked when load completes successfully.
*
* See https://reactnative.dev/docs/image#onload
*/
onLoad?: ?(event: ImageLoadEvent) => void,
/**
* Invoked when load either succeeds or fails.
*
* See https://reactnative.dev/docs/image#onloadend
*/
onLoadEnd?: ?() => void,
/**
* Invoked on load start.
*
* See https://reactnative.dev/docs/image#onloadstart
*/
onLoadStart?: ?() => void,
/**
* The image source (either a remote URL or a local file resource).
*
* This prop can also contain several remote URLs, specified together with their width and height and potentially with scale/other URI arguments.
* The native side will then choose the best uri to display based on the measured size of the image container.
* A cache property can be added to control how networked request interacts with the local cache.
*
* The currently supported formats are png, jpg, jpeg, bmp, gif, webp (Android only), psd (iOS only).
*
* See https://reactnative.dev/docs/image#source
*/
source?: ?ImageSource,
/**
* A string indicating which referrer to use when fetching the resource.
* Similar to referrerpolicy from HTML.
*
* See https://reactnative.dev/docs/image#referrerpolicy
*/
referrerPolicy?: ?(
| 'no-referrer'
| 'no-referrer-when-downgrade'
| 'origin'
| 'origin-when-cross-origin'
| 'same-origin'
| 'strict-origin'
| 'strict-origin-when-cross-origin'
| 'unsafe-url'
),
/**
* Determines how to resize the image when the frame doesn't match the raw
* image dimensions.
*
* 'cover': Scale the image uniformly (maintain the image's aspect ratio)
* so that both dimensions (width and height) of the image will be equal
* to or larger than the corresponding dimension of the view (minus padding).
*
* 'contain': Scale the image uniformly (maintain the image's aspect ratio)
* so that both dimensions (width and height) of the image will be equal to
* or less than the corresponding dimension of the view (minus padding).
*
* 'stretch': Scale width and height independently, This may change the
* aspect ratio of the src.
*
* 'repeat': Repeat the image to cover the frame of the view.
* The image will keep it's size and aspect ratio. (iOS only)
*
* 'center': Scale the image down so that it is completely visible,
* if bigger than the area of the view.
* The image will not be scaled up.
*
* 'none': Do not resize the image. The image will be displayed at its intrinsic size.
*
* See https://reactnative.dev/docs/image#resizemode
*/
resizeMode?: ?ImageResizeMode,
/**
* A unique identifier for this element to be used in UI Automation
* testing scripts.
*
* See https://reactnative.dev/docs/image#testid
*/
testID?: ?string,
/**
* Changes the color of all the non-transparent pixels to the tintColor.
*
* See https://reactnative.dev/docs/image#tintcolor
*/
tintColor?: ColorValue,
/**
* A string representing the resource identifier for the image. Similar to
* src from HTML.
*
* See https://reactnative.dev/docs/image#src
*/
src?: ?string,
/**
* Similar to srcset from HTML.
*
* See https://reactnative.dev/docs/image#srcset
*/
srcSet?: ?string,
children?: empty,
}>;
export type ImageProps = $ReadOnly<{
...ImagePropsIOS,
...ImagePropsAndroid,
...ImagePropsBase,
/**
* See https://reactnative.dev/docs/image#style
*/
style?: ?ImageStyleProp,
}>;
export type ImageBackgroundProps = $ReadOnly<{
...ImageProps,
children?: React.Node,
/**
* Style applied to the outer View component
*
* See https://reactnative.dev/docs/imagebackground#style
*/
style?: ?ViewStyleProp,
/**
* Style applied to the inner Image component
*
* See https://reactnative.dev/docs/imagebackground#imagestyle
*/
imageStyle?: ?ImageStyleProp,
/**
* Allows to set a reference to the inner Image component
*
* See https://reactnative.dev/docs/imagebackground#imageref
*/
imageRef?: React.RefSetter<React.ElementRef<ImageType>>,
}>;

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export type ImageResizeMode =
| 'cover'
| 'contain'
| 'stretch'
| 'repeat'
| 'center'
| 'none';
/**
* @see ImageResizeMode.js
*/
export interface ImageResizeModeStatic {
/**
* contain - The image will be resized such that it will be completely
* visible, contained within the frame of the View.
*/
contain: ImageResizeMode;
/**
* cover - The image will be resized such that the entire area of the view
* is covered by the image, potentially clipping parts of the image.
*/
cover: ImageResizeMode;
/**
* stretch - The image will be stretched to fill the entire frame of the
* view without clipping. This may change the aspect ratio of the image,
* distoring it. Only supported on iOS.
*/
stretch: ImageResizeMode;
/**
* center - The image will be scaled down such that it is completely visible,
* if bigger than the area of the view.
* The image will not be scaled up.
*/
center: ImageResizeMode;
/**
* repeat - The image will be repeated to cover the frame of the View. The
* image will keep it's size and aspect ratio.
*/
repeat: ImageResizeMode;
/**
* none - The image will be displayed at its intrinsic size, which means the
* image will not be scaled up or down.
*/
none: ImageResizeMode;
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @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'
// The image will not be resized at all.
| 'none';

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
/*
* @see https://github.com/facebook/react-native/blob/master/Libraries/Image/ImageSource.js
*/
export interface ImageURISource {
/**
* `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 | undefined;
/**
* `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 | undefined;
/**
* `method` is the HTTP Method to use. Defaults to GET if not specified.
*/
method?: string | undefined;
/**
* `headers` is an object representing the HTTP headers to send along with the
* request for a remote image.
*/
headers?: {[key: string]: string} | undefined;
/**
* `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.
*/
cache?: 'default' | 'reload' | 'force-cache' | 'only-if-cached' | undefined;
/**
* `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 | undefined;
/**
* `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 | undefined;
height?: number | undefined;
/**
* `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 | undefined;
}
export type ImageRequireSource = number;
export type ImageSource =
| ImageRequireSource
| ImageURISource
| ReadonlyArray<ImageURISource>;

View File

@@ -0,0 +1,137 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
'use strict';
/**
* This type is intentionally inexact in order to permit call sites that supply
* extra properties.
*/
export interface ImageURISource {
/**
* `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?: ?{[string]: string};
/**
* `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.
*/
+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;
}
export type ImageRequireSource = number;
export type ImageSource =
| ImageRequireSource
| ImageURISource
| $ReadOnlyArray<ImageURISource>;
type ImageSourceProperties = {
body?: ?string,
bundle?: ?string,
cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached'),
headers?: ?{[string]: string},
height?: ?number,
method?: ?string,
scale?: ?number,
uri?: ?string,
width?: ?number,
...
};
export function getImageSourceProperties(
imageSource: ImageURISource,
): $ReadOnly<ImageSourceProperties> {
const object: ImageSourceProperties = {};
if (imageSource.body != null) {
object.body = imageSource.body;
}
if (imageSource.bundle != null) {
object.bundle = imageSource.bundle;
}
if (imageSource.cache != null) {
object.cache = imageSource.cache;
}
if (imageSource.headers != null) {
object.headers = imageSource.headers;
}
if (imageSource.height != null) {
object.height = imageSource.height;
}
if (imageSource.method != null) {
object.method = imageSource.method;
}
if (imageSource.scale != null) {
object.scale = imageSource.scale;
}
if (imageSource.uri != null) {
object.uri = imageSource.uri;
}
if (imageSource.width != null) {
object.width = imageSource.width;
}
return object;
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageProps} from './ImageProps';
import resolveAssetSource from './resolveAssetSource';
export type ImageSourceHeaders = {
[string]: string,
};
/**
* A function which returns the appropriate value for image source
* by resolving the `source`, `src` and `srcSet` props.
*/
export function getImageSourcesFromImageProps(
imageProps: ImageProps,
):
| ?ResolvedAssetSource
| $ReadOnlyArray<{uri: string, headers: ImageSourceHeaders, ...}> {
let source = resolveAssetSource(imageProps.source);
let sources;
const {crossOrigin, referrerPolicy, src, srcSet, width, height} = imageProps;
const headers: ImageSourceHeaders = {};
if (crossOrigin === 'use-credentials') {
headers['Access-Control-Allow-Credentials'] = 'true';
}
if (referrerPolicy != null) {
headers['Referrer-Policy'] = referrerPolicy;
}
if (srcSet != null) {
const sourceList = [];
const srcSetList = srcSet.split(', ');
// `src` prop should be used with default scale if `srcSet` does not have 1x scale.
let shouldUseSrcForDefaultScale = true;
srcSetList.forEach(imageSrc => {
const [uri, xScale = '1x'] = imageSrc.split(' ');
if (!xScale.endsWith('x')) {
console.warn(
'The provided format for scale is not supported yet. Please use scales like 1x, 2x, etc.',
);
} else {
const scale = parseInt(xScale.split('x')[0], 10);
if (!isNaN(scale)) {
// 1x scale is provided in `srcSet` prop so ignore the `src` prop if provided.
shouldUseSrcForDefaultScale =
scale === 1 ? false : shouldUseSrcForDefaultScale;
sourceList.push({headers, scale, uri, width, height});
}
}
});
if (shouldUseSrcForDefaultScale && src != null) {
sourceList.push({
headers,
scale: 1,
uri: src,
width,
height,
});
}
if (sourceList.length === 0) {
console.warn('The provided value for srcSet is not valid.');
}
sources = sourceList;
} else if (src != null) {
sources = [{uri: src, headers: headers, width, height}];
} else if (source != null && source.uri && Object.keys(headers).length > 0) {
sources = [{...source, headers}];
} else {
sources = source;
}
return sources;
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../..';
import type {RootTag} from '../Types/RootTagTypes';
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageProps as ImagePropsType} from './ImageProps';
import type {ImageSource} from './ImageSource';
import * as React from 'react';
export type ImageSize = {
width: number,
height: number,
};
export type ImageResolvedAssetSource = ResolvedAssetSource;
type ImageComponentStaticsIOS = $ReadOnly<{
getSize(uri: string): Promise<ImageSize>,
getSize(
uri: string,
success: (width: number, height: number) => void,
failure?: (error: mixed) => void,
): void,
getSizeWithHeaders(
uri: string,
headers: {[string]: string, ...},
): Promise<ImageSize>,
getSizeWithHeaders(
uri: string,
headers: {[string]: string, ...},
success: (width: number, height: number) => void,
failure?: (error: mixed) => void,
): void,
prefetch(url: string): Promise<boolean>,
prefetchWithMetadata(
url: string,
queryRootName: string,
rootTag?: ?RootTag,
): Promise<boolean>,
queryCache(
urls: Array<string>,
): Promise<{[url: string]: 'memory' | 'disk' | 'disk/memory', ...}>,
/**
* @see https://reactnative.dev/docs/image#resolveassetsource
*/
resolveAssetSource(source: ImageSource): ?ImageResolvedAssetSource,
}>;
type ImageComponentStaticsAndroid = $ReadOnly<{
...ImageComponentStaticsIOS,
abortPrefetch(requestId: number): void,
}>;
export type AbstractImageAndroid = component(
ref?: React.RefSetter<HostInstance>,
...props: ImagePropsType
);
export type ImageAndroid = AbstractImageAndroid & ImageComponentStaticsAndroid;
export type AbstractImageIOS = component(
ref?: React.RefSetter<HostInstance>,
...props: ImagePropsType
);
export type ImageIOS = AbstractImageIOS & ImageComponentStaticsIOS;
export type ImageType = ImageIOS | ImageAndroid;
export type {ImageProps} from './ImageProps';

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {ImageResizeMode} from './ImageResizeMode';
const objectFitMap: {[string]: ImageResizeMode} = {
contain: 'contain',
cover: 'cover',
fill: 'stretch',
'scale-down': 'contain',
none: 'none',
};
export function convertObjectFitToResizeMode(
objectFit: ?string,
): ?ImageResizeMode {
return objectFit != null ? objectFitMap[objectFit] : undefined;
}

View File

@@ -0,0 +1,171 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {HostComponent} from '../../src/private/types/HostComponent';
import type {HostInstance} from '../../src/private/types/HostInstance';
import type {ViewProps} from '../Components/View/ViewPropTypes';
import type {PartialViewConfig} from '../Renderer/shims/ReactNativeTypes';
import type {
ColorValue,
DangerouslyImpreciseStyle,
ImageStyleProp,
} from '../StyleSheet/StyleSheet';
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageProps} from './ImageProps';
import type {ImageSource} from './ImageSource';
import * as NativeComponentRegistry from '../NativeComponent/NativeComponentRegistry';
import {ConditionallyIgnoredEventHandlers} from '../NativeComponent/ViewConfigIgnore';
import codegenNativeCommands from '../Utilities/codegenNativeCommands';
import Platform from '../Utilities/Platform';
type ImageHostComponentProps = $ReadOnly<{
...ImageProps,
...ViewProps,
style?: ImageStyleProp | DangerouslyImpreciseStyle,
// iOS native props
tintColor?: ColorValue,
// Android native props
shouldNotifyLoadEvents?: boolean,
src?:
| ?ResolvedAssetSource
| ?$ReadOnlyArray<?$ReadOnly<{uri?: ?string, ...}>>,
headers?: ?{[string]: string},
defaultSource?: ?ImageSource | ?string,
loadingIndicatorSrc?: ?string,
}>;
interface NativeCommands {
+setIsVisible_EXPERIMENTAL: (
viewRef: HostInstance,
isVisible: boolean,
time: number,
) => void;
}
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['setIsVisible_EXPERIMENTAL'],
});
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
Platform.OS === 'android'
? {
uiViewClassName: 'RCTImageView',
bubblingEventTypes: {},
directEventTypes: {
topLoadStart: {
registrationName: 'onLoadStart',
},
topProgress: {
registrationName: 'onProgress',
},
topError: {
registrationName: 'onError',
},
topLoad: {
registrationName: 'onLoad',
},
topLoadEnd: {
registrationName: 'onLoadEnd',
},
},
validAttributes: {
blurRadius: true,
defaultSource: true,
internal_analyticTag: true,
resizeMethod: true,
resizeMode: true,
resizeMultiplier: true,
tintColor: {
process: require('../StyleSheet/processColor').default,
},
borderBottomLeftRadius: true,
borderTopLeftRadius: true,
src: true,
// NOTE: New Architecture expects this to be called `source`,
// regardless of the platform, therefore propagate it as well.
// For the backwards compatibility reasons, we keep both `src`
// and `source`, which will be identical at this stage.
source: true,
borderRadius: true,
headers: true,
shouldNotifyLoadEvents: true,
overlayColor: {
process: require('../StyleSheet/processColor').default,
},
borderColor: {
process: require('../StyleSheet/processColor').default,
},
accessible: true,
progressiveRenderingEnabled: true,
fadeDuration: true,
borderBottomRightRadius: true,
borderTopRightRadius: true,
loadingIndicatorSrc: true,
},
}
: {
uiViewClassName: 'RCTImageView',
bubblingEventTypes: {},
directEventTypes: {
topLoadStart: {
registrationName: 'onLoadStart',
},
topProgress: {
registrationName: 'onProgress',
},
topError: {
registrationName: 'onError',
},
topPartialLoad: {
registrationName: 'onPartialLoad',
},
topLoad: {
registrationName: 'onLoad',
},
topLoadEnd: {
registrationName: 'onLoadEnd',
},
},
validAttributes: {
blurRadius: true,
capInsets: {
diff: require('../Utilities/differ/insetsDiffer').default,
},
defaultSource: {
process: require('./resolveAssetSource').default,
},
internal_analyticTag: true,
resizeMode: true,
source: true,
tintColor: {
process: require('../StyleSheet/processColor').default,
},
...ConditionallyIgnoredEventHandlers({
onLoadStart: true,
onLoad: true,
onLoadEnd: true,
onProgress: true,
onError: true,
onPartialLoad: true,
}),
},
};
const ImageViewNativeComponent: HostComponent<ImageHostComponentProps> =
NativeComponentRegistry.get<ImageHostComponentProps>(
'RCTImageView',
() => __INTERNAL_VIEW_CONFIG,
);
export default ImageViewNativeComponent;

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativeImageEditor';
import NativeImageEditor from '../../src/private/specs_DEPRECATED/modules/NativeImageEditor';
export default NativeImageEditor;

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativeImageLoaderAndroid';
import NativeImageLoaderAndroid from '../../src/private/specs_DEPRECATED/modules/NativeImageLoaderAndroid';
export default NativeImageLoaderAndroid;

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativeImageLoaderIOS';
import NativeImageLoaderIOS from '../../src/private/specs_DEPRECATED/modules/NativeImageLoaderIOS';
export default NativeImageLoaderIOS;

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativeImageStoreAndroid';
import NativeImageStoreAndroid from '../../src/private/specs_DEPRECATED/modules/NativeImageStoreAndroid';
export default NativeImageStoreAndroid;

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
export * from '../../src/private/specs_DEPRECATED/modules/NativeImageStoreIOS';
import NativeImageStoreIOS from '../../src/private/specs_DEPRECATED/modules/NativeImageStoreIOS';
export default NativeImageStoreIOS;

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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,175 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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 new];
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;
if (@available(iOS 14.0, *)) {
} else {
// A loop count of 1 means it should animate 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,12 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageURLLoader.h>
@interface RCTBundleAssetImageLoader : NSObject <RCTImageURLLoader>
@end

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBundleAssetImageLoader.h>
#import <atomic>
#import <memory>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTTurboModule.h>
#import "RCTImagePlugins.h"
@interface RCTBundleAssetImageLoader () <RCTTurboModule>
@end
@implementation RCTBundleAssetImageLoader
RCT_EXPORT_MODULE()
- (BOOL)canLoadImageURL:(NSURL *)requestURL
{
return RCTIsBundleAssetURL(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 != nullptr) {
if (progressHandler != nullptr) {
progressHandler(1, 1);
}
completionHandler(nil, image);
} else {
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
RCTLogWarn(@"%@", message);
completionHandler(RCTErrorWithMessage(message), nil);
}
return nil;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
- (float)loaderPriority
{
return 1;
}
@end
Class RCTBundleAssetImageLoaderCls(void)
{
return RCTBundleAssetImageLoader.class;
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.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,31 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageDataDecoder.h>
@interface RCTGIFImageDecoder : NSObject <RCTImageDataDecoder>
@end

View File

@@ -0,0 +1,63 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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") == 0) || (strcmp(header, "GIF89a") == 0);
}
- (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 == nullptr) {
completionHandler(nil, nil);
return ^{
};
}
completionHandler(nil, image);
return ^{
};
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
@end
Class RCTGIFImageDecoderCls()
{
return RCTGIFImageDecoder.class;
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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,101 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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 || (((CGImageGetBitmapInfo(imageRef) & kCGBitmapAlphaInfoMask)) == 0u)) {
UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat];
rendererFormat.scale = inputImage.scale;
UIGraphicsImageRenderer *const renderer = [[UIGraphicsImageRenderer alloc] initWithSize:inputImage.size
format:rendererFormat];
imageRef = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) {
[inputImage drawAtPoint:CGPointZero];
}].CGImage;
}
vImage_Buffer buffer1;
vImage_Buffer 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);
if (buffer1.data == nullptr) {
return inputImage;
}
buffer2.data = malloc(bytes);
if (buffer2.data == nullptr) {
free(buffer1.data);
return inputImage;
}
// 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
vImage_Error tempBufferSize = vImageBoxConvolve_ARGB8888(
&buffer1, &buffer2, NULL, 0, 0, boxSize, boxSize, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend);
if (tempBufferSize <= 0) {
free(buffer1.data);
free(buffer2.data);
return inputImage;
}
void *tempBuffer = malloc(tempBufferSize);
if (tempBuffer == nullptr) {
free(buffer1.data);
free(buffer2.data);
return inputImage;
}
// 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,45 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.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>
RCT_EXTERN void RCTSetImageCacheLimits(
NSUInteger maxCacheableDecodedImageSizeInBytes,
NSUInteger imageCacheTotalCostLimit);
@end

View File

@@ -0,0 +1,164 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageCache.h>
#import <objc/runtime.h>
#import <ImageIO/ImageIO.h>
#import <React/RCTConvert.h>
#import <React/RCTNetworking.h>
#import <React/RCTResizeMode.h>
#import <React/RCTUtils.h>
#import <React/RCTImageUtils.h>
static NSUInteger RCTMaxCacheableDecodedImageSizeInBytes = 2 * 1024 * 1024;
static NSUInteger RCTImageCacheTotalCostLimit = 20 * 1024 * 1024;
void RCTSetImageCacheLimits(NSUInteger maxCacheableDecodedImageSizeInBytes, NSUInteger imageCacheTotalCostLimit)
{
RCTMaxCacheableDecodedImageSizeInBytes = maxCacheableDecodedImageSizeInBytes;
RCTImageCacheTotalCostLimit = imageCacheTotalCostLimit;
}
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 = RCTImageCacheTotalCostLimit;
_cacheStaleTimes = [NSMutableDictionary new];
[[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 <= RCTMaxCacheableDecodedImageSizeInBytes) {
[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 new];
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) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTImageURLLoader.h>
#import <React/RCTResizeMode.h>
#import <React/RCTURLRequestHandler.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) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBridgeModule.h>
@interface RCTImageEditingManager : NSObject <RCTBridgeModule>
@end

View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageEditingManager.h>
#import <FBReactNativeSpec/FBReactNativeSpec.h>
#import <React/RCTConvert.h>
#import <React/RCTImageLoader.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTImageStoreManager.h>
#import <React/RCTImageUtils.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 moduleRegistry = _moduleRegistry;
/**
* 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;
[[_moduleRegistry moduleForName:"ImageLoader"]
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->_moduleRegistry moduleForName:"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>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeImageEditorSpecJSI>(params);
}
@end
Class RCTImageEditingManagerCls()
{
return RCTImageEditingManager.class;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
/**
* The image loader (i.e. RCTImageLoader) implement this to declare whether image performance should be logged.
*/
@protocol RCTImageLoaderLoggableProtocol <NSObject>
/**
* Image instrumentation - declares whether its caller should log images
*/
- (BOOL)shouldEnablePerfLoggingForRequestUrl:(NSURL *)url;
@end
/**
* Image handlers in the image loader implement this to declare whether image performance should be logged.
*/
@protocol RCTImageLoaderLoggable
/**
* Image instrumentation - declares whether its caller should log images
*/
- (BOOL)shouldEnablePerfLogging;
@end

View File

@@ -0,0 +1,135 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTImageCache.h>
#import <React/RCTImageDataDecoder.h>
#import <React/RCTImageURLLoader.h>
#import <React/RCTResizeMode.h>
#import <React/RCTURLRequestHandler.h>
NS_ASSUME_NONNULL_BEGIN
/**
* 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(NSInteger, 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
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTImageURLLoaderWithAttribution.h>
@protocol RCTImageLoaderWithAttributionProtocol <RCTImageLoaderProtocol, RCTImageLoaderLoggableProtocol>
// 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:(RCTImageLoaderCompletionBlockWithMetadata)completionBlock;
#endif
/**
* 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 request was cancelled.
*/
- (void)trackURLImageRequestDidDestroy:(RCTImageURLLoaderRequest *)loaderRequest;
/**
* Image instrumentation - notify that the native image view was destroyed.
*/
- (void)trackURLImageDidDestroy:(RCTImageURLLoaderRequest *)loaderRequest;
@end

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @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,37 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @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) {
// Intentionally leak to avoid crashing after static destructors are run.
static const auto sCoreModuleClassMap = new const std::unordered_map<std::string, Class (*)(void)>{
{"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,17 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTShadowView.h>
#ifndef RCT_REMOVE_LEGACY_ARCH
__attribute__((deprecated("This API will be removed along with the legacy architecture.")))
@interface RCTImageShadowView : RCTShadowView
@end
#endif

View File

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

View File

@@ -0,0 +1,55 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTBridgeProxy.h>
#import <React/RCTURLRequestHandler.h>
RCT_EXTERN void RCTEnableImageStoreManagerStorageQueue(BOOL enabled);
@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
@interface RCTBridgeProxy (RCTImageStoreManager)
@property (nonatomic, readonly) RCTImageStoreManager *imageStoreManager;
@end

View File

@@ -0,0 +1,272 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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/RCTImageUtils.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import "RCTImagePlugins.h"
static NSString *const RCTImageStoreURLScheme = @"rct-image-store";
@interface RCTImageStoreManager () <NativeImageStoreIOSSpec>
@end
@implementation RCTImageStoreManager {
NSMutableDictionary<NSString *, NSData *> *_store;
NSUInteger _id;
}
@synthesize methodQueue = _methodQueue;
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (float)handlerPriority
{
return 1;
}
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)(void))block
{
dispatch_async(_methodQueue, ^{
[self removeImageForTag:imageTag];
if (block != nullptr) {
block();
}
});
}
- (NSString *)_storeImageData:(NSData *)imageData
{
RCTAssertThread(_methodQueue, @"Must be called on RCTImageStoreManager thread");
if (_store == nullptr) {
_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 == nullptr) {
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 != nullptr) {
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 == nullptr) {
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]);
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (sourceRef == nullptr) {
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 != nullptr) {
((void (^)(void))requestToken)();
}
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared<facebook::react::NativeImageStoreIOSSpecJSI>(params);
}
@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]);
});
});
}
@end
@implementation RCTBridge (RCTImageStoreManager)
- (RCTImageStoreManager *)imageStoreManager
{
return [self moduleForClass:[RCTImageStoreManager class]];
}
@end
@implementation RCTBridgeProxy (RCTImageStoreManager)
- (RCTImageStoreManager *)imageStoreManager
{
return [self moduleForClass:[RCTImageStoreManager class]];
}
@end
Class RCTImageStoreManagerCls(void)
{
return RCTImageStoreManager.class;
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTResizeMode.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^RCTImageLoaderProgressBlock)(int64_t progress, int64_t total);
typedef void (^RCTImageLoaderPartialLoadBlock)(UIImage *image);
typedef void (^RCTImageLoaderCompletionBlock)(NSError *_Nullable error, UIImage *_Nullable image);
// Metadata is passed as a id in an additional parameter because there are forks of RN without this parameter,
// and the complexity of RCTImageLoader would make using protocols here difficult to typecheck.
typedef void (^RCTImageLoaderCompletionBlockWithMetadata)(
NSError *_Nullable error,
UIImage *_Nullable image,
id _Nullable metadata);
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;
/**
* DEPRECATED: Please use updated signature for loadImageForURL which uses
* completionHandlerWithMetadata instead.
* 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
RCT_DEPRECATED;
@optional
/*
* 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
completionHandlerWithMetadata:
(RCTImageLoaderCompletionBlockWithMetadata)completionHandlerWithMetadata;
/**
* 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
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageLoaderLoggable.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTImageURLLoader.h>
// TODO (T61325135): Remove C++ checks
#ifdef __cplusplus
#import <string>
namespace facebook::react {
struct ImageURLLoaderAttribution {
int32_t nativeViewTag = 0;
int32_t surfaceId = 0;
std::string queryRootName;
NSString *analyticTag;
};
} // namespace facebook::react
#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, RCTImageLoaderLoggable>
// 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:(RCTImageLoaderCompletionBlockWithMetadata)completionHandler;
#endif
/**
* 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 request was destroyed.
*/
- (void)trackURLImageRequestDidDestroy:(RCTImageURLLoaderRequest *)loaderRequest;
/**
* Image instrumentation - notify that the native image view was destroyed.
*/
- (void)trackURLImageDidDestroy:(RCTImageURLLoaderRequest *)loaderRequest;
@end

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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,94 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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,400 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageUtils.h>
#import <cmath>
#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:
case RCTResizeModeNone:
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;
CGFloat targetAspect = 0.0;
if (resizeMode != RCTResizeModeStretch) {
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:
case RCTResizeModeNone:
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 = (NSNumber *)CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth);
NSNumber *height = (NSNumber *)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 == RCTResizeModeStretch) {
// 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);
CGImageRef imageRef;
BOOL createThumbnail = targetPixelSize.width != 0 && targetPixelSize.height != 0 &&
(sourceSize.width > targetPixelSize.width || sourceSize.height > targetPixelSize.height);
if (createThumbnail) {
CGFloat maxPixelSize = fmax(targetPixelSize.width, targetPixelSize.height);
// Get a thumbnail of the source image. This is usually slower than creating a full-sized image,
// but takes up less memory once it's done.
imageRef = CGImageSourceCreateThumbnailAtIndex(
sourceRef, 0, (__bridge CFDictionaryRef) @{
(id)kCGImageSourceShouldAllowFloat : @YES,
(id)kCGImageSourceCreateThumbnailWithTransform : @YES,
(id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(id)kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize),
});
} else {
// Get an image in full size. This is faster than `CGImageSourceCreateThumbnailAtIndex`
// and consumes less memory if only the target size doesn't require downscaling.
imageRef = CGImageSourceCreateImageAtIndex(
sourceRef, 0, (__bridge CFDictionaryRef) @{
(id)kCGImageSourceShouldAllowFloat : @YES,
});
}
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);
UIGraphicsImageRendererFormat *const rendererFormat = [UIGraphicsImageRendererFormat defaultFormat];
rendererFormat.opaque = opaque;
rendererFormat.scale = destScale;
UIGraphicsImageRenderer *const renderer = [[UIGraphicsImageRenderer alloc] initWithSize:destSize
format:rendererFormat];
return [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) {
CGContextConcatCTM(context.CGContext, transform);
[image drawAtPoint:CGPointZero];
}];
}
BOOL RCTImageHasAlpha(CGImageRef image)
{
switch (CGImageGetAlphaInfo(image)) {
case kCGImageAlphaNone:
case kCGImageAlphaNoneSkipLast:
case kCGImageAlphaNoneSkipFirst:
return NO;
default:
return YES;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTResizeMode.h>
#import <React/RCTView.h>
#import <UIKit/UIKit.h>
#ifndef RCT_REMOVE_LEGACY_ARCH
@class RCTBridge;
@class RCTImageSource;
__attribute__((deprecated("This API will be removed along with the legacy architecture.")))
@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;
@property (nonatomic, copy) NSString *internal_analyticTag;
@end
#endif // RCT_REMOVE_LEGACY_ARCH

View File

@@ -0,0 +1,508 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageView.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTImageBlurUtils.h>
#import <React/RCTImageLoaderWithAttributionProtocol.h>
#import <React/RCTImageSource.h>
#import <React/RCTImageUtils.h>
#import <React/RCTUIImageViewAnimated.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#ifndef RCT_REMOVE_LEGACY_ARCH
/**
* 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 = @{
@"uri" : source.request.URL.absoluteString,
@"width" : @(source.size.width * source.scale),
@"height" : @(source.size.height * source.scale),
};
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;
// Weak reference back to the active image loader.
__weak id<RCTImageLoaderWithAttributionProtocol> _imageLoader;
// 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;
// Whether the latest change of props requires the image to be reloaded
BOOL _needsReload;
RCTUIImageViewAnimated *_imageView;
RCTImageURLLoaderRequest *_loaderRequest;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if ((self = [super initWithFrame:CGRectZero])) {
_bridge = bridge;
_imageView = [RCTUIImageViewAnimated new];
_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];
[center addObserver:self
selector:@selector(clearImageIfDetached)
name:UISceneDidEnterBackgroundNotification
object:nil];
}
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 missized 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)setInternal_analyticTag:(NSString *)internal_analyticTag
{
if (_internal_analyticTag != internal_analyticTag) {
_internal_analyticTag = internal_analyticTag;
_needsReload = YES;
}
}
- (void)cancelImageLoad
{
[_loaderRequest cancel];
_pendingImageSource = nil;
}
- (void)cancelAndClearImageLoad
{
[self cancelImageLoad];
[_imageLoader trackURLImageRequestDidDestroy:_loaderRequest];
_loaderRequest = nil;
if (!self.image) {
self.image = _defaultImage;
}
}
- (void)clearImageIfDetached
{
if (!self.window) {
[self cancelAndClearImageLoad];
self.image = nil;
_imageSource = nil;
}
}
- (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 cancelAndClearImageLoad];
_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 (self.onProgress) {
RCTDirectEventBlock onProgress = self.onProgress;
progressHandler = ^(int64_t loaded, int64_t total) {
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;
}
RCTImageLoaderCompletionBlockWithMetadata completionHandler = ^(NSError *error, UIImage *loadedImage, id metadata) {
[weakSelf imageLoaderLoadedImage:loadedImage error:error forImageSource:source partial:NO];
};
if (!_imageLoader) {
_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],
.analyticTag = self.internal_analyticTag}
progressBlock:progressHandler
partialLoadBlock:partialLoadHandler
completionBlock:completionHandler];
_loaderRequest = loaderRequest;
} else {
[self cancelAndClearImageLoad];
}
}
- (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) {
__weak RCTImageView *weakSelf = self;
RCTExecuteOnMainQueue(^{
weakSelf.image = nil;
});
if (_onError) {
_onError(@{
@"error" : error.localizedDescription,
@"responseCode" : (error.userInfo[@"httpStatusCode"] ?: [NSNull null]),
@"httpResponseHeaders" : (error.userInfo[@"httpResponseHeaders"] ?: [NSNull null])
});
}
if (_onLoadEnd) {
_onLoadEnd(nil);
}
return;
}
__weak RCTImageView *weakSelf = self;
void (^setImageBlock)(UIImage *) = ^(UIImage *image) {
RCTImageView *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (!isPartialLoad) {
strongSelf->_imageSource = source;
strongSelf->_pendingImageSource = nil;
}
strongSelf.image = image;
if (isPartialLoad) {
if (strongSelf->_onPartialLoad) {
strongSelf->_onPartialLoad(nil);
}
} else {
if (strongSelf->_onLoad) {
RCTImageSource *sourceLoaded = [source imageSourceWithSize:image.size scale:image.scale];
strongSelf->_onLoad(onLoadParamsForSource(sourceLoaded));
}
if (strongSelf->_onLoadEnd) {
strongSelf->_onLoadEnd(nil);
}
}
};
if (_blurRadius > __FLT_EPSILON__) {
// Blur on a background thread to avoid blocking interaction
CGFloat blurRadius = self.blurRadius;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *blurredImage = RCTBlurredImageWithRadius(loadedImage, 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(), RCTResizeModeFromUIViewContentMode(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.
// Do not clear _loaderRequest because this component can be visible again without changing image source
[self cancelImageLoad];
} else if ([self shouldChangeImageSource]) {
[self reloadImage];
}
}
- (void)dealloc
{
[_imageLoader trackURLImageDidDestroy:_loaderRequest];
}
@end
#endif // RCT_REMOVE_LEGACY_ARCH

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewManager.h>
#ifndef RCT_REMOVE_LEGACY_ARCH
__attribute__((deprecated("This API will be removed along with the legacy architecture.")))
@interface RCTImageViewManager : RCTViewManager
@end
#endif // RCT_REMOVE_LEGACY_ARCH

View File

@@ -0,0 +1,115 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTImageViewManager.h>
#ifndef RCT_REMOVE_LEGACY_ARCH
#import <UIKit/UIKit.h>
#import <React/RCTConvert.h>
#import <React/RCTImageSource.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTImageShadowView.h>
#import <React/RCTImageView.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_EXPORT_VIEW_PROPERTY(internal_analyticTag, NSString)
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
#endif // RCT_REMOVE_LEGACY_ARCH

View File

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

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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 RCTIsBundleAssetURL(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 != nullptr) {
if (progressHandler != nullptr) {
progressHandler(1, 1);
}
completionHandler(nil, image);
} else {
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
RCTLogWarn(@"%@", message);
completionHandler(RCTErrorWithMessage(message), nil);
}
return nil;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
- (float)loaderPriority
{
return 0;
}
@end
Class RCTLocalAssetImageLoaderCls(void)
{
return RCTLocalAssetImageLoader.class;
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#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.
RCTResizeModeNone = UIViewContentModeTopLeft,
};
static inline RCTResizeMode RCTResizeModeFromUIViewContentMode(UIViewContentMode mode)
{
switch (mode) {
case UIViewContentModeScaleToFill:
return RCTResizeModeStretch;
case UIViewContentModeScaleAspectFit:
return RCTResizeModeContain;
case UIViewContentModeScaleAspectFill:
return RCTResizeModeCover;
case UIViewContentModeTopLeft:
return RCTResizeModeNone;
case UIViewContentModeRedraw:
case UIViewContentModeTop:
case UIViewContentModeBottom:
case UIViewContentModeLeft:
case UIViewContentModeRight:
case UIViewContentModeTopRight:
case UIViewContentModeBottomLeft:
case UIViewContentModeBottomRight:
return RCTResizeModeRepeat;
case UIViewContentModeCenter:
default:
return RCTResizeModeCenter;
}
};
@interface RCTConvert (RCTResizeMode)
+ (RCTResizeMode)RCTResizeMode:(id)json;
@end

View File

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

View File

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

View File

@@ -0,0 +1,332 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTDisplayWeakRefreshable.h>
#import <React/RCTUIImageViewAnimated.h>
#import <mach/mach.h>
#import <objc/runtime.h>
static NSUInteger RCTDeviceTotalMemory(void)
{
return (NSUInteger)[[NSProcessInfo processInfo] physicalMemory];
}
static NSUInteger RCTDeviceFreeMemory(void)
{
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 () <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, 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;
[_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
{
UIImage *thisImage = self.animatedImage != nil ? self.animatedImage : super.image;
if (image == thisImage) {
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.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];
[self prefetchNextFrame:nil fetchFrameIndex:1];
if ([self paused]) {
[self start];
}
}
super.image = image;
}
#pragma mark - Private
- (NSOperationQueue *)fetchQueue
{
if (!_fetchQueue) {
_fetchQueue = [NSOperationQueue new];
_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;
}
- (void)prefetchNextFrame:(UIImage *)fetchFrame fetchFrameIndex:(NSInteger)fetchFrameIndex
{
if (!fetchFrame && !(self.frameBuffer.count == self.totalFrameCount) && 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 - Animation
- (void)start
{
self.displayLink.paused = NO;
}
- (void)stop
{
self.displayLink.paused = YES;
}
- (BOOL)paused
{
return self.displayLink.isPaused;
}
- (void)displayDidRefresh:(CADisplayLink *)displayLink
{
// displaylink.duration -- time interval between frames, assuming maximumFramesPerSecond
// displayLink.preferredFramesPerSecond (>= iOS 10) -- Set to 30 for displayDidRefresh to be called at 30 fps
// durationToNextRefresh -- Time interval to the next time displayDidRefresh is called
NSTimeInterval durationToNextRefresh = displayLink.targetTimestamp - displayLink.timestamp;
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 += durationToNextRefresh;
NSTimeInterval currentDuration = [self.animatedImage animatedImageDurationAtIndex:currentFrameIndex];
if (self.currentTime < currentDuration) {
// Current frame timestamp not reached, return
return;
}
self.currentTime -= currentDuration;
// nextDuration - duration to wait before displaying next image
NSTimeInterval nextDuration = [self.animatedImage animatedImageDurationAtIndex:nextFrameIndex];
if (self.currentTime > nextDuration) {
// Do not skip frame
self.currentTime = nextDuration;
}
currentFrameIndex = nextFrameIndex;
self.currentFrameIndex = nextFrameIndex;
nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount;
}
// 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);
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;
}
dispatch_semaphore_signal(self.lock);
self.currentFrame = currentFrame;
super.image = currentFrame;
self.bufferMiss = NO;
} 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;
}
[self prefetchNextFrame:fetchFrame fetchFrameIndex:fetchFrameIndex];
}
#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((double)total * 0.2, (double)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,57 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
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 if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
header_search_paths = [
"\"${PODS_ROOT}/Headers/Public/ReactCodegen/react/renderer/components\"",
]
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 = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.compiler_flags = '-Wno-nullability-completeness'
s.source = source
s.source_files = podspec_sources("*.{m,mm}", "**/*.h")
s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
s.header_dir = "RCTImage"
s.pod_target_xcconfig = {
"USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
"HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
"DEFINES_MODULE" => "YES"
}
s.framework = ["Accelerate", "UIKit", "QuartzCore", "ImageIO", "CoreGraphics"]
s.dependency "RCTTypeSafety"
s.dependency "React-jsi"
s.dependency "React-Core/RCTImageHeaders"
s.dependency "React-RCTNetwork"
add_dependency(s, "React-RCTFBReactNativeSpec")
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
add_dependency(s, "React-NativeModulesApple")
add_rn_third_party_dependencies(s)
add_rncore_dependency(s)
end

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
'use strict';
// This is a stub for flow to make it understand require('./icon.png')
// See metro/src/Bundler/index.js
const AssetRegistry = require('@react-native/assets-registry/registry');
const RelativeImageStub = (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);
// eslint-disable-next-line @react-native/monorepo/no-commonjs-exports
module.exports = RelativeImageStub;

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {HostComponent} from '../../src/private/types/HostComponent';
import type {ViewProps} from '../Components/View/ViewPropTypes';
import type {PartialViewConfig} from '../Renderer/shims/ReactNativeTypes';
import type {ColorValue} from '../StyleSheet/StyleSheet';
import type {ImageResizeMode} from './ImageResizeMode';
import * as NativeComponentRegistry from '../NativeComponent/NativeComponentRegistry';
type RCTTextInlineImageNativeProps = $ReadOnly<{
...ViewProps,
resizeMode?: ?ImageResizeMode,
src?: ?$ReadOnlyArray<?$ReadOnly<{uri?: ?string, ...}>>,
tintColor?: ?ColorValue,
headers?: ?{[string]: string},
}>;
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
uiViewClassName: 'RCTTextInlineImage',
bubblingEventTypes: {},
directEventTypes: {},
validAttributes: {
resizeMode: true,
src: true,
tintColor: {
process: require('../StyleSheet/processColor').default,
},
headers: true,
},
};
const TextInlineImage: HostComponent<RCTTextInlineImageNativeProps> =
NativeComponentRegistry.get<RCTTextInlineImageNativeProps>(
'RCTTextInlineImage',
() => __INTERNAL_VIEW_CONFIG,
);
export default TextInlineImage;

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {ImageURISource} from './ImageSource';
import Platform from '../Utilities/Platform';
type NativeImageSourceSpec = $ReadOnly<{
android?: string,
ios?: string,
default?: string,
// For more details on width and height, see
// https://reactnative.dev/docs/images#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
*
*/
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,
};
}
export default nativeImageSource;

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// Utilities for resolving an asset into a `source` for e.g. `Image`
import type {ResolvedAssetSource} from './AssetSourceResolver';
import typeof AssetSourceResolverT from './AssetSourceResolver';
import type {ImageSource} from './ImageSource';
import SourceCode from '../NativeModules/specs/NativeSourceCode';
const AssetSourceResolver: AssetSourceResolverT =
require('./AssetSourceResolver').default;
const {pickScale} = require('./AssetUtils');
const AssetRegistry = require('@react-native/assets-registry/registry');
type CustomSourceTransformer = (
resolver: AssetSourceResolver,
) => ?ResolvedAssetSource;
let _customSourceTransformers: Array<CustomSourceTransformer> = [];
let _serverURL: ?string;
let _scriptURL: ?string;
let _sourceCodeScriptURL: ?string;
function getSourceCodeScriptURL(): ?string {
if (_sourceCodeScriptURL != null) {
return _sourceCodeScriptURL;
}
_sourceCodeScriptURL = SourceCode.getConstants().scriptURL;
return _sourceCodeScriptURL;
}
function getDevServerURL(): ?string {
if (_serverURL === undefined) {
const sourceCodeScriptURL = getSourceCodeScriptURL();
const match = 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 {
let normalizedScriptURL = scriptURL;
if (normalizedScriptURL != null) {
if (normalizedScriptURL.startsWith('assets://')) {
// android: running from within assets, no offline path to use
return null;
}
normalizedScriptURL = normalizedScriptURL.substring(
0,
normalizedScriptURL.lastIndexOf('/') + 1,
);
if (!normalizedScriptURL.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.
normalizedScriptURL = 'file://' + normalizedScriptURL;
}
}
return normalizedScriptURL;
}
function getScriptURL(): ?string {
if (_scriptURL === undefined) {
_scriptURL = _coerceLocalScriptURL(getSourceCodeScriptURL());
}
return _scriptURL;
}
/**
* `transformer` can optionally be used to apply a custom transformation when
* resolving an asset source. This methods overrides all other custom transformers
* that may have been previously registered.
*/
function setCustomSourceTransformer(
transformer: CustomSourceTransformer,
): void {
_customSourceTransformers = [transformer];
}
/**
* Adds a `transformer` into the chain of custom source transformers, which will
* be applied in the order registered, until one returns a non-null value.
*/
function addCustomSourceTransformer(
transformer: CustomSourceTransformer,
): void {
_customSourceTransformers.push(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: ?ImageSource): ?ResolvedAssetSource {
if (source == null || typeof source === 'object') {
// $FlowFixMe[incompatible-exact] `source` doesn't exactly match `ResolvedAssetSource`
// $FlowFixMe[incompatible-type] `source` doesn't exactly match `ResolvedAssetSource`
return source;
}
const asset = AssetRegistry.getAssetByID(source);
if (!asset) {
return null;
}
const resolver = new AssetSourceResolver(
getDevServerURL(),
getScriptURL(),
asset,
);
// Apply (chained) custom source transformers, if any
if (_customSourceTransformers) {
for (const customSourceTransformer of _customSourceTransformers) {
const transformedSource = customSourceTransformer(resolver);
if (transformedSource != null) {
return transformedSource;
}
}
}
return resolver.defaultAsset();
}
resolveAssetSource.pickScale = pickScale;
resolveAssetSource.setCustomSourceTransformer = setCustomSourceTransformer;
resolveAssetSource.addCustomSourceTransformer = addCustomSourceTransformer;
export default resolveAssetSource;