Skip to content

Commit 1d3d1fe

Browse files
jmartinespbmarty
andauthored
Fix the orientation of sent images (#1190)
* Fix the orientation of sent images --------- Co-authored-by: Benoit Marty <[email protected]>
1 parent a435d3a commit 1d3d1fe

File tree

4 files changed

+22
-16
lines changed

4 files changed

+22
-16
lines changed

changelog.d/1135.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix the orientation of sent images.

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import android.graphics.Matrix
2222
import androidx.core.graphics.scale
2323
import androidx.exifinterface.media.ExifInterface
2424
import java.io.File
25-
import java.io.InputStream
2625
import kotlin.math.min
2726

2827
fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
@@ -32,13 +31,6 @@ fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int
3231
}
3332
}
3433

35-
/**
36-
* Reads the EXIF metadata from the [inputStream] and rotates the current [Bitmap] to match it.
37-
* @return The resulting [Bitmap] or `null` if no metadata was found.
38-
*/
39-
fun Bitmap.rotateToMetadataOrientation(inputStream: InputStream): Result<Bitmap> =
40-
runCatching { rotateToMetadataOrientation(this, ExifInterface(inputStream)) }
41-
4234
/**
4335
* Scales the current [Bitmap] to fit the ([maxWidth], [maxHeight]) bounds while keeping aspect ratio.
4436
* @throws IllegalStateException if [maxWidth] or [maxHeight] <= 0.
@@ -77,8 +69,11 @@ fun BitmapFactory.Options.calculateInSampleSize(desiredWidth: Int, desiredHeight
7769
return inSampleSize
7870
}
7971

80-
private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInterface): Bitmap {
81-
val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
72+
/**
73+
* Decodes the [inputStream] into a [Bitmap] and applies the needed rotation based on [orientation].
74+
* This orientation value must be one of `ExifInterface.ORIENTATION_*` constants.
75+
*/
76+
fun Bitmap.rotateToMetadataOrientation(orientation: Int): Bitmap {
8277
val matrix = Matrix()
8378
when (orientation) {
8479
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
@@ -94,8 +89,8 @@ private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInter
9489
matrix.preRotate(90f)
9590
matrix.preScale(-1f, 1f)
9691
}
97-
else -> return bitmap
92+
else -> return this
9893
}
9994

100-
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
95+
return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
10196
}

libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,17 @@ class AndroidMediaPreProcessor @Inject constructor(
119119
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
120120

121121
suspend fun processImageWithCompression(): MediaUploadInfo {
122+
// Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail.
123+
val orientation = contentResolver.openInputStream(uri).use { input ->
124+
val exifInterface = input?.let { ExifInterface(it) }
125+
exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
126+
} ?: ExifInterface.ORIENTATION_UNDEFINED
127+
122128
val compressionResult = contentResolver.openInputStream(uri).use { input ->
123129
imageCompressor.compressToTmpFile(
124130
inputStream = requireNotNull(input),
125131
resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE),
132+
orientation = orientation,
126133
).getOrThrow()
127134
}
128135
val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file)

libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package io.element.android.libraries.mediaupload
1919
import android.content.Context
2020
import android.graphics.Bitmap
2121
import android.graphics.BitmapFactory
22+
import androidx.exifinterface.media.ExifInterface
2223
import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize
2324
import io.element.android.libraries.androidutils.bitmap.resizeToMax
2425
import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation
@@ -37,17 +38,18 @@ class ImageCompressor @Inject constructor(
3738

3839
/**
3940
* Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode], then writes it into a
40-
* temporary file using the passed [format] and [desiredQuality].
41+
* temporary file using the passed [format], [orientation] and [desiredQuality].
4142
* @return a [Result] containing the resulting [ImageCompressionResult] with the temporary [File] and some metadata.
4243
*/
4344
suspend fun compressToTmpFile(
4445
inputStream: InputStream,
4546
resizeMode: ResizeMode,
4647
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
48+
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
4749
desiredQuality: Int = 80,
4850
): Result<ImageCompressionResult> = withContext(Dispatchers.IO) {
4951
runCatching {
50-
val compressedBitmap = compressToBitmap(inputStream, resizeMode).getOrThrow()
52+
val compressedBitmap = compressToBitmap(inputStream, resizeMode, orientation).getOrThrow()
5153
// Encode bitmap to the destination temporary file
5254
val tmpFile = context.createTmpFile(extension = "jpeg")
5355
tmpFile.outputStream().use {
@@ -63,19 +65,20 @@ class ImageCompressor @Inject constructor(
6365
}
6466

6567
/**
66-
* Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode].
68+
* Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode] and [orientation].
6769
* @return a [Result] containing the resulting [Bitmap].
6870
*/
6971
fun compressToBitmap(
7072
inputStream: InputStream,
7173
resizeMode: ResizeMode,
74+
orientation: Int,
7275
): Result<Bitmap> = runCatching {
7376
BufferedInputStream(inputStream).use { input ->
7477
val options = BitmapFactory.Options()
7578
calculateDecodingScale(input, resizeMode, options)
7679
val decodedBitmap = BitmapFactory.decodeStream(input, null, options)
7780
?: error("Decoding Bitmap from InputStream failed")
78-
val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(input).getOrThrow()
81+
val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(orientation)
7982
if (resizeMode is ResizeMode.Strict) {
8083
rotatedBitmap.resizeToMax(resizeMode.maxWidth, resizeMode.maxHeight)
8184
} else {

0 commit comments

Comments
 (0)