Skip to content

Commit a9897a6

Browse files
authored
Merge pull request #2 from michaelwolz/perf/android/improve-image-processing
Improve image processing on Android
2 parents 2e32af2 + 6770578 commit a9897a6

2 files changed

Lines changed: 75 additions & 52 deletions

File tree

android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package com.michaelwolz.capacitorcameraview
33
import android.content.Context
44
import android.content.Context.CAMERA_SERVICE
55
import android.graphics.Bitmap
6-
import android.graphics.BitmapFactory
7-
import android.graphics.Matrix
86
import android.hardware.camera2.CameraCharacteristics
97
import android.hardware.camera2.CameraManager
108
import android.util.Base64
@@ -18,13 +16,13 @@ import androidx.camera.core.CameraSelector
1816
import androidx.camera.core.ImageAnalysis
1917
import androidx.camera.core.ImageCapture
2018
import androidx.camera.core.ImageCaptureException
21-
import androidx.camera.core.ImageProxy
2219
import androidx.camera.core.resolutionselector.AspectRatioStrategy
2320
import androidx.camera.core.resolutionselector.ResolutionSelector
2421
import androidx.camera.mlkit.vision.MlKitAnalyzer
2522
import androidx.camera.view.LifecycleCameraController
2623
import androidx.camera.view.PreviewView
2724
import androidx.core.content.ContextCompat
25+
import androidx.exifinterface.media.ExifInterface
2826
import androidx.lifecycle.LifecycleOwner
2927
import com.getcapacitor.Plugin
3028
import com.google.mlkit.vision.barcode.BarcodeScanner
@@ -36,6 +34,8 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
3634
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
3735
import com.michaelwolz.capacitorcameraview.model.ZoomFactors
3836
import java.io.ByteArrayOutputStream
37+
import java.io.File
38+
import java.util.UUID
3939
import java.util.concurrent.ExecutorService
4040
import 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) {

android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class CameraViewPlugin : Plugin() {
7373

7474
@PluginMethod
7575
fun capture(call: PluginCall) {
76-
val quality = call.getInt("quality", 90)
76+
val quality = call.getInt("quality") ?: 90
7777

7878
if (quality !in 0..100) {
7979
call.reject("Quality must be between 0 and 100")

0 commit comments

Comments
 (0)