fix(android): rotate camera photos by EXIF orientation
This commit is contained in:
committed by
Peter Steinberger
parent
e0a30c4abc
commit
04ae9bdbef
@@ -5,8 +5,10 @@ import android.content.Context
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.ExifInterface
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.ImageCapture
|
import androidx.camera.core.ImageCapture
|
||||||
@@ -86,18 +88,19 @@ class CameraCaptureManager(private val context: Context) {
|
|||||||
provider.unbindAll()
|
provider.unbindAll()
|
||||||
provider.bindToLifecycle(owner, selector, capture)
|
provider.bindToLifecycle(owner, selector, capture)
|
||||||
|
|
||||||
val bytes = capture.takeJpegBytes(context.mainExecutor())
|
val (bytes, orientation) = capture.takeJpegWithExif(context.mainExecutor())
|
||||||
val decoded = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
val decoded = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||||
?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image")
|
?: throw IllegalStateException("UNAVAILABLE: failed to decode captured image")
|
||||||
|
val rotated = rotateBitmapByExif(decoded, orientation)
|
||||||
val scaled =
|
val scaled =
|
||||||
if (maxWidth != null && maxWidth > 0 && decoded.width > maxWidth) {
|
if (maxWidth != null && maxWidth > 0 && rotated.width > maxWidth) {
|
||||||
val h =
|
val h =
|
||||||
(decoded.height.toDouble() * (maxWidth.toDouble() / decoded.width.toDouble()))
|
(rotated.height.toDouble() * (maxWidth.toDouble() / rotated.width.toDouble()))
|
||||||
.toInt()
|
.toInt()
|
||||||
.coerceAtLeast(1)
|
.coerceAtLeast(1)
|
||||||
decoded.scale(maxWidth, h)
|
rotated.scale(maxWidth, h)
|
||||||
} else {
|
} else {
|
||||||
decoded
|
rotated
|
||||||
}
|
}
|
||||||
|
|
||||||
val maxPayloadBytes = 5 * 1024 * 1024
|
val maxPayloadBytes = 5 * 1024 * 1024
|
||||||
@@ -194,6 +197,31 @@ class CameraCaptureManager(private val context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun rotateBitmapByExif(bitmap: Bitmap, orientation: Int): Bitmap {
|
||||||
|
val matrix = Matrix()
|
||||||
|
when (orientation) {
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
|
||||||
|
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||||
|
matrix.postRotate(90f)
|
||||||
|
matrix.postScale(-1f, 1f)
|
||||||
|
}
|
||||||
|
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||||
|
matrix.postRotate(-90f)
|
||||||
|
matrix.postScale(-1f, 1f)
|
||||||
|
}
|
||||||
|
else -> return bitmap
|
||||||
|
}
|
||||||
|
val rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||||
|
if (rotated !== bitmap) {
|
||||||
|
bitmap.recycle()
|
||||||
|
}
|
||||||
|
return rotated
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseFacing(paramsJson: String?): String? =
|
private fun parseFacing(paramsJson: String?): String? =
|
||||||
when {
|
when {
|
||||||
paramsJson?.contains("\"front\"") == true -> "front"
|
paramsJson?.contains("\"front\"") == true -> "front"
|
||||||
@@ -254,7 +282,8 @@ private suspend fun Context.cameraProvider(): ProcessCameraProvider =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ImageCapture.takeJpegBytes(executor: Executor): ByteArray =
|
/** Returns (jpegBytes, exifOrientation) so caller can rotate the decoded bitmap. */
|
||||||
|
private suspend fun ImageCapture.takeJpegWithExif(executor: Executor): Pair<ByteArray, Int> =
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
val file = File.createTempFile("clawdbot-snap-", ".jpg")
|
val file = File.createTempFile("clawdbot-snap-", ".jpg")
|
||||||
val options = ImageCapture.OutputFileOptions.Builder(file).build()
|
val options = ImageCapture.OutputFileOptions.Builder(file).build()
|
||||||
@@ -263,13 +292,19 @@ private suspend fun ImageCapture.takeJpegBytes(executor: Executor): ByteArray =
|
|||||||
executor,
|
executor,
|
||||||
object : ImageCapture.OnImageSavedCallback {
|
object : ImageCapture.OnImageSavedCallback {
|
||||||
override fun onError(exception: ImageCaptureException) {
|
override fun onError(exception: ImageCaptureException) {
|
||||||
|
file.delete()
|
||||||
cont.resumeWithException(exception)
|
cont.resumeWithException(exception)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||||
try {
|
try {
|
||||||
|
val exif = ExifInterface(file.absolutePath)
|
||||||
|
val orientation = exif.getAttributeInt(
|
||||||
|
ExifInterface.TAG_ORIENTATION,
|
||||||
|
ExifInterface.ORIENTATION_NORMAL,
|
||||||
|
)
|
||||||
val bytes = file.readBytes()
|
val bytes = file.readBytes()
|
||||||
cont.resume(bytes)
|
cont.resume(Pair(bytes, orientation))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
cont.resumeWithException(e)
|
cont.resumeWithException(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user