From 17f675657e1d0f1fd76f66f5c18772499596e9ae Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Sat, 9 Nov 2024 19:52:05 -0700 Subject: [PATCH] WIP --- .../mrousavy/camera/CameraView+TakePhoto.kt | 63 +++++-------------- .../java/com/mrousavy/camera/CameraView.kt | 2 +- .../com/mrousavy/camera/core/PreviewView.kt | 45 ++++++++++++- .../com/mrousavy/camera/utils/FileUtils.kt | 26 +++++++- 4 files changed, 83 insertions(+), 53 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt index c09ae3f..a33958c 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt @@ -13,69 +13,36 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import com.mrousavy.camera.core.CameraSession import com.mrousavy.camera.core.InsufficientStorageError +import com.mrousavy.camera.utils.FileUtils import com.mrousavy.camera.types.Flash -import com.mrousavy.camera.types.QualityPrioritization import com.mrousavy.camera.utils.* import java.io.File import java.io.FileOutputStream import java.io.IOException import kotlinx.coroutines.* -private const val TAG = "CameraView.takePhoto" +private const val TAG = "CameraView.takeSnapshot" @SuppressLint("UnsafeOptInUsageError") suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap { val options = optionsMap.toHashMap() - Log.i(TAG, "Taking photo... Options: $options") + Log.i(TAG, "Taking snapshot... Options: $options") + val bitmap = previewView.getBitmap() ?: throw Error() - val qualityPrioritization = options["qualityPrioritization"] as? String ?: "balanced" - val flash = options["flash"] as? String ?: "off" - val enableAutoStabilization = options["enableAutoStabilization"] == true - val enableShutterSound = options["enableShutterSound"] as? Boolean ?: true - val enablePrecapture = options["enablePrecapture"] as? Boolean ?: false + val file = FileUtils.createTempFile(context, "png"); - // TODO: Implement Red Eye Reduction - options["enableAutoRedEyeReduction"] + // Write snapshot to .jpg file + FileUtils.writeBitmapTofile(bitmap, file, 100) - val flashMode = Flash.fromUnionValue(flash) - val qualityPrioritizationMode = QualityPrioritization.fromUnionValue(qualityPrioritization) + Log.i(TAG, "Successfully saved snapshot to file!") - val photo = cameraSession.takePhoto( - qualityPrioritizationMode, - flashMode, - enableShutterSound, - enableAutoStabilization, - enablePrecapture, - orientation - ) - - photo.use { - Log.i(TAG, "Successfully captured ${photo.image.width} x ${photo.image.height} photo!") - - val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId!!) - - val path = try { - savePhotoToFile(context, cameraCharacteristics, photo) - } catch (e: IOException) { - if (e.message?.contains("no space left", true) == true) { - throw InsufficientStorageError() - } else { - throw e - } - } - - 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.format == ImageFormat.RAW_SENSOR) - map.putBoolean("isMirrored", photo.isMirrored) - - return map - } + // Parse output data + val map = Arguments.createMap() + map.putString("path", file.absolutePath) + map.putInt("width", bitmap.width) + map.putInt("height", bitmap.height) + map.putBoolean("isMirrored", false) + return map } private fun writePhotoToFile(photo: CameraSession.CapturedPhoto, file: File) { diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index fa25aae..3569cd3 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -102,7 +102,7 @@ class CameraView(context: Context) : // session internal val cameraSession: CameraSession - private val previewView: PreviewView + val previewView: PreviewView private var currentConfigureCall: Long = System.currentTimeMillis() internal var frameProcessor: FrameProcessor? = null diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt index e660e2c..8e8f7f5 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PreviewView.kt @@ -1,11 +1,14 @@ package com.mrousavy.camera.core import android.annotation.SuppressLint -import android.content.res.Configuration import android.content.Context +import android.content.res.Configuration import android.graphics.Point +import android.os.Handler +import android.os.Looper import android.util.Log import android.util.Size +import android.view.PixelCopy import android.view.SurfaceHolder import android.view.SurfaceView import com.facebook.react.bridge.UiThreadUtil @@ -13,9 +16,20 @@ import com.mrousavy.camera.extensions.resize import com.mrousavy.camera.extensions.rotatedBy import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.ResizeMode +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException import kotlin.math.roundToInt import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import android.graphics.Bitmap +import android.graphics.Matrix + +fun Bitmap.rotateBitmap(degrees: Float): Bitmap { + val matrix = android.graphics.Matrix().apply { postRotate(degrees) } + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) +} + @SuppressLint("ViewConstructor") class PreviewView(context: Context, callback: SurfaceHolder.Callback) : @@ -81,6 +95,35 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) : } } + suspend fun getBitmap(): Bitmap? = withContext(Dispatchers.Main) { + val frame = holder.getSurfaceFrame() + val width = frame.width() + val height = frame.height() + + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + + // Use a coroutine to suspend until the PixelCopy request is complete + suspendCancellableCoroutine { continuation -> + PixelCopy.request( + holder.surface, + bitmap, + { copyResult -> + if (copyResult == PixelCopy.SUCCESS) { + val rotationDegrees = inputOrientation.toDegrees().toFloat() + val rotatedBitmap = bitmap.rotateBitmap(rotationDegrees) + continuation.resume(bitmap) + } else { + continuation.resumeWithException( + RuntimeException("PixelCopy failed with error code $copyResult") + ) + } + }, + Handler(Looper.getMainLooper()) + ) + } + } + + fun convertLayerPointToCameraCoordinates(point: Point, cameraDeviceDetails: CameraDeviceDetails): Point { val sensorOrientation = cameraDeviceDetails.sensorOrientation val cameraSize = Size(cameraDeviceDetails.activeSize.width(), cameraDeviceDetails.activeSize.height()) diff --git a/package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt b/package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt index f3b462b..667ce9d 100644 --- a/package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt +++ b/package/android/src/main/java/com/mrousavy/camera/utils/FileUtils.kt @@ -1,13 +1,33 @@ package com.mrousavy.camera.utils import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Size import java.io.File +import java.io.FileOutputStream class FileUtils { companion object { - fun createTempFile(context: Context, extension: String): File = - File.createTempFile("mrousavy", extension, context.cacheDir).also { - it.deleteOnExit() + fun writeBitmapTofile(bitmap: Bitmap, file: File, quality: Int) { + FileOutputStream(file).use { stream -> + bitmap.compress(Bitmap.CompressFormat.PNG, quality, stream) } + } + + fun getImageSize(imagePath: String): Size { + val bitmapOptions = BitmapFactory.Options().also { + it.inJustDecodeBounds = true + } + BitmapFactory.decodeFile(imagePath, bitmapOptions) + val width = bitmapOptions.outWidth + val height = bitmapOptions.outHeight + return Size(width, height) + } + + fun createTempFile(context: Context, extension: String): File = + File.createTempFile("mrousavy", extension, context.cacheDir).also { + it.deleteOnExit() + } } }