Compare commits
	
		
			4 Commits
		
	
	
		
			c64516693c
			...
			4798aad464
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4798aad464 | |||
|  | 2c8d503e66 | ||
| 5b52acda26 | |||
| 17f675657e | 
| @@ -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) | ||||
|  | ||||
|   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") | ||||
|   Log.i(TAG, "Successfully saved snapshot to file!") | ||||
|  | ||||
|   // Parse output data | ||||
|   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) | ||||
|  | ||||
|   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) { | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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<Bitmap?> { 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()) | ||||
|   | ||||
| @@ -1,10 +1,30 @@ | ||||
| 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 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() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user