Skip to content

Commit

Permalink
feat: Write proper photo metadata (orientation & isMirrored) (#2660)
Browse files Browse the repository at this point in the history
* feat: Write proper photo metadata (orientation & isMirrored)

* Format
  • Loading branch information
mrousavy authored Mar 18, 2024
1 parent 3d68d74 commit babed3c
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 111 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
package com.mrousavy.camera

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.ImageFormat
import android.hardware.camera2.*
import android.util.Log
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.mrousavy.camera.core.InsufficientStorageError
import com.mrousavy.camera.core.Photo
import com.mrousavy.camera.types.Flash
import com.mrousavy.camera.utils.*
import java.io.IOException
import kotlinx.coroutines.*

private const val TAG = "CameraView.takePhoto"
Expand All @@ -31,57 +26,15 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap {
orientation
)

photo.use {
Log.i(TAG, "Successfully captured ${photo.image.width} x ${photo.image.height} photo!")
Log.i(TAG, "Successfully captured ${photo.width} x ${photo.height} photo!")

val path = try {
savePhotoToFile(context, photo)
} catch (e: IOException) {
if (e.message?.contains("no space left", true) == true) {
throw InsufficientStorageError()
} else {
throw e
}
}
val map = Arguments.createMap()
map.putString("path", photo.path)
map.putInt("width", photo.width)
map.putInt("height", photo.height)
map.putString("orientation", photo.orientation.unionValue)
map.putBoolean("isRawPhoto", false)
map.putBoolean("isMirrored", photo.isMirrored)

Log.i(TAG, "Successfully saved photo to file! $path")

val map = Arguments.createMap()
map.putString("path", path)
map.putInt("width", photo.image.width)
map.putInt("height", photo.image.height)
map.putString("orientation", photo.orientation.unionValue)
map.putBoolean("isRawPhoto", photo.isRawPhoto)
map.putBoolean("isMirrored", photo.isMirrored)

return map
}
return map
}

private suspend fun savePhotoToFile(context: Context, photo: Photo): String =
withContext(Dispatchers.IO) {
when (photo.image.format) {
ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
// When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is
val file = FileUtils.createTempFile(context, ".jpg")
FileUtils.writePhotoToFile(photo, file)
return@withContext file.absolutePath
}

ImageFormat.RAW_SENSOR -> {
// When the format is RAW we use the DngCreator utility library
throw Error("Writing RAW photos is currently not supported!")
// TODO: Write RAW photos using DngCreator?
// val dngCreator = DngCreator(cameraCharacteristics, photo.metadata)
// val file = FileUtils.createTempFile(context, ".dng")
// FileOutputStream(file).use { stream ->
// dngCreator.writeImage(stream, photo.image.image)
// }
// return@withContext file.absolutePath
}

else -> {
throw Error("Failed to save Photo to file, image format is not supported! ${photo.image.format}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.util.Log
import android.view.Gravity
import android.view.ScaleGestureDetector
import android.widget.FrameLayout
import androidx.camera.core.Preview.SurfaceProvider
import androidx.camera.view.PreviewView
import com.google.mlkit.vision.barcode.common.Barcode
import com.mrousavy.camera.core.CameraConfiguration
Expand Down Expand Up @@ -143,7 +142,13 @@ class CameraView(context: Context) :
}
}

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
override fun onLayout(
changed: Boolean,
left: Int,
top: Int,
right: Int,
bottom: Int
) {
val width = right - left
val height = bottom - top

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.util.Log
import android.util.Range
import android.util.Size
Expand Down Expand Up @@ -447,9 +448,18 @@ class CameraSession(private val context: Context, private val callback: Callback
photoOutput.flashMode = flash.toFlashMode()
photoOutput.targetRotation = outputOrientation.toDegrees()

val image = photoOutput.takePicture(enableShutterSound, callback, CameraQueues.cameraExecutor)
val isMirrored = camera.cameraInfo.lensFacing == CameraSelector.LENS_FACING_FRONT
return Photo(image, isMirrored)
val photoFile = photoOutput.takePicture(context, enableShutterSound, callback, CameraQueues.cameraExecutor)
val isMirrored = photoFile.metadata.isReversedHorizontal

val bitmapOptions = BitmapFactory.Options().also {
it.inJustDecodeBounds = true
}
BitmapFactory.decodeFile(photoFile.uri.path, bitmapOptions)
val width = bitmapOptions.outWidth
val height = bitmapOptions.outHeight
val orientation = outputOrientation

return Photo(photoFile.uri.path, width, height, orientation, isMirrored)
}

@OptIn(ExperimentalPersistentRecording::class)
Expand Down
14 changes: 1 addition & 13 deletions package/android/src/main/java/com/mrousavy/camera/core/Photo.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
package com.mrousavy.camera.core

import android.graphics.ImageFormat
import androidx.camera.core.ImageProxy
import com.mrousavy.camera.types.Orientation
import java.io.Closeable

data class Photo(val image: ImageProxy, val isMirrored: Boolean) : Closeable {
val orientation: Orientation
get() = Orientation.fromRotationDegrees(image.imageInfo.rotationDegrees)
val isRawPhoto
get() = image.format in listOf(ImageFormat.RAW_SENSOR, ImageFormat.RAW10, ImageFormat.RAW12, ImageFormat.RAW_PRIVATE)

override fun close() {
image.close()
}
}
data class Photo(val path: String, val width: Int, val height: Int, val orientation: Orientation, val isMirrored: Boolean)
Original file line number Diff line number Diff line change
@@ -1,25 +1,46 @@
package com.mrousavy.camera.extensions

import android.annotation.SuppressLint
import android.content.Context
import android.media.MediaActionSound
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.OutputFileOptions
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import com.mrousavy.camera.core.CameraSession
import com.mrousavy.camera.types.ShutterType
import com.mrousavy.camera.utils.FileUtils
import java.net.URI
import java.util.concurrent.Executor
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine

suspend inline fun ImageCapture.takePicture(enableShutterSound: Boolean, callback: CameraSession.Callback, executor: Executor): ImageProxy =
data class PhotoFileInfo(val uri: URI, val metadata: ImageCapture.Metadata)

@SuppressLint("RestrictedApi")
suspend inline fun ImageCapture.takePicture(
context: Context,
enableShutterSound: Boolean,
callback: CameraSession.Callback,
executor: Executor
): PhotoFileInfo =
suspendCancellableCoroutine { continuation ->
// Shutter sound
val shutterSound = if (enableShutterSound) MediaActionSound() else null
shutterSound?.load(MediaActionSound.SHUTTER_CLICK)

val file = FileUtils.createTempFile(context, ".jpg")
val outputFileOptionsBuilder = OutputFileOptions.Builder(file).also { options ->
val metadata = ImageCapture.Metadata()
metadata.isReversedHorizontal = camera?.isFrontFacing == true
options.setMetadata(metadata)
}
val outputFileOptions = outputFileOptionsBuilder.build()

takePicture(
outputFileOptions,
executor,
object : ImageCapture.OnImageCapturedCallback() {
object : ImageCapture.OnImageSavedCallback {
override fun onCaptureStarted() {
super.onCaptureStarted()
if (enableShutterSound) {
Expand All @@ -29,15 +50,15 @@ suspend inline fun ImageCapture.takePicture(enableShutterSound: Boolean, callbac
callback.onShutter(ShutterType.PHOTO)
}

override fun onCaptureSuccess(image: ImageProxy) {
super.onCaptureSuccess(image)
@SuppressLint("RestrictedApi")
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
if (continuation.isActive) {
continuation.resume(image)
val info = PhotoFileInfo(file.toURI(), outputFileOptions.metadata)
continuation.resume(info)
}
}

override fun onError(exception: ImageCaptureException) {
super.onError(exception)
if (continuation.isActive) {
continuation.resumeWithException(exception)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ package com.mrousavy.camera.utils

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import com.mrousavy.camera.core.InvalidImageTypeError
import com.mrousavy.camera.core.Photo
import java.io.File
import java.io.FileOutputStream
import java.nio.ByteBuffer

class FileUtils {
companion object {
Expand All @@ -17,29 +12,6 @@ class FileUtils {
it.deleteOnExit()
}

private fun writeBufferToFile(byteBuffer: ByteBuffer, isMirrored: Boolean, file: File) {
if (isMirrored) {
val imageBytes = ByteArray(byteBuffer.remaining()).apply { byteBuffer.get(this) }
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
val matrix = Matrix()
matrix.preScale(-1f, 1f)
val processedBitmap =
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
FileOutputStream(file).use { stream ->
processedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
}
} else {
FileOutputStream(file).use { stream ->
stream.channel.write(byteBuffer)
}
}
}

fun writePhotoToFile(photo: Photo, file: File) {
val plane = photo.image.planes[0] ?: throw InvalidImageTypeError()
writeBufferToFile(plane.buffer, photo.isMirrored, file)
}

fun writeBitmapTofile(bitmap: Bitmap, file: File, quality: Int = 100) {
FileOutputStream(file).use { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
Expand Down
4 changes: 2 additions & 2 deletions package/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ PODS:
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- SocketRocket (0.6.1)
- VisionCamera (4.0.0-beta.5):
- VisionCamera (4.0.0-beta.6):
- React
- React-callinvoker
- React-Core
Expand Down Expand Up @@ -724,7 +724,7 @@ SPEC CHECKSUMS:
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
VisionCamera: 81972e59d3cac50c97a37fa109dafd63720a8bd0
VisionCamera: 7a9e59cc5e65d458d9809a9ac23700265df0f14e
Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5

PODFILE CHECKSUM: 27f53791141a3303d814e09b55770336416ff4eb
Expand Down

0 comments on commit babed3c

Please sign in to comment.