Fix portrait image metadata when uploading without media optimization (#6362)

* fix(media): preserve image orientation metadata without optimization

* style: linting
This commit is contained in:
Gianluca Iavicoli 2026-04-08 11:01:54 +02:00 committed by GitHub
parent d38f483cd1
commit 523ede744a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 67 additions and 12 deletions

View file

@ -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(),
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<Long, Long> {
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<Long, Long> {
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
}

View file

@ -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,