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..ef46649 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,69 @@ 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 rotateBitmap90CounterClockwise(source: Bitmap): Bitmap { + val width = source.width + val height = source.height + + // Create a new Bitmap with swapped width and height + val rotatedBitmap = Bitmap.createBitmap(height, width, source.config ?: Bitmap.Config.ARGB_8888) + + for (y in 0 until height) { + for (x in 0 until width) { + // Set the pixel in the new position + rotatedBitmap.setPixel(y, width - 1 - x, source.getPixel(x, y)) + } + } + + return rotatedBitmap +} + + +fun Bitmap.transformBitmap(orientation: Orientation): Bitmap { + return when (orientation) { + Orientation.PORTRAIT -> this // No transformation needed + Orientation.LANDSCAPE_LEFT -> { + // Transpose (swap width and height) + val transposedBitmap = Bitmap.createBitmap(height, width, config ?: Bitmap.Config.ARGB_8888) + for (y in 0 until height) { + for (x in 0 until width) { + transposedBitmap.setPixel(y, width - 1 - x, getPixel(x, y)) + } + } + transposedBitmap + } + Orientation.PORTRAIT_UPSIDE_DOWN -> { + // Invert vertically and horizontally (180-degree rotation) + val invertedBitmap = Bitmap.createBitmap(width, height, config ?: Bitmap.Config.ARGB_8888) + for (y in 0 until height) { + for (x in 0 until width) { + invertedBitmap.setPixel(width - 1 - x, height - 1 - y, getPixel(x, y)) + } + } + invertedBitmap + } + Orientation.LANDSCAPE_RIGHT -> { + // Transpose (swap width and height) and invert vertically + val transposedBitmap = Bitmap.createBitmap(height, width, config ?: Bitmap.Config.ARGB_8888) + for (y in 0 until height) { + for (x in 0 until width) { + transposedBitmap.setPixel(height - 1 - y, x, getPixel(x, y)) + } + } + transposedBitmap + } + } +} + @SuppressLint("ViewConstructor") class PreviewView(context: Context, callback: SurfaceHolder.Callback) : @@ -81,6 +144,34 @@ 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(height, width, 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) { + continuation.resume(rotateBitmap90CounterClockwise(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..0b46475 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.JPEG, 50, 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() + } } }