@@ -3,8 +3,6 @@ package com.michaelwolz.capacitorcameraview
33import android.content.Context
44import android.content.Context.CAMERA_SERVICE
55import android.graphics.Bitmap
6- import android.graphics.BitmapFactory
7- import android.graphics.Matrix
86import android.hardware.camera2.CameraCharacteristics
97import android.hardware.camera2.CameraManager
108import android.util.Base64
@@ -18,13 +16,13 @@ import androidx.camera.core.CameraSelector
1816import androidx.camera.core.ImageAnalysis
1917import androidx.camera.core.ImageCapture
2018import androidx.camera.core.ImageCaptureException
21- import androidx.camera.core.ImageProxy
2219import androidx.camera.core.resolutionselector.AspectRatioStrategy
2320import androidx.camera.core.resolutionselector.ResolutionSelector
2421import androidx.camera.mlkit.vision.MlKitAnalyzer
2522import androidx.camera.view.LifecycleCameraController
2623import androidx.camera.view.PreviewView
2724import androidx.core.content.ContextCompat
25+ import androidx.exifinterface.media.ExifInterface
2826import androidx.lifecycle.LifecycleOwner
2927import com.getcapacitor.Plugin
3028import com.google.mlkit.vision.barcode.BarcodeScanner
@@ -36,6 +34,8 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
3634import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
3735import com.michaelwolz.capacitorcameraview.model.ZoomFactors
3836import java.io.ByteArrayOutputStream
37+ import java.io.File
38+ import java.util.UUID
3939import java.util.concurrent.ExecutorService
4040import java.util.concurrent.Executors
4141
@@ -124,32 +124,30 @@ class CameraView(plugin: Plugin) {
124124 }
125125
126126 /* * Capture a photo with the current camera configuration */
127- fun capturePhoto (quality : Int? , callback : (String? , Exception ? ) -> Unit ) {
128- val controller =
129- this .cameraController
130- ? : run {
131- callback(null , Exception (" Camera controller not initialized" ))
132- return
133- }
127+ fun capturePhoto (quality : Int , callback : (String? , Exception ? ) -> Unit ) {
128+ val controller = this .cameraController
129+ ? : run {
130+ callback(null , Exception (" Camera controller not initialized" ))
131+ return
132+ }
134133
135134 mainHandler.post {
136135 try {
136+ // Create temporary file for the captured image
137+ val tempFile =
138+ File .createTempFile(UUID .randomUUID().toString(), " .jpg" , context.cacheDir)
139+ val outputOptions = ImageCapture .OutputFileOptions .Builder (tempFile).build()
140+
137141 controller.takePicture(
142+ outputOptions,
138143 cameraExecutor,
139- object : ImageCapture .OnImageCapturedCallback () {
140- override fun onCaptureSuccess (image : ImageProxy ) {
141- try {
142- val base64String = imageProxyToBase64(image, quality)
143- callback(base64String, null )
144- } catch (e: Exception ) {
145- Log .e(TAG , " Error processing captured image" , e)
146- callback(null , e)
147- } finally {
148- image.close()
149- }
144+ object : ImageCapture .OnImageSavedCallback {
145+ override fun onImageSaved (outputFileResults : ImageCapture .OutputFileResults ) {
146+ handleImageSaved(tempFile, quality, callback)
150147 }
151148
152149 override fun onError (exception : ImageCaptureException ) {
150+ tempFile.delete()
153151 Log .e(TAG , " Error capturing image" , exception)
154152 callback(null , exception)
155153 }
@@ -162,6 +160,61 @@ class CameraView(plugin: Plugin) {
162160 }
163161 }
164162
163+ /* *
164+ * Handles the image saved callback, re-encodes the JPEG if quality is specified
165+ * and returns the Base64 encoded string as the callback result.
166+ */
167+ private fun handleImageSaved (
168+ tempFile : File ,
169+ quality : Int ,
170+ callback : (String? , Exception ? ) -> Unit
171+ ) {
172+ val startTime = System .currentTimeMillis()
173+ try {
174+ val jpegBytes = tempFile.readBytes()
175+ val base64String = if (quality == 100 ) {
176+ // If quality is 100, return the original JPEG without re-encoding
177+ Log .d(TAG , " Encoding original JPEG (quality 100)" )
178+ Base64 .encodeToString(jpegBytes, Base64 .NO_WRAP )
179+ } else {
180+ // Otherwise, re-encode the JPEG with the specified quality
181+ // which is a little bit more expensive
182+ Log .d(TAG , " Re-encoding JPEG with quality $quality " )
183+ val originalExif = ExifInterface (tempFile.absolutePath)
184+ val orientation = originalExif.getAttributeInt(
185+ ExifInterface .TAG_ORIENTATION ,
186+ ExifInterface .ORIENTATION_UNDEFINED
187+ )
188+
189+ val bitmap =
190+ android.graphics.BitmapFactory .decodeByteArray(jpegBytes, 0 , jpegBytes.size)
191+ val compressedFile =
192+ File .createTempFile(UUID .randomUUID().toString(), " .jpg" , context.cacheDir)
193+ val outputStream = compressedFile.outputStream()
194+ bitmap.compress(Bitmap .CompressFormat .JPEG , quality, outputStream)
195+ outputStream.close()
196+
197+ val newExif = ExifInterface (compressedFile.absolutePath)
198+ newExif.setAttribute(ExifInterface .TAG_ORIENTATION , orientation.toString())
199+ newExif.saveAttributes()
200+
201+ val compressedBytes = compressedFile.readBytes()
202+ compressedFile.delete()
203+ Base64 .encodeToString(compressedBytes, Base64 .NO_WRAP )
204+ }
205+ val endTime = System .currentTimeMillis()
206+ Log .d(
207+ TAG ,
208+ " Image processing took ${endTime - startTime} ms (quality: ${quality ? : 100 } )"
209+ )
210+ tempFile.delete()
211+ callback(base64String, null )
212+ } catch (e: Exception ) {
213+ tempFile.delete()
214+ callback(null , e)
215+ }
216+ }
217+
165218 /* *
166219 * Capture a frame directly from the preview without using the full photo pipeline which is
167220 * faster but has lower quality.
@@ -494,36 +547,6 @@ class CameraView(plugin: Plugin) {
494547 lastBarcodeDetectionTime = now
495548 }
496549
497- /* * Converts an ImageProxy to a Base64 encoded string */
498- private fun imageProxyToBase64 (image : ImageProxy , quality : Int? ): String {
499- val buffer = image.planes[0 ].buffer
500- val bytes = ByteArray (buffer.remaining())
501- buffer.get(bytes)
502-
503- var bitmap = BitmapFactory .decodeByteArray(bytes, 0 , bytes.size)
504-
505- try {
506- // Apply rotation if needed
507- if (image.imageInfo.rotationDegrees != 0 ) {
508- val matrix = Matrix ()
509- matrix.postRotate(image.imageInfo.rotationDegrees.toFloat())
510- val rotatedBitmap =
511- Bitmap .createBitmap(bitmap, 0 , 0 , bitmap.width, bitmap.height, matrix, true )
512- // Recycle the original bitmap to prevent memory leaks
513- bitmap.recycle()
514- bitmap = rotatedBitmap
515- }
516-
517- val outputStream = ByteArrayOutputStream ()
518- bitmap.compress(Bitmap .CompressFormat .JPEG , quality ? : 90 , outputStream)
519- val byteArray = outputStream.toByteArray()
520- return Base64 .encodeToString(byteArray, Base64 .NO_WRAP )
521- } finally {
522- // Ensure bitmap is always recycled
523- bitmap.recycle()
524- }
525- }
526-
527550 private fun notifyBarcodeDetected (result : BarcodeDetectionResult ) {
528551 pluginDelegate.let { plugin ->
529552 if (plugin is CameraViewPlugin ) {
0 commit comments