From 7c15762cc057b7c02b7da5444adca5de4e182745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 16 Jan 2026 17:16:20 +0100 Subject: [PATCH 1/8] fix(android): pass full image object to native side for `defaultSource` & `loadingIndicatorSrc` --- packages/react-native/Libraries/Image/Image.android.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-native/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js index 9ce0947ff9d117..5730e415013ff9 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -256,14 +256,14 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) { } if (defaultSource_ != null && defaultSource_.uri != null) { - nativeProps.defaultSource = defaultSource_.uri; + nativeProps.defaultSource = defaultSource_; } if ( loadingIndicatorSource_ != null && loadingIndicatorSource_.uri != null ) { - nativeProps.loadingIndicatorSrc = loadingIndicatorSource_.uri; + nativeProps.loadingIndicatorSrc = loadingIndicatorSource_; } if (ariaLabel != null) { @@ -395,9 +395,9 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) { /* $FlowFixMe[prop-missing](>=0.78.0 site=react_native_android_fb) This issue was found * when making Flow check .android.js files. */ headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}), - defaultSource: defaultSource ? defaultSource.uri : null, + defaultSource: defaultSource ? defaultSource : null, loadingIndicatorSrc: loadingIndicatorSource - ? loadingIndicatorSource.uri + ? loadingIndicatorSource : null, accessibilityLabel: props['aria-label'] ?? props.accessibilityLabel ?? props.alt, @@ -424,6 +424,8 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) { const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef); + console.log('Image props=', nativeProps); + return ( {analyticTag => { From 6f3e8d93c0720b19ed354a89291e222657bae351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 16 Jan 2026 17:17:56 +0100 Subject: [PATCH 2/8] fix(android): image w&h are not to be wrapped in size this is needed on ios but not android --- .../ReactCommon/react/renderer/imagemanager/primitives.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h index 0bae06a4138aa5..0efd42fd312f66 100644 --- a/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/imagemanager/primitives.h @@ -66,9 +66,8 @@ class ImageSource { imageSourceResult["scale"] = scale; folly::dynamic sizeResult = folly::dynamic::object(); - sizeResult["width"] = size.width; - sizeResult["height"] = size.height; - imageSourceResult["size"] = sizeResult; + imageSourceResult["width"] = size.width; + imageSourceResult["height"] = size.height; imageSourceResult["body"] = body; imageSourceResult["method"] = method; From 4a7f5c25209aa034e431b16b2081b0cb96246d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 16 Jan 2026 17:18:18 +0100 Subject: [PATCH 3/8] fix setting default source --- .../react/views/image/ReactImageManager.kt | 32 +++++++++++-- .../react/views/image/ReactImageView.kt | 45 +++++++++++-------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt index ec04d15097fe23..e96af9ad24572c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt @@ -12,8 +12,11 @@ import android.graphics.PorterDuff import com.facebook.common.logging.FLog import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.controller.AbstractDraweeControllerBuilder +import com.facebook.react.BuildConfig +import com.facebook.react.bridge.ReactNoCrashSoftException import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.SoftAssertions import com.facebook.react.common.ReactConstants import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.BackgroundStyleApplicator @@ -133,14 +136,35 @@ public constructor( } @ReactProp(name = "defaultSource") - public fun setDefaultSource(view: ReactImageView, source: String?) { - view.setDefaultSource(source) + public fun setDefaultSource(view: ReactImageView, source: ReadableMap?) { + if (source == null) { + view.setDefaultSource(null) + return + } + val imageSource = view.readableMapToImageSource(source, false) + if (!BuildConfig.DEBUG && !imageSource.isResource) { + throw ReactNoCrashSoftException( + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}", + ) + } + + view.setDefaultSource(imageSource.uri.toString()) } // In JS this is Image.props.loadingIndicatorSource.uri @ReactProp(name = "loadingIndicatorSrc") - public fun setLoadingIndicatorSource(view: ReactImageView, source: String?) { - view.setLoadingIndicatorSource(source) + public fun setLoadingIndicatorSource(view: ReactImageView, source: ReadableMap?) { + if (source == null) { + view.setLoadingIndicatorSource(null) + return + } + val imageSource = view.readableMapToImageSource(source, false) + if (!BuildConfig.DEBUG && !imageSource.isResource) { + throw ReactNoCrashSoftException( + "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}", + ) + } + view.setLoadingIndicatorSource(imageSource.uri.toString()) } @ReactProp(name = "borderColor", customType = "Color") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index ecbcc9b3083b24..63fe37ec4e0c20 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -273,6 +273,30 @@ public class ReactImageView( } } + // todo: this must live elsewhere i think + public fun readableMapToImageSource(source: ReadableMap, includeSize: Boolean = false): ImageSource { + val cacheControl = computeCacheControl(source.getString("cache")) + val uri = source.getString("uri") + var imageSource = if (includeSize) { + ImageSource( + context, + uri, + source.getDouble("width"), + source.getDouble("height"), + cacheControl, + ) + } else { + ImageSource(context, uri, cacheControl = cacheControl) + } + + if (Uri.EMPTY == imageSource.uri) { + warnImageSource(uri) + imageSource = getTransparentBitmapImageSource(context) + } + + return imageSource + } + public fun setSource(sources: ReadableArray?) { val tmpSources = mutableListOf() @@ -281,29 +305,12 @@ public class ReactImageView( } else if (sources.size() == 1) { // Optimize for the case where we have just one uri, case in which we don't need the sizes val source = checkNotNull(sources.getMap(0)) - val cacheControl = computeCacheControl(source.getString("cache")) - var imageSource = ImageSource(context, source.getString("uri"), cacheControl = cacheControl) - if (Uri.EMPTY == imageSource.uri) { - warnImageSource(source.getString("uri")) - imageSource = getTransparentBitmapImageSource(context) - } + val imageSource = readableMapToImageSource(source, includeSize = false) tmpSources.add(imageSource) } else { for (idx in 0 until sources.size()) { val source = sources.getMap(idx) ?: continue - val cacheControl = computeCacheControl(source.getString("cache")) - var imageSource = - ImageSource( - context, - source.getString("uri"), - source.getDouble("width"), - source.getDouble("height"), - cacheControl, - ) - if (Uri.EMPTY == imageSource.uri) { - warnImageSource(source.getString("uri")) - imageSource = getTransparentBitmapImageSource(context) - } + val imageSource = readableMapToImageSource(source, includeSize = true) tmpSources.add(imageSource) } } From 22c7f86108937b0b7418c5969c65e6b95e8f7e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 19 Jan 2026 19:10:27 +0100 Subject: [PATCH 4/8] change to soft assertion to not break current API --- .../react/views/image/ReactImageManager.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt index e96af9ad24572c..4737912f6a5a21 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt @@ -142,13 +142,11 @@ public constructor( return } val imageSource = view.readableMapToImageSource(source, false) - if (!BuildConfig.DEBUG && !imageSource.isResource) { - throw ReactNoCrashSoftException( - "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}", - ) - } + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}") - view.setDefaultSource(imageSource.uri.toString()) + view.setDefaultSource(imageSource.source) } // In JS this is Image.props.loadingIndicatorSource.uri @@ -159,11 +157,10 @@ public constructor( return } val imageSource = view.readableMapToImageSource(source, false) - if (!BuildConfig.DEBUG && !imageSource.isResource) { - throw ReactNoCrashSoftException( - "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}", - ) - } + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}") + view.setLoadingIndicatorSource(imageSource.uri.toString()) } From 45a78ef9c4823388b3733800d2d6068ef7839eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 19 Jan 2026 19:19:55 +0100 Subject: [PATCH 5/8] cleanup --- .../react/views/image/ReactImageManager.kt | 25 +----- .../react/views/image/ReactImageView.kt | 77 ++++++++++++------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt index 4737912f6a5a21..998093159c6c37 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt @@ -12,11 +12,8 @@ import android.graphics.PorterDuff import com.facebook.common.logging.FLog import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.controller.AbstractDraweeControllerBuilder -import com.facebook.react.BuildConfig -import com.facebook.react.bridge.ReactNoCrashSoftException import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.SoftAssertions import com.facebook.react.common.ReactConstants import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.BackgroundStyleApplicator @@ -137,31 +134,13 @@ public constructor( @ReactProp(name = "defaultSource") public fun setDefaultSource(view: ReactImageView, source: ReadableMap?) { - if (source == null) { - view.setDefaultSource(null) - return - } - val imageSource = view.readableMapToImageSource(source, false) - SoftAssertions.assertCondition( - !BuildConfig.DEBUG && !imageSource.isResource, - "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}") - - view.setDefaultSource(imageSource.source) + view.setDefaultSource(source) } // In JS this is Image.props.loadingIndicatorSource.uri @ReactProp(name = "loadingIndicatorSrc") public fun setLoadingIndicatorSource(view: ReactImageView, source: ReadableMap?) { - if (source == null) { - view.setLoadingIndicatorSource(null) - return - } - val imageSource = view.readableMapToImageSource(source, false) - SoftAssertions.assertCondition( - !BuildConfig.DEBUG && !imageSource.isResource, - "ReactImageView: Only local resources can be used as loading indicator image. Uri: ${imageSource.uri}") - - view.setLoadingIndicatorSource(imageSource.uri.toString()) + view.setLoadingIndicatorSource(source) } @ReactProp(name = "borderColor", customType = "Color") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 63fe37ec4e0c20..51339ee7c03790 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -44,9 +44,11 @@ import com.facebook.imagepipeline.request.ImageRequest import com.facebook.imagepipeline.request.ImageRequest.RequestLevel import com.facebook.imagepipeline.request.ImageRequestBuilder import com.facebook.imagepipeline.request.Postprocessor +import com.facebook.react.BuildConfig import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.SoftAssertions import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.annotations.VisibleForTesting import com.facebook.react.common.build.ReactBuildConfig @@ -273,30 +275,6 @@ public class ReactImageView( } } - // todo: this must live elsewhere i think - public fun readableMapToImageSource(source: ReadableMap, includeSize: Boolean = false): ImageSource { - val cacheControl = computeCacheControl(source.getString("cache")) - val uri = source.getString("uri") - var imageSource = if (includeSize) { - ImageSource( - context, - uri, - source.getDouble("width"), - source.getDouble("height"), - cacheControl, - ) - } else { - ImageSource(context, uri, cacheControl = cacheControl) - } - - if (Uri.EMPTY == imageSource.uri) { - warnImageSource(uri) - imageSource = getTransparentBitmapImageSource(context) - } - - return imageSource - } - public fun setSource(sources: ReadableArray?) { val tmpSources = mutableListOf() @@ -343,16 +321,36 @@ public class ReactImageView( } } - public fun setDefaultSource(name: String?) { - val newDefaultDrawable = ResourceDrawableIdHelper.getResourceDrawable(context, name) + public fun setDefaultSource(source: ReadableMap?) { + var newDefaultDrawable: Drawable? = null + if (source != null) { + val imageSource = readableMapToImageSource(source, false) + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}" + ) + + newDefaultDrawable = ResourceDrawableIdHelper.getResourceDrawable(context, imageSource.source) + } + if (defaultImageDrawable != newDefaultDrawable) { defaultImageDrawable = newDefaultDrawable isDirty = true } } - public fun setLoadingIndicatorSource(name: String?) { - val drawable = ResourceDrawableIdHelper.getResourceDrawable(context, name) + public fun setLoadingIndicatorSource(source: ReadableMap?) { + var drawable: Drawable? = null + if (source != null) { + val imageSource = readableMapToImageSource(source, false) + SoftAssertions.assertCondition( + !BuildConfig.DEBUG && !imageSource.isResource, + "ReactImageView: Only local resources can be used as default image. Uri: ${imageSource.uri}" + ) + + drawable = ResourceDrawableIdHelper.getResourceDrawable(context, imageSource.source) + } + val newLoadingIndicatorSource = drawable?.let { AutoRotateDrawable(it, 1000) } if (loadingImageDrawable != newLoadingIndicatorSource) { loadingImageDrawable = newLoadingIndicatorSource @@ -562,6 +560,29 @@ public class ReactImageView( private val isTiled: Boolean get() = tileMode != TileMode.CLAMP + private fun readableMapToImageSource(source: ReadableMap, includeSize: Boolean = false): ImageSource { + val cacheControl = computeCacheControl(source.getString("cache")) + val uri = source.getString("uri") + var imageSource = if (includeSize) { + ImageSource( + context, + uri, + source.getDouble("width"), + source.getDouble("height"), + cacheControl, + ) + } else { + ImageSource(context, uri, cacheControl = cacheControl) + } + + if (Uri.EMPTY == imageSource.uri) { + warnImageSource(uri) + imageSource = getTransparentBitmapImageSource(context) + } + + return imageSource + } + private fun setSourceImage() { imageSource = null if (sources.isEmpty()) { From 0a78d964dfca045800f4902dd02201521f0e713e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 19 Jan 2026 19:43:58 +0100 Subject: [PATCH 6/8] update flow type for loadingIndicatorSrc --- .../react-native/Libraries/Image/ImageViewNativeComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js index d1572c5de53dbb..44165b7eaf7852 100644 --- a/packages/react-native/Libraries/Image/ImageViewNativeComponent.js +++ b/packages/react-native/Libraries/Image/ImageViewNativeComponent.js @@ -40,7 +40,7 @@ type ImageHostComponentProps = Readonly<{ src?: ?ResolvedAssetSource | ?ReadonlyArray>, headers?: ?{[string]: string}, defaultSource?: ?ImageSource | ?string, - loadingIndicatorSrc?: ?string, + loadingIndicatorSrc?: ?ImageSource | ?string, }>; interface NativeCommands { From 985edfbbb9a0a1a3af76724c6c92a476073b6d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 20 Jan 2026 10:12:12 +0100 Subject: [PATCH 7/8] update fantom test local image is now referenced correctly as local --- .../react-native/Libraries/Image/__tests__/Image-itest.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Image/__tests__/Image-itest.js b/packages/react-native/Libraries/Image/__tests__/Image-itest.js index 38878826922722..4dd70dbbfce7aa 100644 --- a/packages/react-native/Libraries/Image/__tests__/Image-itest.js +++ b/packages/react-native/Libraries/Image/__tests__/Image-itest.js @@ -162,7 +162,9 @@ describe('', () => { root.getRenderedOutput({props: ['defaultSource']}).toJSX(), ).toEqual( , ); From 2d1b2f5708ba653a358741f041e6c3ef76113c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Tue, 20 Jan 2026 11:14:08 +0100 Subject: [PATCH 8/8] remove console log --- packages/react-native/Libraries/Image/Image.android.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-native/Libraries/Image/Image.android.js b/packages/react-native/Libraries/Image/Image.android.js index 5730e415013ff9..c687b581e871e6 100644 --- a/packages/react-native/Libraries/Image/Image.android.js +++ b/packages/react-native/Libraries/Image/Image.android.js @@ -424,8 +424,6 @@ if (ReactNativeFeatureFlags.reduceDefaultPropsInImage()) { const actualRef = useWrapRefWithImageAttachedCallbacks(forwardedRef); - console.log('Image props=', nativeProps); - return ( {analyticTag => {