feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

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.
*
* @flow strict
* @format
*/
'use strict';
module.exports = require('@react-native/assets-registry/registry');

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.
*
* @flow strict-local
* @format
*/
'use strict';
export type ResolvedAssetSource = {|
+__packager_asset: boolean,
+width: ?number,
+height: ?number,
+uri: string,
+scale: number,
|};
import type {PackagerAsset} from '@react-native/assets-registry/registry';
const PixelRatio = require('../Utilities/PixelRatio').default;
const Platform = require('../Utilities/Platform');
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;
}
class AssetSourceResolver {
serverUrl: ?string;
// where the jsbundle is being run from
jsbundleUrl: ?string;
// the asset to resolve
asset: PackagerAsset;
constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
this.serverUrl = serverUrl;
this.jsbundleUrl = jsbundleUrl;
this.asset = asset;
}
isLoadedFromServer(): boolean {
return !!this.serverUrl;
}
isLoadedFromFileSystem(): boolean {
return this.jsbundleUrl != null && this.jsbundleUrl?.startsWith('file://');
}
defaultAsset(): ResolvedAssetSource {
if (this.isLoadedFromServer()) {
return this.assetServerURL();
}
if (Platform.OS === 'android') {
return this.isLoadedFromFileSystem()
? this.drawableFolderInBundle()
: this.resourceIdentifierWithoutScale();
} else {
return this.scaledAssetURLNearBundle();
}
}
/**
* Returns an absolute URL which can be used to fetch the asset
* from the devserver
*/
assetServerURL(): ResolvedAssetSource {
invariant(this.serverUrl != 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;
}
module.exports = 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,321 @@
/**
* 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 {ImageStyleProp} from '../StyleSheet/StyleSheet';
import type {RootTag} from '../Types/RootTagTypes';
import type {AbstractImageAndroid, ImageAndroid} from './ImageTypes.flow';
import flattenStyle from '../StyleSheet/flattenStyle';
import StyleSheet from '../StyleSheet/StyleSheet';
import TextAncestor from '../Text/TextAncestor';
import ImageAnalyticsTagContext from './ImageAnalyticsTagContext';
import {
unstable_getImageComponentDecorator,
useWrapRefWithImageAttachedCallbacks,
} from './ImageInjection';
import {getImageSourcesFromImageProps} from './ImageSourceUtils';
import {convertObjectFitToResizeMode} from './ImageUtils';
import ImageViewNativeComponent from './ImageViewNativeComponent';
import NativeImageLoaderAndroid from './NativeImageLoaderAndroid';
import resolveAssetSource from './resolveAssetSource';
import TextInlineImageNativeComponent from './TextInlineImageNativeComponent';
import * as React 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 {
NativeImageLoaderAndroid.getSize(url)
.then(function (sizes) {
success(sizes.width, sizes.height);
})
.catch(
failure ||
function () {
console.warn('Failed to get size for image: ' + url);
},
);
}
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it
* with the ability to provide the headers for the request
*
* See https://reactnative.dev/docs/image#getsizewithheaders
*/
function getSizeWithHeaders(
url: string,
headers: {[string]: string, ...},
success: (width: number, height: number) => void,
failure?: (error: mixed) => void,
): void {
NativeImageLoaderAndroid.getSizeWithHeaders(url, headers)
.then(function (sizes) {
success(sizes.width, sizes.height);
})
.catch(
failure ||
function () {
console.warn('Failed to get size for image: ' + url);
},
);
}
function 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);
}
/**
* 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: AbstractImageAndroid = React.forwardRef(
(props, forwardedRef) => {
let source = getImageSourcesFromImageProps(props) || {
uri: undefined,
width: undefined,
height: undefined,
};
const defaultSource = resolveAssetSource(props.defaultSource);
const loadingIndicatorSource = resolveAssetSource(
props.loadingIndicatorSource,
);
if (props.children) {
throw new Error(
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
);
}
if (props.defaultSource != 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;
let sources;
if (Array.isArray(source)) {
style = flattenStyle<ImageStyleProp>([styles.base, props.style]);
sources = source;
} else {
const {uri} = source;
const width = source.width ?? props.width;
const height = source.height ?? props.height;
style = flattenStyle<ImageStyleProp>([
{width, height},
styles.base,
props.style,
]);
sources = [source];
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
}
const {height, width, ...restProps} = props;
const {onLoadStart, onLoad, onLoadEnd, onError} = props;
const nativeProps = {
...restProps,
style,
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError),
src: sources,
/* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
* when making Flow check .android.js files. */
headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}),
defaultSrc: 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,
},
};
const objectFit = style?.objectFit
? convertObjectFitToResizeMode(style.objectFit)
: null;
const resizeMode =
objectFit || props.resizeMode || style?.resizeMode || 'cover';
const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);
return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
const nativePropsWithAnalytics =
analyticTag !== null
? {
...nativeProps,
internal_analyticTag: analyticTag,
}
: nativeProps;
return (
<TextAncestor.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}
/>
);
}}
</TextAncestor.Consumer>
);
}}
</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: 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',
},
});
module.exports = Image;

View File

@@ -0,0 +1,383 @@
/**
* 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 {NativeMethods} 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';
/**
* @see ImagePropsIOS.onProgress
*/
export interface ImageProgressEventDataIOS {
loaded: number;
total: number;
}
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: NativeSyntheticEvent<ImageProgressEventDataIOS>) => 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.
*/
resizeMethod?: 'auto' | 'resize' | 'scale' | 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;
export interface ImageLoadEventData {
source: {
height: number;
width: number;
uri: string;
};
}
export interface ImageErrorEventData {
error: any;
}
/**
* @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: NativeSyntheticEvent<ImageErrorEventData>) => void)
| undefined;
/**
* Invoked when load completes successfully
* { source: { uri, height, width } }.
*/
onLoad?:
| ((event: NativeSyntheticEvent<ImageLoadEventData>) => 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.
*/
resizeMode?: ImageResizeMode | undefined;
/**
* 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.
*
* More details about `resize` and `scale` can be found at http://frescolib.org/docs/resizing-rotating.html.
*
* @platform android
*/
resizeMethod?: 'auto' | 'resize' | 'scale' | 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;
}
declare class ImageComponent extends React.Component<ImageProps> {}
declare const ImageBase: Constructor<NativeMethods> & typeof ImageComponent;
export class Image extends ImageBase {
static getSize(
uri: string,
success: (width: number, height: number) => void,
failure?: (error: any) => void,
): any;
static getSizeWithHeaders(
uri: string,
headers: {[index: string]: string},
success: (width: number, height: number) => void,
failure?: (error: any) => void,
): any;
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<NativeMethods> &
typeof ImageBackgroundComponent;
export class ImageBackground extends ImageBackgroundBase {
resizeMode: ImageResizeMode;
getSize(
uri: string,
success: (width: number, height: number) => void,
failure: (error: any) => void,
): any;
prefetch(url: string): any;
abortPrefetch?(requestId: number): void;
queryCache?(
urls: string[],
): Promise<{[url: string]: 'memory' | 'disk' | 'disk/memory'}>;
}

View File

@@ -0,0 +1,255 @@
/**
* 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 {ImageStyle, ImageStyleProp} from '../StyleSheet/StyleSheet';
import type {RootTag} from '../Types/RootTagTypes';
import type {AbstractImageIOS, ImageIOS} from './ImageTypes.flow';
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 {
NativeImageLoaderIOS.getSize(uri)
.then(([width, height]) => success(width, height))
.catch(
failure ||
function () {
console.warn('Failed to get size for image ' + uri);
},
);
}
function getSizeWithHeaders(
uri: string,
headers: {[string]: string, ...},
success: (width: number, height: number) => void,
failure?: (error: mixed) => void,
): void {
NativeImageLoaderIOS.getSizeWithHeaders(uri, headers)
.then(function (sizes) {
success(sizes.width, sizes.height);
})
.catch(
failure ||
function () {
console.warn('Failed to get size for image: ' + uri);
},
);
}
function 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 = React.forwardRef((props, forwardedRef) => {
const source = getImageSourcesFromImageProps(props) || {
uri: undefined,
width: undefined,
height: undefined,
};
let sources;
let style: ImageStyle;
if (Array.isArray(source)) {
style =
flattenStyle<ImageStyleProp>([styles.base, props.style]) ||
({}: ImageStyle);
sources = source;
} else {
const {uri} = source;
const width = source.width ?? props.width;
const height = source.height ?? props.height;
style =
flattenStyle<ImageStyleProp>([
{width, height},
styles.base,
props.style,
]) || ({}: ImageStyle);
sources = [source];
if (uri === '') {
console.warn('source.uri should not be an empty string');
}
}
const objectFit =
style.objectFit != null
? convertObjectFitToResizeMode(style.objectFit)
: null;
const resizeMode =
objectFit || props.resizeMode || style.resizeMode || 'cover';
const tintColor = props.tintColor ?? style.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,
height,
src,
width,
...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,
};
const accessibilityLabel = props['aria-label'] ?? props.accessibilityLabel;
const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef);
return (
<ImageAnalyticsTagContext.Consumer>
{analyticTag => {
return (
<ImageViewNativeComponent
accessibilityState={_accessibilityState}
{...restProps}
accessible={props.alt !== undefined ? true : props.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',
},
});
module.exports = Image;

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.
*
* @flow strict-local
* @format
*/
import type {Image} from './ImageTypes.flow';
declare module.exports: Image;

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';
type ContextType = ?string;
const Context: React.Context<ContextType> =
React.createContext<ContextType>(null);
if (__DEV__) {
Context.displayName = 'ImageAnalyticsTagContext';
}
export default Context;

View File

@@ -0,0 +1,116 @@
/**
* 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 {HostComponent} from '../Renderer/shims/ReactNativeTypes';
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';
/**
* 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 | React$ElementRef<
React$AbstractComponent<
ViewProps,
React.ElementRef<HostComponent<ViewProps>>,
>,
>,
) => {
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>
);
}
}
module.exports = ImageBackground;

View File

@@ -0,0 +1,87 @@
/**
* 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
* @flow strict-local
*/
import type {
AbstractImageAndroid,
AbstractImageIOS,
Image as ImageComponent,
} 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 = React.ElementRef<ImageComponent>;
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,
);
}

View File

@@ -0,0 +1,284 @@
/**
* 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 {LayoutEvent, SyntheticEvent} from '../Types/CoreEventTypes';
import typeof Image from './Image';
import type {ImageSource} from './ImageSource';
import type {Node, Ref} from 'react';
export type ImageLoadEvent = SyntheticEvent<
$ReadOnly<{|
source: $ReadOnly<{|
width: number,
height: number,
uri: string,
|}>,
|}>,
>;
type IOSImageProps = $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: SyntheticEvent<$ReadOnly<{|loaded: number, total: number|}>>,
) => void,
|}>;
type AndroidImageProps = $ReadOnly<{|
loadingIndicatorSource?: ?(number | $ReadOnly<{|uri: string|}>),
progressiveRenderingEnabled?: ?boolean,
fadeDuration?: ?number,
|}>;
export type ImageProps = {|
...$Diff<ViewProps, $ReadOnly<{|style: ?ViewStyleProp|}>>,
...IOSImageProps,
...AndroidImageProps,
/**
* When true, indicates the image is an accessibility element.
*
* See https://reactnative.dev/docs/image#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: SyntheticEvent<
$ReadOnly<{|
error: string,
|}>,
>,
) => void,
/**
* Invoked on mount and layout changes with
* `{nativeEvent: {layout: {x, y, width, height}}}`.
*
* See https://reactnative.dev/docs/image#onlayout
*/
onLayout?: ?(event: LayoutEvent) => 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,
/**
* See https://reactnative.dev/docs/image#resizemethod
*/
resizeMethod?: ?('auto' | 'resize' | 'scale'),
/**
* The image source (either a remote URL or a local file resource).
*
* See https://reactnative.dev/docs/image#source
*/
source?: ?ImageSource,
/**
* See https://reactnative.dev/docs/image#style
*/
style?: ?ImageStyleProp,
/**
* A string indicating which referrer to use when fetching the resource.
* Similar to referrerpolicy from HTML.
*
* See https://reactnative.dev/docs/image#referrerpolicy
*/
referrerPolicy?: ?(
| 'no-referrer'
| 'no-referrer-when-downgrade'
| 'origin'
| 'origin-when-cross-origin'
| 'same-origin'
| 'strict-origin'
| 'strict-origin-when-cross-origin'
| 'unsafe-url'
),
/**
* Determines how to resize the image when the frame doesn't match the raw
* image dimensions.
*
* See https://reactnative.dev/docs/image#resizemode
*/
resizeMode?: ?('cover' | 'contain' | 'stretch' | 'repeat' | 'center'),
/**
* A unique identifier for this element to be used in UI Automation
* testing scripts.
*
* See https://reactnative.dev/docs/image#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 ImageBackgroundProps = $ReadOnly<{|
...ImageProps,
children?: 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?: Ref<Image>,
|}>;

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.
*
* @format
*/
export type ImageResizeMode =
| 'cover'
| 'contain'
| 'stretch'
| 'repeat'
| 'center';
/**
* @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;
}

View File

@@ -0,0 +1,36 @@
/**
* 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';

View File

@@ -0,0 +1,76 @@
/**
* 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.
*
* @platform ios
*/
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;

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.
*
* @platform ios
*/
+cache?: ?('default' | 'reload' | 'force-cache' | 'only-if-cached');
/**
* `width` and `height` can be specified if known at build time, in which case
* these will be used to set the default `<Image/>` component dimensions.
*/
+width?: ?number;
+height?: ?number;
/**
* `scale` is used to indicate the scale factor of the image. Defaults to 1.0 if
* unspecified, meaning that one image pixel equates to one display point / DIP.
*/
+scale?: ?number;
}
export type ImageSource =
| number
| 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,80 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageProps} from './ImageProps';
import resolveAssetSource from './resolveAssetSource';
/**
* A function which returns the appropriate value for image source
* by resolving the `source`, `src` and `srcSet` props.
*/
export function getImageSourcesFromImageProps(
imageProps: ImageProps,
): ?ResolvedAssetSource | $ReadOnlyArray<{uri: string, ...}> {
let source = resolveAssetSource(imageProps.source);
let sources;
const {crossOrigin, referrerPolicy, src, srcSet, width, height} = imageProps;
const headers: {[string]: string} = {};
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: headers, scale, uri, width, height});
}
}
});
if (shouldUseSrcForDefaultScale && src != null) {
sourceList.push({
headers: 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 {
sources = source;
}
return sources;
}

View File

@@ -0,0 +1,71 @@
/**
* 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 {RootTag} from '../Types/RootTagTypes';
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageProps as ImagePropsType} from './ImageProps';
import type {ImageSource} from './ImageSource';
import typeof ImageViewNativeComponent from './ImageViewNativeComponent';
import typeof TextInlineImageNativeComponent from './TextInlineImageNativeComponent';
import * as React from 'react';
type ImageComponentStaticsIOS = $ReadOnly<{
getSize: (
uri: string,
success: (width: number, height: number) => void,
failure?: (error: mixed) => void,
) => void,
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<{[string]: 'memory' | 'disk' | 'disk/memory', ...}>,
resolveAssetSource(source: ImageSource): ?ResolvedAssetSource,
}>;
type ImageComponentStaticsAndroid = $ReadOnly<{
...ImageComponentStaticsIOS,
abortPrefetch(requestId: number): void,
}>;
export type AbstractImageAndroid = React.AbstractComponent<
ImagePropsType,
| React.ElementRef<TextInlineImageNativeComponent>
| React.ElementRef<ImageViewNativeComponent>,
>;
export type ImageAndroid = AbstractImageAndroid & ImageComponentStaticsAndroid;
export type AbstractImageIOS = React.AbstractComponent<
ImagePropsType,
React.ElementRef<ImageViewNativeComponent>,
>;
export type ImageIOS = AbstractImageIOS & ImageComponentStaticsIOS;
export type Image = ImageIOS | ImageAndroid;
export type {ImageProps} from './ImageProps';

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.
*
* @flow strict
* @format
*/
type ResizeMode = 'cover' | 'contain' | 'stretch' | 'repeat' | 'center';
export function convertObjectFitToResizeMode(objectFit: string): ResizeMode {
const objectFitMap = {
contain: 'contain',
cover: 'cover',
fill: 'stretch',
'scale-down': 'contain',
};
return objectFitMap[objectFit];
}

View File

@@ -0,0 +1,170 @@
/**
* 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 {ViewProps} from '../Components/View/ViewPropTypes';
import type {
HostComponent,
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 {ElementRef} from 'react';
import * as NativeComponentRegistry from '../NativeComponent/NativeComponentRegistry';
import {ConditionallyIgnoredEventHandlers} from '../NativeComponent/ViewConfigIgnore';
import codegenNativeCommands from '../Utilities/codegenNativeCommands';
import Platform from '../Utilities/Platform';
type Props = $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},
defaultSrc?: ?string,
loadingIndicatorSrc?: ?string,
}>;
interface NativeCommands {
+setIsVisible_EXPERIMENTAL: (
viewRef: ElementRef<HostComponent<mixed>>,
isVisible: boolean,
) => 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,
internal_analyticTag: true,
resizeMode: true,
tintColor: {
process: require('../StyleSheet/processColor').default,
},
borderBottomLeftRadius: true,
borderTopLeftRadius: true,
resizeMethod: 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,
defaultSrc: 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'),
},
defaultSource: {
process: require('./resolveAssetSource'),
},
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<Props> =
NativeComponentRegistry.get<Props>(
'RCTImageView',
() => __INTERNAL_VIEW_CONFIG,
);
export default ImageViewNativeComponent;

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.
*
* @flow strict
* @format
*/
export * from '../../src/private/specs/modules/NativeImageEditor';
import NativeImageEditor from '../../src/private/specs/modules/NativeImageEditor';
export default NativeImageEditor;

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.
*
* @flow strict-local
* @format
*/
export * from '../../src/private/specs/modules/NativeImageLoaderAndroid';
import NativeImageLoaderAndroid from '../../src/private/specs/modules/NativeImageLoaderAndroid';
export default NativeImageLoaderAndroid;

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.
*
* @flow strict-local
* @format
*/
export * from '../../src/private/specs/modules/NativeImageLoaderIOS';
import NativeImageLoaderIOS from '../../src/private/specs/modules/NativeImageLoaderIOS';
export default NativeImageLoaderIOS;

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.
*
* @flow strict
* @format
*/
export * from '../../src/private/specs/modules/NativeImageStoreAndroid';
import NativeImageStoreAndroid from '../../src/private/specs/modules/NativeImageStoreAndroid';
export default NativeImageStoreAndroid;

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.
*
* @flow strict
* @format
*/
export * from '../../src/private/specs/modules/NativeImageStoreIOS';
import NativeImageStoreIOS from '../../src/private/specs/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) {
if (progressHandler) {
progressHandler(1, 1);
}
completionHandler(nil, image);
} else {
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
RCTLogWarn(@"%@", message);
completionHandler(RCTErrorWithMessage(message), nil);
}
return nil;
}
- (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") || !strcmp(header, "GIF89a");
}
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
{
RCTAnimatedImage *image = [[RCTAnimatedImage alloc] initWithData:imageData scale:scale];
if (!image) {
completionHandler(nil, nil);
return ^{
};
}
completionHandler(nil, image);
return ^{
};
}
- (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,100 @@
/*
* 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))) {
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, 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) {
return inputImage;
}
buffer2.data = malloc(bytes);
if (!buffer2.data) {
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) {
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,113 @@
/*
* 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,30 @@
/*
* 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.
*/
/**
* The image loader (i.e. RCTImageLoader) implement this to declare whether image performance should be logged.
*/
@protocol RCTImageLoaderLoggableProtocol
/**
* 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,51 @@
/*
* 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>
RCT_EXTERN BOOL RCTImageLoadingPerfInstrumentationEnabled(void);
RCT_EXTERN void RCTEnableImageLoadingPerfInstrumentation(BOOL enabled);
@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,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/RCTShadowView.h>
@interface RCTImageShadowView : RCTShadowView
@end

View File

@@ -0,0 +1,24 @@
/*
* 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>
@implementation RCTImageShadowView
- (BOOL)isYogaLeafNode
{
return YES;
}
- (BOOL)canHaveSubviews
{
return NO;
}
@end

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,296 @@
/*
* 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";
static BOOL gImageStoreManagerStorageQueueEnabled = NO;
void RCTEnableImageStoreManagerStorageQueue(BOOL enabled)
{
gImageStoreManagerStorageQueueEnabled = enabled;
}
@interface RCTImageStoreManager () <NativeImageStoreIOSSpec>
@end
@implementation RCTImageStoreManager {
NSMutableDictionary<NSString *, NSData *> *_store;
NSUInteger _id;
dispatch_queue_t _storageQueue;
}
@synthesize methodQueue = _methodQueue;
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (instancetype)init
{
if (self = [super init]) {
if (gImageStoreManagerStorageQueueEnabled) {
_storageQueue = dispatch_queue_create("com.facebook.react.imagestoremanager.storage", DISPATCH_QUEUE_SERIAL);
}
}
return self;
}
- (dispatch_queue_t)_getAsyncQueue
{
return gImageStoreManagerStorageQueueEnabled ? _storageQueue : _methodQueue;
}
- (float)handlerPriority
{
return 1;
}
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)(void))block
{
dispatch_async([self _getAsyncQueue], ^{
[self removeImageForTag:imageTag];
if (block) {
block();
}
});
}
- (NSString *)_storeImageData:(NSData *)imageData
{
RCTAssertThread([self _getAsyncQueue], @"Must be called on RCTImageStoreManager thread");
if (!_store) {
_store = [NSMutableDictionary new];
_id = 0;
}
NSString *imageTag = [NSString stringWithFormat:@"%@://%tu", RCTImageStoreURLScheme, _id++];
_store[imageTag] = imageData;
return imageTag;
}
- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block
{
RCTAssertParam(block);
dispatch_async([self _getAsyncQueue], ^{
block([self _storeImageData:imageData]);
});
}
- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block
{
RCTAssertParam(block);
dispatch_async([self _getAsyncQueue], ^{
block(self->_store[imageTag]);
});
}
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
{
RCTAssertParam(block);
dispatch_async([self _getAsyncQueue], ^{
NSString *imageTag = [self _storeImageData:RCTGetImageData(image, 0.75)];
dispatch_async(dispatch_get_main_queue(), ^{
block(imageTag);
});
});
}
RCT_EXPORT_METHOD(removeImageForTag : (NSString *)imageTag)
{
[_store removeObjectForKey:imageTag];
}
RCT_EXPORT_METHOD(hasImageForTag : (NSString *)imageTag callback : (RCTResponseSenderBlock)callback)
{
callback(@[ @(_store[imageTag] != nil) ]);
}
// TODO (#5906496): Name could be more explicit - something like getBase64EncodedDataForTag:?
RCT_EXPORT_METHOD(getBase64ForTag
: (NSString *)imageTag successCallback
: (RCTResponseSenderBlock)successCallback errorCallback
: (RCTResponseSenderBlock)errorCallback)
{
NSData *imageData = _store[imageTag];
if (!imageData) {
errorCallback(
@[ RCTJSErrorFromNSError(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag])) ]);
return;
}
// Dispatching to a background thread to perform base64 encoding
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
successCallback(@[ [imageData base64EncodedStringWithOptions:0] ]);
});
}
RCT_EXPORT_METHOD(addImageFromBase64
: (NSString *)base64String successCallback
: (RCTResponseSenderBlock)successCallback errorCallback
: (RCTResponseSenderBlock)errorCallback)
{
// Dispatching to a background thread to perform base64 decoding
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
if (imageData) {
dispatch_async([self _getAsyncQueue], ^{
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([self _getAsyncQueue], ^{
if (cancelled->load()) {
return;
}
NSString *imageTag = request.URL.absoluteString;
NSData *imageData = self->_store[imageTag];
if (!imageData) {
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]);
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (!sourceRef) {
NSError *error =
RCTErrorWithMessage([NSString stringWithFormat:@"Unable to decode data for imageTag: %@", imageTag]);
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
return;
}
CFStringRef UTI = CGImageSourceGetType(sourceRef);
CFRelease(sourceRef);
NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
MIMEType:MIMEType
expectedContentLength:imageData.length
textEncodingName:nil];
CFRelease(UTI);
[delegate URLRequest:cancellationBlock didReceiveResponse:response];
[delegate URLRequest:cancellationBlock didReceiveData:imageData];
[delegate URLRequest:cancellationBlock didCompleteWithError:nil];
});
return cancellationBlock;
}
- (void)cancelRequest:(id)requestToken
{
if (requestToken) {
((void (^)(void))requestToken)();
}
}
- (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([self _getAsyncQueue], ^{
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([self _getAsyncQueue], ^{
imageData = self->_store[imageTag];
});
return [UIImage imageWithData:imageData];
}
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
{
RCTAssertParam(block);
dispatch_async([self _getAsyncQueue], ^{
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,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 <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;
/**
* Send a network request to load the request URL. The method should call the
* progressHandler (if applicable) and the completionHandler when the request
* has finished. The method should also return a cancellation block, if
* applicable.
*/
- (nullable RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(RCTResizeMode)resizeMode
progressHandler:(RCTImageLoaderProgressBlock)progressHandler
partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;
@optional
/**
* If more than one RCTImageURLLoader responds YES to `-canLoadImageURL:`
* then `loaderPriority` is used to determine which one to use. The loader
* with the highest priority will be selected. Default priority is zero. If
* two or more valid loaders have the same priority, the selection order is
* undefined.
*/
- (float)loaderPriority;
/**
* If the loader must be called on the serial url cache queue, and whether the completion
* block should be dispatched off the main thread. If this is NO, the loader will be
* called from the main queue. Defaults to YES.
*
* Use with care: disabling scheduling will reduce RCTImageLoader's ability to throttle
* network requests.
*/
- (BOOL)requiresScheduling;
/**
* If images loaded by the loader should be cached in the decoded image cache.
* Defaults to YES.
*/
- (BOOL)shouldCacheLoadedImages;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,78 @@
/*
* 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
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,387 @@
/*
* 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 <tgmath.h>
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
{
return ceil(value * scale) / scale;
}
static CGFloat RCTFloorValue(CGFloat value, CGFloat scale)
{
return floor(value * scale) / scale;
}
static CGSize RCTCeilSize(CGSize size, CGFloat scale)
{
return (CGSize){RCTCeilValue(size.width, scale), RCTCeilValue(size.height, scale)};
}
static CGImagePropertyOrientation CGImagePropertyOrientationFromUIImageOrientation(UIImageOrientation imageOrientation)
{
// see https://stackoverflow.com/a/6699649/496389
switch (imageOrientation) {
case UIImageOrientationUp:
return kCGImagePropertyOrientationUp;
case UIImageOrientationDown:
return kCGImagePropertyOrientationDown;
case UIImageOrientationLeft:
return kCGImagePropertyOrientationLeft;
case UIImageOrientationRight:
return kCGImagePropertyOrientationRight;
case UIImageOrientationUpMirrored:
return kCGImagePropertyOrientationUpMirrored;
case UIImageOrientationDownMirrored:
return kCGImagePropertyOrientationDownMirrored;
case UIImageOrientationLeftMirrored:
return kCGImagePropertyOrientationLeftMirrored;
case UIImageOrientationRightMirrored:
return kCGImagePropertyOrientationRightMirrored;
default:
return kCGImagePropertyOrientationUp;
}
}
CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, CGFloat destScale, RCTResizeMode resizeMode)
{
if (CGSizeEqualToSize(destSize, CGSizeZero)) {
// Assume we require the largest size available
return (CGRect){CGPointZero, sourceSize};
}
CGFloat aspect = sourceSize.width / sourceSize.height;
// If only one dimension in destSize is non-zero (for example, an Image
// with `flex: 1` whose height is indeterminate), calculate the unknown
// dimension based on the aspect ratio of sourceSize
if (destSize.width == 0) {
destSize.width = destSize.height * aspect;
}
if (destSize.height == 0) {
destSize.height = destSize.width / aspect;
}
// Calculate target aspect ratio if needed
CGFloat targetAspect = 0.0;
if (resizeMode != RCTResizeModeCenter && resizeMode != RCTResizeModeStretch) {
targetAspect = destSize.width / destSize.height;
if (aspect == targetAspect) {
resizeMode = RCTResizeModeStretch;
}
}
switch (resizeMode) {
case RCTResizeModeStretch:
case RCTResizeModeRepeat:
return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)};
case RCTResizeModeContain:
if (targetAspect <= aspect) { // target is taller than content
sourceSize.width = destSize.width;
sourceSize.height = sourceSize.width / aspect;
} else { // target is wider than content
sourceSize.height = destSize.height;
sourceSize.width = sourceSize.height * aspect;
}
return (CGRect){
{
RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale),
RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale),
},
RCTCeilSize(sourceSize, destScale)};
case RCTResizeModeCover:
if (targetAspect <= aspect) { // target is taller than content
sourceSize.height = destSize.height;
sourceSize.width = sourceSize.height * aspect;
destSize.width = destSize.height * targetAspect;
return (CGRect){
{RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale), 0}, RCTCeilSize(sourceSize, destScale)};
} else { // target is wider than content
sourceSize.width = destSize.width;
sourceSize.height = sourceSize.width / aspect;
destSize.height = destSize.width / targetAspect;
return (CGRect){
{0, RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale)},
RCTCeilSize(sourceSize, destScale)};
}
case RCTResizeModeCenter:
// Make sure the image is not clipped by the target.
if (sourceSize.height > destSize.height) {
sourceSize.width = destSize.width;
sourceSize.height = sourceSize.width / aspect;
}
if (sourceSize.width > destSize.width) {
sourceSize.height = destSize.height;
sourceSize.width = sourceSize.height * aspect;
}
return (CGRect){
{
RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale),
RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale),
},
RCTCeilSize(sourceSize, destScale)};
}
}
CGAffineTransform RCTTransformFromTargetRect(CGSize sourceSize, CGRect targetRect)
{
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, targetRect.origin.x, targetRect.origin.y);
transform = CGAffineTransformScale(
transform, targetRect.size.width / sourceSize.width, targetRect.size.height / sourceSize.height);
return transform;
}
CGSize RCTTargetSize(
CGSize sourceSize,
CGFloat sourceScale,
CGSize destSize,
CGFloat destScale,
RCTResizeMode resizeMode,
BOOL allowUpscaling)
{
switch (resizeMode) {
case RCTResizeModeCenter:
return RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size;
case RCTResizeModeStretch:
if (!allowUpscaling) {
CGFloat scale = sourceScale / destScale;
destSize.width = MIN(sourceSize.width * scale, destSize.width);
destSize.height = MIN(sourceSize.height * scale, destSize.height);
}
return RCTCeilSize(destSize, destScale);
default: {
// Get target size
CGSize size = RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size;
if (!allowUpscaling) {
// return sourceSize if target size is larger
if (sourceSize.width * sourceScale < size.width * destScale) {
return sourceSize;
}
}
return size;
}
}
}
BOOL RCTUpscalingRequired(
CGSize sourceSize,
CGFloat sourceScale,
CGSize destSize,
CGFloat destScale,
RCTResizeMode resizeMode)
{
if (CGSizeEqualToSize(destSize, CGSizeZero)) {
// Assume we require the largest size available
return YES;
}
// Precompensate for scale
CGFloat scale = sourceScale / destScale;
sourceSize.width *= scale;
sourceSize.height *= scale;
// Calculate aspect ratios if needed (don't bother if resizeMode == stretch)
CGFloat aspect = 0.0, targetAspect = 0.0;
if (resizeMode != 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:
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);
CGFloat maxPixelSize =
fmax(fmin(sourceSize.width, targetPixelSize.width), fmin(sourceSize.height, targetPixelSize.height));
NSDictionary<NSString *, NSNumber *> *options = @{
(id)kCGImageSourceShouldAllowFloat : @YES,
(id)kCGImageSourceCreateThumbnailWithTransform : @YES,
(id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
(id)kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize),
};
// Get thumbnail
CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options);
CFRelease(sourceRef);
if (!imageRef) {
return nil;
}
// Return image
UIImage *image = [UIImage imageWithCGImage:imageRef scale:destScale orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
return image;
}
NSDictionary<NSString *, id> *__nullable RCTGetImageMetadata(NSData *data)
{
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!sourceRef) {
return nil;
}
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
CFRelease(sourceRef);
return (__bridge_transfer id)imageProperties;
}
NSData *__nullable RCTGetImageData(UIImage *image, float quality)
{
CGImageRef cgImage = image.CGImage;
if (!cgImage) {
return NULL;
}
NSMutableDictionary *properties = [[NSMutableDictionary alloc] initWithDictionary:@{
(id)kCGImagePropertyOrientation : @(CGImagePropertyOrientationFromUIImageOrientation(image.imageOrientation))
}];
CGImageDestinationRef destination;
CFMutableDataRef imageData = CFDataCreateMutable(NULL, 0);
if (RCTImageHasAlpha(cgImage)) {
// get png data
destination = CGImageDestinationCreateWithData(imageData, kUTTypePNG, 1, NULL);
} else {
// get jpeg data
destination = CGImageDestinationCreateWithData(imageData, kUTTypeJPEG, 1, NULL);
[properties setValue:@(quality) forKey:(id)kCGImageDestinationLossyCompressionQuality];
}
if (!destination) {
CFRelease(imageData);
return NULL;
}
CGImageDestinationAddImage(destination, cgImage, (__bridge CFDictionaryRef)properties);
if (!CGImageDestinationFinalize(destination)) {
CFRelease(imageData);
imageData = NULL;
}
CFRelease(destination);
return (__bridge_transfer NSData *)imageData;
}
UIImage *__nullable RCTTransformImage(UIImage *image, CGSize destSize, CGFloat destScale, CGAffineTransform transform)
{
if (destSize.width <= 0 | destSize.height <= 0 || destScale <= 0) {
return nil;
}
BOOL opaque = !RCTImageHasAlpha(image.CGImage);
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,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.
*/
#import <React/RCTResizeMode.h>
#import <React/RCTView.h>
#import <UIKit/UIKit.h>
@class RCTBridge;
@class RCTImageSource;
@interface RCTImageView : RCTView
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
@property (nonatomic, assign) UIEdgeInsets capInsets;
@property (nonatomic, strong) UIImage *defaultImage;
@property (nonatomic, assign) UIImageRenderingMode renderingMode;
@property (nonatomic, copy) NSArray<RCTImageSource *> *imageSources;
@property (nonatomic, assign) CGFloat blurRadius;
@property (nonatomic, assign) RCTResizeMode resizeMode;
@property (nonatomic, copy) NSString *internal_analyticTag;
@end

View File

@@ -0,0 +1,504 @@
/*
* 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>
/**
* 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),
@"height" : @(source.size.height),
};
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:source.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(), (RCTResizeMode)self.contentMode, YES);
// Don't reload if the current image or target image size is close enough
if (!RCTShouldReloadImageForSizeChange(imageSize, idealSize) ||
!RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) {
return;
}
// Don't reload if the current image size is the maximum size of either the pending image source or image source
CGSize imageSourceSize = (_imageSource ? _imageSource : _pendingImageSource).size;
if (imageSize.width * imageScale == imageSourceSize.width * _imageSource.scale &&
imageSize.height * imageScale == imageSourceSize.height * _imageSource.scale) {
return;
}
RCTLogInfo(
@"Reloading image %@ as size %@", _imageSource.request.URL.absoluteString, NSStringFromCGSize(idealSize));
// If the existing image or an image being loaded are not the right
// size, reload the asset in case there is a better size available.
_targetSize = idealSize;
[self reloadImage];
}
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
if (_needsReload) {
[self reloadImage];
}
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (!self.window) {
// Cancel loading the image if we've moved offscreen. In addition to helping
// prioritise image requests that are actually on-screen, this removes
// requests that have gotten "stuck" from the queue, unblocking other images
// from loading.
// 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

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/RCTViewManager.h>
@interface RCTImageViewManager : RCTViewManager
@end

View File

@@ -0,0 +1,116 @@
/*
* 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>
#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

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) {
if (progressHandler) {
progressHandler(1, 1);
}
completionHandler(nil, image);
} else {
NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL];
RCTLogWarn(@"%@", message);
completionHandler(RCTErrorWithMessage(message), nil);
}
return nil;
}
- (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,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.
*/
#import <React/RCTConvert.h>
typedef NS_ENUM(NSInteger, RCTResizeMode) {
RCTResizeModeCover = UIViewContentModeScaleAspectFill,
RCTResizeModeContain = UIViewContentModeScaleAspectFit,
RCTResizeModeStretch = UIViewContentModeScaleToFill,
RCTResizeModeCenter = UIViewContentModeCenter,
RCTResizeModeRepeat = -1, // Use negative values to avoid conflicts with iOS enum values.
};
@interface RCTConvert (RCTResizeMode)
+ (RCTResizeMode)RCTResizeMode:(id)json;
@end

View File

@@ -0,0 +1,24 @@
/*
* 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),
}),
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,342 @@
/*
* 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 () <CALayerDelegate, RCTDisplayRefreshable>
@property (nonatomic, assign) NSUInteger maxBufferSize;
@property (nonatomic, strong, readwrite) UIImage *currentFrame;
@property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
@property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
@property (nonatomic, assign) NSUInteger totalFrameCount;
@property (nonatomic, assign) NSUInteger totalLoopCount;
@property (nonatomic, strong) UIImage<RCTAnimatedImage> *animatedImage;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *frameBuffer;
@property (nonatomic, assign) NSTimeInterval currentTime;
@property (nonatomic, assign) BOOL bufferMiss;
@property (nonatomic, assign) NSUInteger maxBufferCount;
@property (nonatomic, strong) NSOperationQueue *fetchQueue;
@property (nonatomic, strong) dispatch_semaphore_t lock;
@property (nonatomic, assign) CGFloat animatedImageScale;
@property (nonatomic, strong) CADisplayLink *displayLink;
@end
@implementation RCTUIImageViewAnimated
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.lock = dispatch_semaphore_create(1);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
- (void)resetAnimatedImage
{
self.animatedImage = nil;
self.totalFrameCount = 0;
self.totalLoopCount = 0;
self.currentFrame = nil;
self.currentFrameIndex = 0;
self.currentLoopCount = 0;
self.currentTime = 0;
self.bufferMiss = NO;
self.maxBufferCount = 0;
self.animatedImageScale = 1;
[_fetchQueue cancelAllOperations];
_fetchQueue = nil;
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
[_frameBuffer removeAllObjects];
_frameBuffer = nil;
dispatch_semaphore_signal(self.lock);
}
- (void)setImage:(UIImage *)image
{
if (self.image == image) {
return;
}
[self stop];
[self resetAnimatedImage];
if ([image respondsToSelector:@selector(animatedImageFrameAtIndex:)]) {
NSUInteger animatedImageFrameCount = ((UIImage<RCTAnimatedImage> *)image).animatedImageFrameCount;
// In case frame count is 0, there is no reason to continue.
if (animatedImageFrameCount == 0) {
return;
}
self.animatedImage = (UIImage<RCTAnimatedImage> *)image;
self.totalFrameCount = animatedImageFrameCount;
// Get the current frame and loop count.
self.totalLoopCount = self.animatedImage.animatedImageLoopCount;
self.animatedImageScale = image.scale;
self.currentFrame = image;
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame;
dispatch_semaphore_signal(self.lock);
// Calculate max buffer size
[self calculateMaxBufferCount];
if ([self paused]) {
[self start];
}
[self.layer setNeedsDisplay];
} else {
super.image = image;
}
}
#pragma mark - Private
- (NSOperationQueue *)fetchQueue
{
if (!_fetchQueue) {
_fetchQueue = [NSOperationQueue 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;
}
#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;
}
}
// Update the current frame
UIImage *currentFrame;
UIImage *fetchFrame;
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
currentFrame = self.frameBuffer[@(currentFrameIndex)];
fetchFrame = currentFrame ? self.frameBuffer[@(nextFrameIndex)] : nil;
dispatch_semaphore_signal(self.lock);
BOOL bufferFull = NO;
if (currentFrame) {
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
// Remove the frame buffer if need
if (self.frameBuffer.count > self.maxBufferCount) {
self.frameBuffer[@(currentFrameIndex)] = nil;
}
// Check whether we can stop fetch
if (self.frameBuffer.count == totalFrameCount) {
bufferFull = YES;
}
dispatch_semaphore_signal(self.lock);
self.currentFrame = currentFrame;
self.currentFrameIndex = nextFrameIndex;
self.bufferMiss = NO;
[self.layer setNeedsDisplay];
} else {
self.bufferMiss = YES;
}
// Update the loop count when last frame rendered
if (nextFrameIndex == 0 && !self.bufferMiss) {
// Update the loop count
self.currentLoopCount++;
// if reached the max loop count, stop animating, 0 means loop indefinitely
NSUInteger maxLoopCount = self.totalLoopCount;
if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) {
[self stop];
return;
}
}
// Check if we should prefetch next frame or current frame
NSUInteger fetchFrameIndex;
if (self.bufferMiss) {
// When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
fetchFrameIndex = currentFrameIndex;
} else {
// Or, most cases, the decode speed is faster than render speed, we fetch next frame
fetchFrameIndex = nextFrameIndex;
}
if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) {
// Prefetch next frame in background queue
UIImage<RCTAnimatedImage> *animatedImage = self.animatedImage;
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
UIImage *frame = [animatedImage animatedImageFrameAtIndex:fetchFrameIndex];
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
self.frameBuffer[@(fetchFrameIndex)] = frame;
dispatch_semaphore_signal(self.lock);
}];
[self.fetchQueue addOperation:operation];
}
}
#pragma mark - CALayerDelegate
- (void)displayLayer:(CALayer *)layer
{
if (_currentFrame) {
layer.contentsScale = self.animatedImageScale;
layer.contents = (__bridge id)_currentFrame.CGImage;
} else {
[super displayLayer:layer];
}
}
#pragma mark - Util
- (void)calculateMaxBufferCount
{
NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage);
if (bytes == 0)
bytes = 1024;
NSUInteger max = 0;
if (self.maxBufferSize > 0) {
max = self.maxBufferSize;
} else {
// Calculate based on current memory, these factors are by experience
NSUInteger total = RCTDeviceTotalMemory();
NSUInteger free = RCTDeviceFreeMemory();
max = MIN((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,60 @@
# 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
folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
header_search_paths = [
"\"$(PODS_ROOT)/RCT-Folly\"",
"\"${PODS_ROOT}/Headers/Public/React-Codegen/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 = folly_compiler_flags + ' -Wno-nullability-completeness'
s.source = source
s.source_files = "*.{m,mm}"
s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
s.header_dir = "RCTImage"
s.pod_target_xcconfig = {
"USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
"HEADER_SEARCH_PATHS" => header_search_paths.join(' ')
}
s.framework = ["Accelerate", "UIKit"]
s.dependency "RCT-Folly", folly_version
s.dependency "RCTTypeSafety"
s.dependency "React-jsi"
s.dependency "React-Core/RCTImageHeaders"
s.dependency "React-RCTNetwork"
add_dependency(s, "React-Codegen")
add_dependency(s, "ReactCommon", :subspec => "turbomodule/core", :additional_framework_paths => ["react/nativemodule/core"])
add_dependency(s, "React-NativeModulesApple")
end

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.
*
* @format
* @flow strict
*/
'use strict';
// This is a stub for flow to make it understand require('./icon.png')
// See metro/src/Bundler/index.js
const AssetRegistry = require('@react-native/assets-registry/registry');
module.exports = (AssetRegistry.registerAsset({
__packager_asset: true,
fileSystemLocation: '/full/path/to/directory',
httpServerLocation: '/assets/full/path/to/directory',
width: 100,
height: 100,
scales: [1, 2, 3],
hash: 'nonsense',
name: 'icon',
type: 'png',
}): number);

View File

@@ -0,0 +1,51 @@
/**
* 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
* @flow strict-local
*/
'use strict';
import type {ViewProps} from '../Components/View/ViewPropTypes';
import type {
HostComponent,
PartialViewConfig,
} from '../Renderer/shims/ReactNativeTypes';
import type {ColorValue} from '../StyleSheet/StyleSheet';
import type {ImageResizeMode} from './ImageResizeMode';
import * as NativeComponentRegistry from '../NativeComponent/NativeComponentRegistry';
type NativeProps = $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<NativeProps> =
NativeComponentRegistry.get<NativeProps>(
'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,
};
}
module.exports = nativeImageSource;

View File

@@ -0,0 +1,143 @@
/**
* 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
* @flow strict-local
*/
// Utilities for resolving an asset into a `source` for e.g. `Image`
import type {ResolvedAssetSource} from './AssetSourceResolver';
import type {ImageSource} from './ImageSource';
import SourceCode from '../NativeModules/specs/NativeSourceCode';
const AssetSourceResolver = require('./AssetSourceResolver');
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-return] `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;
module.exports = resolveAssetSource;