From f8985575fe6827bbd2361c6b7aeea947a2c3dbe9 Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Fri, 8 May 2026 17:38:29 +0200 Subject: [PATCH 1/3] Fix Image.getSize returning downsampled dimensions on Android --- .../react/modules/image/ImageLoaderModule.kt | 108 ++++++++---------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt index d76da5ffc600..40200be2a65c 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt @@ -10,6 +10,7 @@ package com.facebook.react.modules.image import android.net.Uri import android.util.SparseArray import com.facebook.common.executors.CallerThreadExecutor +import com.facebook.common.memory.PooledByteBuffer import com.facebook.common.references.CloseableReference import com.facebook.datasource.BaseDataSubscriber import com.facebook.datasource.DataSource @@ -17,7 +18,7 @@ import com.facebook.datasource.DataSubscriber import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.fbreact.specs.NativeImageLoaderAndroidSpec import com.facebook.imagepipeline.core.ImagePipeline -import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.image.EncodedImage import com.facebook.imagepipeline.request.ImageRequest import com.facebook.imagepipeline.request.ImageRequestBuilder import com.facebook.react.bridge.GuardedAsyncTask @@ -83,38 +84,9 @@ internal class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventL } val source = ImageSource(reactApplicationContext, uriString) val request: ImageRequest = ImageRequestBuilder.newBuilderWithSource(source.uri).build() - val dataSource: DataSource> = - this.imagePipeline.fetchDecodedImage(request, this.callerContext) - val dataSubscriber: DataSubscriber> = - object : BaseDataSubscriber>() { - override fun onNewResultImpl(dataSource: DataSource>) { - if (!dataSource.isFinished) { - return - } - val ref = dataSource.result - if (ref != null) { - try { - val image: CloseableImage = ref.get() - val sizes = buildReadableMap { - put("width", image.width) - put("height", image.height) - } - promise.resolve(sizes) - } catch (e: Exception) { - promise.reject(ERROR_GET_SIZE_FAILURE, e) - } finally { - CloseableReference.closeSafely(ref) - } - } else { - promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image") - } - } - - override fun onFailureImpl(dataSource: DataSource>) { - promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.failureCause) - } - } - dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()) + val dataSource: DataSource> = + this.imagePipeline.fetchEncodedImage(request, this.callerContext) + dataSource.subscribe(createSizeSubscriber(promise), CallerThreadExecutor.getInstance()) } /** @@ -136,39 +108,53 @@ internal class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventL ImageRequestBuilder.newBuilderWithSource(source.uri) val request: ImageRequest = ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers) - val dataSource: DataSource> = - this.imagePipeline.fetchDecodedImage(request, this.callerContext) - val dataSubscriber: DataSubscriber> = - object : BaseDataSubscriber>() { - override fun onNewResultImpl(dataSource: DataSource>) { - if (!dataSource.isFinished) { - return - } - val ref = dataSource.result - if (ref != null) { - try { - val image: CloseableImage = ref.get() - val sizes = buildReadableMap { - put("width", image.width) - put("height", image.height) - } - promise.resolve(sizes) - } catch (e: Exception) { - promise.reject(ERROR_GET_SIZE_FAILURE, e) - } finally { - CloseableReference.closeSafely(ref) + val dataSource: DataSource> = + this.imagePipeline.fetchEncodedImage(request, this.callerContext) + dataSource.subscribe(createSizeSubscriber(promise), CallerThreadExecutor.getInstance()) + } + + private fun createSizeSubscriber( + promise: Promise + ): DataSubscriber> = + object : BaseDataSubscriber>() { + override fun onNewResultImpl(dataSource: DataSource>) { + if (!dataSource.isFinished) { + return + } + val ref = dataSource.result + if (ref != null) { + var encodedImage: EncodedImage? = null + try { + encodedImage = EncodedImage(ref) + // Swap width and height when the image is rotated 90 or 270 degrees so the + // values reflect the visible dimensions, matching iOS behavior. + val rotated = encodedImage.rotationAngle == 90 || encodedImage.rotationAngle == 270 + val width = if (rotated) encodedImage.height else encodedImage.width + val height = if (rotated) encodedImage.width else encodedImage.height + if (width < 0 || height < 0) { + promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image") + return + } + val sizes = buildReadableMap { + put("width", width) + put("height", height) } - } else { - promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image") + promise.resolve(sizes) + } catch (e: Exception) { + promise.reject(ERROR_GET_SIZE_FAILURE, e) + } finally { + encodedImage?.close() + CloseableReference.closeSafely(ref) } + } else { + promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image") } + } - override fun onFailureImpl(dataSource: DataSource>) { - promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.failureCause) - } + override fun onFailureImpl(dataSource: DataSource>) { + promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.failureCause) } - dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance()) - } + } /** * Prefetches the given image to the Fresco image disk cache. From e60e0508f454ef6af2c6aa2bee25c64d1f81a52c Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Mon, 11 May 2026 09:26:54 +0200 Subject: [PATCH 2/3] Add setRotationOptions(RotationOptions.disableRotation()) --- .../com/facebook/react/modules/image/ImageLoaderModule.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt index 40200be2a65c..dde4006c66df 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt @@ -17,6 +17,7 @@ import com.facebook.datasource.DataSource import com.facebook.datasource.DataSubscriber import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.fbreact.specs.NativeImageLoaderAndroidSpec +import com.facebook.imagepipeline.common.RotationOptions import com.facebook.imagepipeline.core.ImagePipeline import com.facebook.imagepipeline.image.EncodedImage import com.facebook.imagepipeline.request.ImageRequest @@ -83,7 +84,10 @@ internal class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventL return } val source = ImageSource(reactApplicationContext, uriString) - val request: ImageRequest = ImageRequestBuilder.newBuilderWithSource(source.uri).build() + val request: ImageRequest = + ImageRequestBuilder.newBuilderWithSource(source.uri) + .setRotationOptions(RotationOptions.disableRotation()) + .build() val dataSource: DataSource> = this.imagePipeline.fetchEncodedImage(request, this.callerContext) dataSource.subscribe(createSizeSubscriber(promise), CallerThreadExecutor.getInstance()) @@ -106,6 +110,7 @@ internal class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventL val source = ImageSource(reactApplicationContext, uriString) val imageRequestBuilder: ImageRequestBuilder = ImageRequestBuilder.newBuilderWithSource(source.uri) + .setRotationOptions(RotationOptions.disableRotation()) val request: ImageRequest = ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers) val dataSource: DataSource> = From f9477bb1cde47529f62ff5b1faf4d1ba76d95497 Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Mon, 11 May 2026 20:16:58 +0200 Subject: [PATCH 3/3] review --- .../react/modules/image/ImageLoaderModule.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt index dde4006c66df..509efc1f7b06 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt @@ -7,6 +7,7 @@ package com.facebook.react.modules.image +import android.media.ExifInterface import android.net.Uri import android.util.SparseArray import com.facebook.common.executors.CallerThreadExecutor @@ -131,9 +132,14 @@ internal class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventL var encodedImage: EncodedImage? = null try { encodedImage = EncodedImage(ref) - // Swap width and height when the image is rotated 90 or 270 degrees so the - // values reflect the visible dimensions, matching iOS behavior. - val rotated = encodedImage.rotationAngle == 90 || encodedImage.rotationAngle == 270 + // Swap width and height when the image's EXIF orientation swaps the X/Y axes + // (90°/270° rotations, or transpose/transverse), so the values reflect the + // visible dimensions, matching iOS behavior. + val rotated = + encodedImage.rotationAngle == 90 || + encodedImage.rotationAngle == 270 || + encodedImage.exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE || + encodedImage.exifOrientation == ExifInterface.ORIENTATION_TRANSVERSE val width = if (rotated) encodedImage.height else encodedImage.width val height = if (rotated) encodedImage.width else encodedImage.height if (width < 0 || height < 0) {