Fix the orientation of sent images (#1190)
* Fix the orientation of sent images --------- Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
parent
f7dc44f35d
commit
b16dc45754
4 changed files with 22 additions and 16 deletions
1
changelog.d/1135.bugfix
Normal file
1
changelog.d/1135.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix the orientation of sent images.
|
||||||
|
|
@ -22,7 +22,6 @@ import android.graphics.Matrix
|
||||||
import androidx.core.graphics.scale
|
import androidx.core.graphics.scale
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
|
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<Bitmap> =
|
|
||||||
runCatching { rotateToMetadataOrientation(this, ExifInterface(inputStream)) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scales the current [Bitmap] to fit the ([maxWidth], [maxHeight]) bounds while keeping aspect ratio.
|
* Scales the current [Bitmap] to fit the ([maxWidth], [maxHeight]) bounds while keeping aspect ratio.
|
||||||
* @throws IllegalStateException if [maxWidth] or [maxHeight] <= 0.
|
* @throws IllegalStateException if [maxWidth] or [maxHeight] <= 0.
|
||||||
|
|
@ -77,8 +69,11 @@ fun BitmapFactory.Options.calculateInSampleSize(desiredWidth: Int, desiredHeight
|
||||||
return inSampleSize
|
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()
|
val matrix = Matrix()
|
||||||
when (orientation) {
|
when (orientation) {
|
||||||
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
||||||
|
|
@ -94,8 +89,8 @@ private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInter
|
||||||
matrix.preRotate(90f)
|
matrix.preRotate(90f)
|
||||||
matrix.preScale(-1f, 1f)
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,17 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
|
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
|
||||||
|
|
||||||
suspend fun processImageWithCompression(): 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 ->
|
val compressionResult = contentResolver.openInputStream(uri).use { input ->
|
||||||
imageCompressor.compressToTmpFile(
|
imageCompressor.compressToTmpFile(
|
||||||
inputStream = requireNotNull(input),
|
inputStream = requireNotNull(input),
|
||||||
resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE),
|
resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE),
|
||||||
|
orientation = orientation,
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}
|
}
|
||||||
val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file)
|
val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.libraries.mediaupload
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize
|
import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize
|
||||||
import io.element.android.libraries.androidutils.bitmap.resizeToMax
|
import io.element.android.libraries.androidutils.bitmap.resizeToMax
|
||||||
import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation
|
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
|
* 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.
|
* @return a [Result] containing the resulting [ImageCompressionResult] with the temporary [File] and some metadata.
|
||||||
*/
|
*/
|
||||||
suspend fun compressToTmpFile(
|
suspend fun compressToTmpFile(
|
||||||
inputStream: InputStream,
|
inputStream: InputStream,
|
||||||
resizeMode: ResizeMode,
|
resizeMode: ResizeMode,
|
||||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||||
|
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||||
desiredQuality: Int = 80,
|
desiredQuality: Int = 80,
|
||||||
): Result<ImageCompressionResult> = withContext(Dispatchers.IO) {
|
): Result<ImageCompressionResult> = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val compressedBitmap = compressToBitmap(inputStream, resizeMode).getOrThrow()
|
val compressedBitmap = compressToBitmap(inputStream, resizeMode, orientation).getOrThrow()
|
||||||
// Encode bitmap to the destination temporary file
|
// Encode bitmap to the destination temporary file
|
||||||
val tmpFile = context.createTmpFile(extension = "jpeg")
|
val tmpFile = context.createTmpFile(extension = "jpeg")
|
||||||
tmpFile.outputStream().use {
|
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].
|
* @return a [Result] containing the resulting [Bitmap].
|
||||||
*/
|
*/
|
||||||
fun compressToBitmap(
|
fun compressToBitmap(
|
||||||
inputStream: InputStream,
|
inputStream: InputStream,
|
||||||
resizeMode: ResizeMode,
|
resizeMode: ResizeMode,
|
||||||
|
orientation: Int,
|
||||||
): Result<Bitmap> = runCatching {
|
): Result<Bitmap> = runCatching {
|
||||||
BufferedInputStream(inputStream).use { input ->
|
BufferedInputStream(inputStream).use { input ->
|
||||||
val options = BitmapFactory.Options()
|
val options = BitmapFactory.Options()
|
||||||
calculateDecodingScale(input, resizeMode, options)
|
calculateDecodingScale(input, resizeMode, options)
|
||||||
val decodedBitmap = BitmapFactory.decodeStream(input, null, options)
|
val decodedBitmap = BitmapFactory.decodeStream(input, null, options)
|
||||||
?: error("Decoding Bitmap from InputStream failed")
|
?: error("Decoding Bitmap from InputStream failed")
|
||||||
val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(input).getOrThrow()
|
val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(orientation)
|
||||||
if (resizeMode is ResizeMode.Strict) {
|
if (resizeMode is ResizeMode.Strict) {
|
||||||
rotatedBitmap.resizeToMax(resizeMode.maxWidth, resizeMode.maxHeight)
|
rotatedBitmap.resizeToMax(resizeMode.maxWidth, resizeMode.maxHeight)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue