From b16dc457542056771f6cbd18e7dc7afbe65a57f2 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 30 Aug 2023 19:02:37 +0200 Subject: [PATCH] Fix the orientation of sent images (#1190) * Fix the orientation of sent images --------- Co-authored-by: Benoit Marty --- changelog.d/1135.bugfix | 1 + .../libraries/androidutils/bitmap/Bitmap.kt | 19 +++++++------------ .../mediaupload/AndroidMediaPreProcessor.kt | 7 +++++++ .../libraries/mediaupload/ImageCompressor.kt | 11 +++++++---- 4 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 changelog.d/1135.bugfix diff --git a/changelog.d/1135.bugfix b/changelog.d/1135.bugfix new file mode 100644 index 0000000000..2b963c7732 --- /dev/null +++ b/changelog.d/1135.bugfix @@ -0,0 +1 @@ +Fix the orientation of sent images. diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt index 6f8aa76d03..c3b7e3110e 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt @@ -22,7 +22,6 @@ import android.graphics.Matrix import androidx.core.graphics.scale import androidx.exifinterface.media.ExifInterface import java.io.File -import java.io.InputStream import kotlin.math.min fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { @@ -32,13 +31,6 @@ fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int } } -/** - * Reads the EXIF metadata from the [inputStream] and rotates the current [Bitmap] to match it. - * @return The resulting [Bitmap] or `null` if no metadata was found. - */ -fun Bitmap.rotateToMetadataOrientation(inputStream: InputStream): Result = - runCatching { rotateToMetadataOrientation(this, ExifInterface(inputStream)) } - /** * Scales the current [Bitmap] to fit the ([maxWidth], [maxHeight]) bounds while keeping aspect ratio. * @throws IllegalStateException if [maxWidth] or [maxHeight] <= 0. @@ -77,8 +69,11 @@ fun BitmapFactory.Options.calculateInSampleSize(desiredWidth: Int, desiredHeight return inSampleSize } -private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInterface): Bitmap { - val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) +/** + * Decodes the [inputStream] into a [Bitmap] and applies the needed rotation based on [orientation]. + * This orientation value must be one of `ExifInterface.ORIENTATION_*` constants. + */ +fun Bitmap.rotateToMetadataOrientation(orientation: Int): Bitmap { val matrix = Matrix() when (orientation) { ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) @@ -94,8 +89,8 @@ private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInter matrix.preRotate(90f) matrix.preScale(-1f, 1f) } - else -> return bitmap + else -> return this } - return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 8ff40fae39..9fc160252b 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -119,10 +119,17 @@ class AndroidMediaPreProcessor @Inject constructor( private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { suspend fun processImageWithCompression(): MediaUploadInfo { + // Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail. + val orientation = contentResolver.openInputStream(uri).use { input -> + val exifInterface = input?.let { ExifInterface(it) } + exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + } ?: ExifInterface.ORIENTATION_UNDEFINED + val compressionResult = contentResolver.openInputStream(uri).use { input -> imageCompressor.compressToTmpFile( inputStream = requireNotNull(input), resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE), + orientation = orientation, ).getOrThrow() } val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt index ab30f67b65..a619a27bd9 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.mediaupload import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import androidx.exifinterface.media.ExifInterface import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation @@ -37,17 +38,18 @@ class ImageCompressor @Inject constructor( /** * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode], then writes it into a - * temporary file using the passed [format] and [desiredQuality]. + * temporary file using the passed [format], [orientation] and [desiredQuality]. * @return a [Result] containing the resulting [ImageCompressionResult] with the temporary [File] and some metadata. */ suspend fun compressToTmpFile( inputStream: InputStream, resizeMode: ResizeMode, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, + orientation: Int = ExifInterface.ORIENTATION_UNDEFINED, desiredQuality: Int = 80, ): Result = withContext(Dispatchers.IO) { runCatching { - val compressedBitmap = compressToBitmap(inputStream, resizeMode).getOrThrow() + val compressedBitmap = compressToBitmap(inputStream, resizeMode, orientation).getOrThrow() // Encode bitmap to the destination temporary file val tmpFile = context.createTmpFile(extension = "jpeg") tmpFile.outputStream().use { @@ -63,19 +65,20 @@ class ImageCompressor @Inject constructor( } /** - * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode]. + * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode] and [orientation]. * @return a [Result] containing the resulting [Bitmap]. */ fun compressToBitmap( inputStream: InputStream, resizeMode: ResizeMode, + orientation: Int, ): Result = runCatching { BufferedInputStream(inputStream).use { input -> val options = BitmapFactory.Options() calculateDecodingScale(input, resizeMode, options) val decodedBitmap = BitmapFactory.decodeStream(input, null, options) ?: error("Decoding Bitmap from InputStream failed") - val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(input).getOrThrow() + val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(orientation) if (resizeMode is ResizeMode.Strict) { rotatedBitmap.resizeToMax(resizeMode.maxWidth, resizeMode.maxHeight) } else {