Skip to content

Commit ff84256

Browse files
zoontekreact-native-bot
authored andcommitted
Fix Image.getSize returning downsampled dimensions on Android (#56736)
Summary: `Image.getSize` and `getSizeWithHeaders` call Fresco's `fetchDecodedImage`, which returns a `CloseableImage` whose width / height reflect the bitmap after Fresco's automatic downsampling (typically capped at the screen size). For sources larger than the screen this returned the wrong dimensions. ex: a 4000x3000 image on a 1920x1080 device came back as 1920x1440. Switch to `fetchEncodedImage` and read the dimensions from the parsed image metadata (JPEG / PNG / WebP / HEIF headers), so the values are the true source dimensions. Swap width / height for 90°/270° EXIF rotation to match the iOS behavior in `RCTImageLoader`. Fixes #33498 Closes facebook/fresco#2236 ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [ANDROID] [FIXED] - `Image.getSize` and `Image.getSizeWithHeaders` now return the true source dimensions instead of Fresco's downsampled values Pull Request resolved: #56736 Test Plan: In the RNTester app, add: ```ts useEffect(() => { Image.getSize( 'https://images.pexels.com/photos/842711/pexels-photo-842711.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2', (width, height) => { console.log({width, height}); }, ); }, []); ``` ### Before <img width="811" height="467" alt="before" src="https://github.com/user-attachments/assets/6fb401ed-cf5a-4496-bcc1-4cfd243bc0b7" /> ### After <img width="811" height="467" alt="after" src="https://github.com/user-attachments/assets/dc809d5f-c554-45d6-a3a3-7712bf39ac44" /> Reviewed By: javache Differential Revision: D104533593 Pulled By: Abbondanzo fbshipit-source-id: f087d528474c40cffbf24eb1e3958b4b18fb118e
1 parent 1d8d46b commit ff84256

1 file changed

Lines changed: 59 additions & 62 deletions

File tree

  • packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/image/ImageLoaderModule.kt

Lines changed: 59 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@
77

88
package com.facebook.react.modules.image
99

10+
import android.media.ExifInterface
1011
import android.net.Uri
1112
import android.util.SparseArray
1213
import com.facebook.common.executors.CallerThreadExecutor
14+
import com.facebook.common.memory.PooledByteBuffer
1315
import com.facebook.common.references.CloseableReference
1416
import com.facebook.datasource.BaseDataSubscriber
1517
import com.facebook.datasource.DataSource
1618
import com.facebook.datasource.DataSubscriber
1719
import com.facebook.drawee.backends.pipeline.Fresco
1820
import com.facebook.fbreact.specs.NativeImageLoaderAndroidSpec
21+
import com.facebook.imagepipeline.common.RotationOptions
1922
import com.facebook.imagepipeline.core.ImagePipeline
20-
import com.facebook.imagepipeline.image.CloseableImage
23+
import com.facebook.imagepipeline.image.EncodedImage
2124
import com.facebook.imagepipeline.request.ImageRequest
2225
import com.facebook.imagepipeline.request.ImageRequestBuilder
2326
import com.facebook.react.bridge.GuardedAsyncTask
@@ -82,39 +85,13 @@ internal class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventL
8285
return
8386
}
8487
val source = ImageSource(reactApplicationContext, uriString)
85-
val request: ImageRequest = ImageRequestBuilder.newBuilderWithSource(source.uri).build()
86-
val dataSource: DataSource<CloseableReference<CloseableImage>> =
87-
this.imagePipeline.fetchDecodedImage(request, this.callerContext)
88-
val dataSubscriber: DataSubscriber<CloseableReference<CloseableImage>> =
89-
object : BaseDataSubscriber<CloseableReference<CloseableImage>>() {
90-
override fun onNewResultImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
91-
if (!dataSource.isFinished) {
92-
return
93-
}
94-
val ref = dataSource.result
95-
if (ref != null) {
96-
try {
97-
val image: CloseableImage = ref.get()
98-
val sizes = buildReadableMap {
99-
put("width", image.width)
100-
put("height", image.height)
101-
}
102-
promise.resolve(sizes)
103-
} catch (e: Exception) {
104-
promise.reject(ERROR_GET_SIZE_FAILURE, e)
105-
} finally {
106-
CloseableReference.closeSafely(ref)
107-
}
108-
} else {
109-
promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image")
110-
}
111-
}
112-
113-
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
114-
promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.failureCause)
115-
}
116-
}
117-
dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance())
88+
val request: ImageRequest =
89+
ImageRequestBuilder.newBuilderWithSource(source.uri)
90+
.setRotationOptions(RotationOptions.disableRotation())
91+
.build()
92+
val dataSource: DataSource<CloseableReference<PooledByteBuffer>> =
93+
this.imagePipeline.fetchEncodedImage(request, this.callerContext)
94+
dataSource.subscribe(createSizeSubscriber(promise), CallerThreadExecutor.getInstance())
11895
}
11996

12097
/**
@@ -134,41 +111,61 @@ internal class ImageLoaderModule : NativeImageLoaderAndroidSpec, LifecycleEventL
134111
val source = ImageSource(reactApplicationContext, uriString)
135112
val imageRequestBuilder: ImageRequestBuilder =
136113
ImageRequestBuilder.newBuilderWithSource(source.uri)
114+
.setRotationOptions(RotationOptions.disableRotation())
137115
val request: ImageRequest =
138116
ReactNetworkImageRequest.fromBuilderWithHeaders(imageRequestBuilder, headers)
139-
val dataSource: DataSource<CloseableReference<CloseableImage>> =
140-
this.imagePipeline.fetchDecodedImage(request, this.callerContext)
141-
val dataSubscriber: DataSubscriber<CloseableReference<CloseableImage>> =
142-
object : BaseDataSubscriber<CloseableReference<CloseableImage>>() {
143-
override fun onNewResultImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
144-
if (!dataSource.isFinished) {
145-
return
146-
}
147-
val ref = dataSource.result
148-
if (ref != null) {
149-
try {
150-
val image: CloseableImage = ref.get()
151-
val sizes = buildReadableMap {
152-
put("width", image.width)
153-
put("height", image.height)
154-
}
155-
promise.resolve(sizes)
156-
} catch (e: Exception) {
157-
promise.reject(ERROR_GET_SIZE_FAILURE, e)
158-
} finally {
159-
CloseableReference.closeSafely(ref)
117+
val dataSource: DataSource<CloseableReference<PooledByteBuffer>> =
118+
this.imagePipeline.fetchEncodedImage(request, this.callerContext)
119+
dataSource.subscribe(createSizeSubscriber(promise), CallerThreadExecutor.getInstance())
120+
}
121+
122+
private fun createSizeSubscriber(
123+
promise: Promise
124+
): DataSubscriber<CloseableReference<PooledByteBuffer>> =
125+
object : BaseDataSubscriber<CloseableReference<PooledByteBuffer>>() {
126+
override fun onNewResultImpl(dataSource: DataSource<CloseableReference<PooledByteBuffer>>) {
127+
if (!dataSource.isFinished) {
128+
return
129+
}
130+
val ref = dataSource.result
131+
if (ref != null) {
132+
var encodedImage: EncodedImage? = null
133+
try {
134+
encodedImage = EncodedImage(ref)
135+
// Swap width and height when the image's EXIF orientation swaps the X/Y axes
136+
// (90°/270° rotations, or transpose/transverse), so the values reflect the
137+
// visible dimensions, matching iOS behavior.
138+
val rotated =
139+
encodedImage.rotationAngle == 90 ||
140+
encodedImage.rotationAngle == 270 ||
141+
encodedImage.exifOrientation == ExifInterface.ORIENTATION_TRANSPOSE ||
142+
encodedImage.exifOrientation == ExifInterface.ORIENTATION_TRANSVERSE
143+
val width = if (rotated) encodedImage.height else encodedImage.width
144+
val height = if (rotated) encodedImage.width else encodedImage.height
145+
if (width < 0 || height < 0) {
146+
promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image")
147+
return
148+
}
149+
val sizes = buildReadableMap {
150+
put("width", width)
151+
put("height", height)
160152
}
161-
} else {
162-
promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image")
153+
promise.resolve(sizes)
154+
} catch (e: Exception) {
155+
promise.reject(ERROR_GET_SIZE_FAILURE, e)
156+
} finally {
157+
encodedImage?.close()
158+
CloseableReference.closeSafely(ref)
163159
}
160+
} else {
161+
promise.reject(ERROR_GET_SIZE_FAILURE, "Failed to get the size of the image")
164162
}
163+
}
165164

166-
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
167-
promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.failureCause)
168-
}
165+
override fun onFailureImpl(dataSource: DataSource<CloseableReference<PooledByteBuffer>>) {
166+
promise.reject(ERROR_GET_SIZE_FAILURE, dataSource.failureCause)
169167
}
170-
dataSource.subscribe(dataSubscriber, CallerThreadExecutor.getInstance())
171-
}
168+
}
172169

173170
/**
174171
* Prefetches the given image to the Fresco image disk cache.

0 commit comments

Comments
 (0)