From 523ede744a035caef438311c073bd926b0b5bbd1 Mon Sep 17 00:00:00 2001 From: Gianluca Iavicoli Date: Wed, 8 Apr 2026 11:01:54 +0200 Subject: [PATCH] Fix portrait image metadata when uploading without media optimization (#6362) * fix(media): preserve image orientation metadata without optimization * style: linting --- .../impl/AndroidMediaPreProcessor.kt | 54 ++++++++++++++----- .../impl/AndroidMediaPreProcessorTest.kt | 25 +++++++++ 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index 2b1883619e..2ab1cd0619 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -195,18 +195,16 @@ class AndroidMediaPreProcessor( file = file, mimeType = mimeType, ) - val imageInfo = contentResolver.openInputStream(uri).use { input -> - val bitmap = BitmapFactory.decodeStream(input, null, null)!! - ImageInfo( - width = bitmap.width.toLong(), - height = bitmap.height.toLong(), - mimetype = mimeType, - size = file.length(), - thumbnailInfo = thumbnailResult?.info, - thumbnailSource = null, - blurhash = thumbnailResult?.blurhash, - ) - } + val (width, height) = extractOrientedImageDimensions(file) + val imageInfo = ImageInfo( + width = width, + height = height, + mimetype = mimeType, + size = file.length(), + thumbnailInfo = thumbnailResult?.info, + thumbnailSource = null, + blurhash = thumbnailResult?.blurhash, + ) removeSensitiveImageMetadata(file) return MediaUploadInfo.Image( file = file, @@ -354,6 +352,23 @@ class AndroidMediaPreProcessor( return contentResolver.openInputStream(uri)?.use { createTmpFileWithInput(it) } ?: error("Could not copy the contents of $uri to a temporary file") } + + private fun extractOrientedImageDimensions(file: File): Pair { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(file.path, options) + + val rawWidth = options.outWidth.toLong() + val rawHeight = options.outHeight.toLong() + val orientation = tryOrNull { + ExifInterface(file).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + } ?: ExifInterface.ORIENTATION_UNDEFINED + + return orientedImageDimensions( + rawWidth = rawWidth, + rawHeight = rawHeight, + orientation = orientation, + ) + } } private fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailResult: ThumbnailResult?) = ImageInfo( @@ -371,3 +386,18 @@ private fun MediaMetadataRetriever.extractDuration(): Duration { val durationInMs = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L return durationInMs.milliseconds } + +internal fun orientedImageDimensions(rawWidth: Long, rawHeight: Long, orientation: Int): Pair { + return if (orientation.rotatesRightAngle()) { + rawHeight to rawWidth + } else { + rawWidth to rawHeight + } +} + +private fun Int.rotatesRightAngle(): Boolean { + return this == ExifInterface.ORIENTATION_ROTATE_90 || + this == ExifInterface.ORIENTATION_ROTATE_270 || + this == ExifInterface.ORIENTATION_TRANSPOSE || + this == ExifInterface.ORIENTATION_TRANSVERSE +} diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt index f4b4e7d4a5..57726ac5a2 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt @@ -12,6 +12,7 @@ import android.content.Context import android.net.Uri import android.os.Build import androidx.core.net.toUri +import androidx.exifinterface.media.ExifInterface import androidx.test.platform.app.InstrumentationRegistry import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.file.TemporaryUriDeleter @@ -42,6 +43,30 @@ import kotlin.time.Duration @RunWith(RobolectricTestRunner::class) class AndroidMediaPreProcessorTest { + @Test + fun `orientedImageDimensions swaps width and height for 90 degree exif orientation`() { + val (width, height) = orientedImageDimensions( + rawWidth = 4032, + rawHeight = 2268, + orientation = ExifInterface.ORIENTATION_ROTATE_90, + ) + + assertThat(width).isEqualTo(2268) + assertThat(height).isEqualTo(4032) + } + + @Test + fun `orientedImageDimensions keeps width and height for upright exif orientation`() { + val (width, height) = orientedImageDimensions( + rawWidth = 4032, + rawHeight = 2268, + orientation = ExifInterface.ORIENTATION_NORMAL, + ) + + assertThat(width).isEqualTo(4032) + assertThat(height).isEqualTo(2268) + } + private suspend fun TestScope.process( asset: Asset, mediaOptimizationConfig: MediaOptimizationConfig,