capacitor-camera-view 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,8 +3,6 @@ package com.michaelwolz.capacitorcameraview
3
3
  import android.content.Context
4
4
  import android.content.Context.CAMERA_SERVICE
5
5
  import android.graphics.Bitmap
6
- import android.graphics.BitmapFactory
7
- import android.graphics.Matrix
8
6
  import android.hardware.camera2.CameraCharacteristics
9
7
  import android.hardware.camera2.CameraManager
10
8
  import android.util.Base64
@@ -18,13 +16,13 @@ import androidx.camera.core.CameraSelector
18
16
  import androidx.camera.core.ImageAnalysis
19
17
  import androidx.camera.core.ImageCapture
20
18
  import androidx.camera.core.ImageCaptureException
21
- import androidx.camera.core.ImageProxy
22
19
  import androidx.camera.core.resolutionselector.AspectRatioStrategy
23
20
  import androidx.camera.core.resolutionselector.ResolutionSelector
24
21
  import androidx.camera.mlkit.vision.MlKitAnalyzer
25
22
  import androidx.camera.view.LifecycleCameraController
26
23
  import androidx.camera.view.PreviewView
27
24
  import androidx.core.content.ContextCompat
25
+ import androidx.exifinterface.media.ExifInterface
28
26
  import androidx.lifecycle.LifecycleOwner
29
27
  import com.getcapacitor.Plugin
30
28
  import com.google.mlkit.vision.barcode.BarcodeScanner
@@ -36,6 +34,8 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
36
34
  import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
37
35
  import com.michaelwolz.capacitorcameraview.model.ZoomFactors
38
36
  import java.io.ByteArrayOutputStream
37
+ import java.io.File
38
+ import java.util.UUID
39
39
  import java.util.concurrent.ExecutorService
40
40
  import java.util.concurrent.Executors
41
41
 
@@ -124,32 +124,30 @@ class CameraView(plugin: Plugin) {
124
124
  }
125
125
 
126
126
  /** 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
+ }
134
133
 
135
134
  mainHandler.post {
136
135
  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
+
137
141
  controller.takePicture(
142
+ outputOptions,
138
143
  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)
150
147
  }
151
148
 
152
149
  override fun onError(exception: ImageCaptureException) {
150
+ tempFile.delete()
153
151
  Log.e(TAG, "Error capturing image", exception)
154
152
  callback(null, exception)
155
153
  }
@@ -162,6 +160,61 @@ class CameraView(plugin: Plugin) {
162
160
  }
163
161
  }
164
162
 
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
+
165
218
  /**
166
219
  * Capture a frame directly from the preview without using the full photo pipeline which is
167
220
  * faster but has lower quality.
@@ -494,36 +547,6 @@ class CameraView(plugin: Plugin) {
494
547
  lastBarcodeDetectionTime = now
495
548
  }
496
549
 
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
-
527
550
  private fun notifyBarcodeDetected(result: BarcodeDetectionResult) {
528
551
  pluginDelegate.let { plugin ->
529
552
  if (plugin is CameraViewPlugin) {
@@ -73,7 +73,7 @@ class CameraViewPlugin : Plugin() {
73
73
 
74
74
  @PluginMethod
75
75
  fun capture(call: PluginCall) {
76
- val quality = call.getInt("quality", 90)
76
+ val quality = call.getInt("quality") ?: 90
77
77
 
78
78
  if (quality !in 0..100) {
79
79
  call.reject("Quality must be between 0 and 100")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-camera-view",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A Capacitor plugin for embedding a live camera feed directly into your app.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",