@capgo/camera-preview 7.4.0-beta.1 → 7.4.0-beta.10

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.
Files changed (31) hide show
  1. package/README.md +195 -31
  2. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  4. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  5. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  6. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  7. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  8. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  9. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  10. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  11. package/android/.gradle/file-system.probe +0 -0
  12. package/android/build.gradle +3 -1
  13. package/android/src/main/AndroidManifest.xml +5 -3
  14. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +282 -45
  15. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +902 -102
  16. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +82 -0
  17. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +19 -5
  18. package/dist/docs.json +235 -6
  19. package/dist/esm/definitions.d.ts +119 -3
  20. package/dist/esm/definitions.js.map +1 -1
  21. package/dist/esm/web.d.ts +47 -3
  22. package/dist/esm/web.js +262 -78
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +258 -78
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +258 -78
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Sources/CapgoCameraPreview/CameraController.swift +245 -28
  29. package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
  30. package/ios/Sources/CapgoCameraPreview/Plugin.swift +657 -90
  31. package/package.json +1 -1
@@ -1,6 +1,11 @@
1
1
  import Foundation
2
2
  import Capacitor
3
3
  import AVFoundation
4
+ import Photos
5
+ import CoreImage
6
+ import CoreLocation
7
+ import MobileCoreServices
8
+
4
9
 
5
10
  extension UIWindow {
6
11
  static var isLandscape: Bool {
@@ -32,7 +37,7 @@ extension UIWindow {
32
37
  * here: https://capacitor.ionicframework.com/docs/plugins/ios
33
38
  */
34
39
  @objc(CameraPreview)
35
- public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
40
+ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
36
41
  public let identifier = "CameraPreviewPlugin"
37
42
  public let jsName = "CameraPreview"
38
43
  public let pluginMethods: [CAPPluginMethod] = [
@@ -54,11 +59,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
54
59
  CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
55
60
  CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
56
61
  CAPPluginMethod(name: "setDeviceId", returnType: CAPPluginReturnPromise),
57
- CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise)
62
+ CAPPluginMethod(name: "getDeviceId", returnType: CAPPluginReturnPromise),
63
+ CAPPluginMethod(name: "setAspectRatio", returnType: CAPPluginReturnPromise),
64
+ CAPPluginMethod(name: "getAspectRatio", returnType: CAPPluginReturnPromise),
65
+ CAPPluginMethod(name: "setGridMode", returnType: CAPPluginReturnPromise),
66
+ CAPPluginMethod(name: "getGridMode", returnType: CAPPluginReturnPromise),
67
+ CAPPluginMethod(name: "getPreviewSize", returnType: CAPPluginReturnPromise),
68
+ CAPPluginMethod(name: "setPreviewSize", returnType: CAPPluginReturnPromise)
58
69
  ]
59
70
  // Camera state tracking
60
71
  private var isInitializing: Bool = false
61
72
  private var isInitialized: Bool = false
73
+ private var backgroundSession: AVCaptureSession?
62
74
 
63
75
  var previewView: UIView!
64
76
  var cameraPosition = String()
@@ -74,33 +86,44 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
74
86
  var enableZoom: Bool?
75
87
  var highResolutionOutput: Bool = false
76
88
  var disableAudio: Bool = false
89
+ var locationManager: CLLocationManager?
90
+ var currentLocation: CLLocation?
91
+ private var aspectRatio: String?
92
+ private var gridMode: String = "none"
77
93
 
78
94
  // MARK: - Transparency Methods
79
-
95
+
96
+
80
97
  private func makeWebViewTransparent() {
81
98
  guard let webView = self.webView else { return }
82
-
99
+
100
+
83
101
  // Define a recursive function to traverse the view hierarchy
84
102
  func makeSubviewsTransparent(_ view: UIView) {
85
103
  // Set the background color to clear
86
104
  view.backgroundColor = .clear
87
-
105
+
106
+
88
107
  // Recurse for all subviews
89
108
  for subview in view.subviews {
90
109
  makeSubviewsTransparent(subview)
91
110
  }
92
111
  }
93
-
112
+
113
+
94
114
  // Set the main webView to be transparent
95
115
  webView.isOpaque = false
96
116
  webView.backgroundColor = .clear
97
-
117
+
118
+
98
119
  // Recursively make all subviews transparent
99
120
  makeSubviewsTransparent(webView)
100
-
121
+
122
+
101
123
  // Also ensure the webview's container is transparent
102
124
  webView.superview?.backgroundColor = .clear
103
-
125
+
126
+
104
127
  // Force a layout pass to apply changes
105
128
  DispatchQueue.main.async {
106
129
  webView.setNeedsLayout()
@@ -119,14 +142,26 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
119
142
  let paddingBottom = self.paddingBottom ?? 0
120
143
  let height = heightValue - paddingBottom
121
144
 
122
- if UIWindow.isLandscape {
123
- previewView.frame = CGRect(x: posY, y: posX, width: max(height, width), height: min(height, width))
124
- self.cameraController.previewLayer?.frame = previewView.frame
125
- }
145
+ // Handle auto-centering during rotation
146
+ if posX == -1 || posY == -1 {
147
+ // Trigger full recalculation for auto-centered views
148
+ self.updateCameraFrame()
149
+ } else {
150
+ // Manual positioning - use original rotation logic with no animation
151
+ CATransaction.begin()
152
+ CATransaction.setDisableActions(true)
153
+
154
+ if UIWindow.isLandscape {
155
+ previewView.frame = CGRect(x: posY, y: posX, width: max(height, width), height: min(height, width))
156
+ self.cameraController.previewLayer?.frame = previewView.bounds
157
+ }
126
158
 
127
- if UIWindow.isPortrait {
128
- previewView.frame = CGRect(x: posX, y: posY, width: min(height, width), height: max(height, width))
129
- self.cameraController.previewLayer?.frame = previewView.frame
159
+ if UIWindow.isPortrait {
160
+ previewView.frame = CGRect(x: posX, y: posY, width: min(height, width), height: max(height, width))
161
+ self.cameraController.previewLayer?.frame = previewView.bounds
162
+ }
163
+
164
+ CATransaction.commit()
130
165
  }
131
166
 
132
167
  if let connection = self.cameraController.fileVideoOutput?.connection(with: .video) {
@@ -145,13 +180,130 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
145
180
  }
146
181
 
147
182
  cameraController.updateVideoOrientation()
148
-
183
+
184
+ cameraController.updateVideoOrientation()
185
+
186
+ // Update grid overlay frame if it exists - no animation
187
+ if let gridOverlay = self.cameraController.gridOverlayView {
188
+ CATransaction.begin()
189
+ CATransaction.setDisableActions(true)
190
+ gridOverlay.frame = previewView.bounds
191
+ CATransaction.commit()
192
+ }
193
+
149
194
  // Ensure webview remains transparent after rotation
150
195
  if self.isInitialized {
151
196
  self.makeWebViewTransparent()
152
197
  }
153
198
  }
154
-
199
+
200
+ @objc func setAspectRatio(_ call: CAPPluginCall) {
201
+ guard self.isInitialized else {
202
+ call.reject("camera not started")
203
+ return
204
+ }
205
+
206
+ guard let newAspectRatio = call.getString("aspectRatio") else {
207
+ call.reject("aspectRatio parameter is required")
208
+ return
209
+ }
210
+
211
+ self.aspectRatio = newAspectRatio
212
+
213
+ // When aspect ratio changes, calculate maximum size possible from current position
214
+ if let posX = self.posX, let posY = self.posY {
215
+ let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
216
+ let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
217
+ let paddingBottom = self.paddingBottom ?? 0
218
+
219
+ // Calculate available space from current position
220
+ let availableWidth: CGFloat
221
+ let availableHeight: CGFloat
222
+
223
+ if posX == -1 || posY == -1 {
224
+ // Auto-centering mode - use full dimensions
225
+ availableWidth = webViewWidth
226
+ availableHeight = webViewHeight - paddingBottom
227
+ } else {
228
+ // Manual positioning - calculate remaining space
229
+ availableWidth = webViewWidth - posX
230
+ availableHeight = webViewHeight - posY - paddingBottom
231
+ }
232
+
233
+ // Parse aspect ratio - convert to portrait orientation for camera use
234
+ let ratioParts = newAspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
235
+ // For camera, we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
236
+ let ratio = ratioParts[1] / ratioParts[0]
237
+
238
+ // Calculate maximum size that fits the aspect ratio in available space
239
+ let maxWidthByHeight = availableHeight * CGFloat(ratio)
240
+ let maxHeightByWidth = availableWidth / CGFloat(ratio)
241
+
242
+ if maxWidthByHeight <= availableWidth {
243
+ // Height is the limiting factor
244
+ self.width = maxWidthByHeight
245
+ self.height = availableHeight
246
+ } else {
247
+ // Width is the limiting factor
248
+ self.width = availableWidth
249
+ self.height = maxHeightByWidth
250
+ }
251
+
252
+ print("[CameraPreview] Aspect ratio changed to \(newAspectRatio), new size: \(self.width!)x\(self.height!)")
253
+ }
254
+
255
+ self.updateCameraFrame()
256
+
257
+ // Return the actual preview bounds
258
+ var result = JSObject()
259
+ result["x"] = Double(self.previewView.frame.origin.x)
260
+ result["y"] = Double(self.previewView.frame.origin.y)
261
+ result["width"] = Double(self.previewView.frame.width)
262
+ result["height"] = Double(self.previewView.frame.height)
263
+ call.resolve(result)
264
+ }
265
+
266
+ @objc func getAspectRatio(_ call: CAPPluginCall) {
267
+ guard self.isInitialized else {
268
+ call.reject("camera not started")
269
+ return
270
+ }
271
+ call.resolve(["aspectRatio": self.aspectRatio ?? "4:3"])
272
+ }
273
+
274
+ @objc func setGridMode(_ call: CAPPluginCall) {
275
+ guard self.isInitialized else {
276
+ call.reject("camera not started")
277
+ return
278
+ }
279
+
280
+ guard let gridMode = call.getString("gridMode") else {
281
+ call.reject("gridMode parameter is required")
282
+ return
283
+ }
284
+
285
+ self.gridMode = gridMode
286
+
287
+ // Update grid overlay
288
+ DispatchQueue.main.async {
289
+ if gridMode == "none" {
290
+ self.cameraController.removeGridOverlay()
291
+ } else {
292
+ self.cameraController.addGridOverlay(to: self.previewView, gridMode: gridMode)
293
+ }
294
+ }
295
+
296
+ call.resolve()
297
+ }
298
+
299
+ @objc func getGridMode(_ call: CAPPluginCall) {
300
+ guard self.isInitialized else {
301
+ call.reject("camera not started")
302
+ return
303
+ }
304
+ call.resolve(["gridMode": self.gridMode])
305
+ }
306
+
155
307
  @objc func appDidBecomeActive() {
156
308
  if self.isInitialized {
157
309
  DispatchQueue.main.async {
@@ -159,7 +311,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
159
311
  }
160
312
  }
161
313
  }
162
-
314
+
315
+
163
316
  @objc func appWillEnterForeground() {
164
317
  if self.isInitialized {
165
318
  DispatchQueue.main.async {
@@ -266,18 +419,33 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
266
419
  self.highResolutionOutput = call.getBool("enableHighResolution") ?? false
267
420
  self.cameraController.highResolutionOutput = self.highResolutionOutput
268
421
 
269
- if call.getInt("width") != nil {
270
- self.width = CGFloat(call.getInt("width")!)
422
+ // Set width - use screen width if not provided or if 0
423
+ if let width = call.getInt("width"), width > 0 {
424
+ self.width = CGFloat(width)
271
425
  } else {
272
426
  self.width = UIScreen.main.bounds.size.width
273
427
  }
274
- if call.getInt("height") != nil {
275
- self.height = CGFloat(call.getInt("height")!)
428
+
429
+ // Set height - use screen height if not provided or if 0
430
+ if let height = call.getInt("height"), height > 0 {
431
+ self.height = CGFloat(height)
276
432
  } else {
277
433
  self.height = UIScreen.main.bounds.size.height
278
434
  }
279
- self.posX = call.getInt("x") != nil ? CGFloat(call.getInt("x")!)/UIScreen.main.scale: 0
280
- self.posY = call.getInt("y") != nil ? CGFloat(call.getInt("y")!) / (call.getBool("includeSafeAreaInsets") ?? false ? 1.0 : UIScreen.main.scale) + (call.getBool("includeSafeAreaInsets") ?? false ? UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0 : 0) : 0
435
+
436
+ // Set x position - use exact CSS pixel value from web view, or mark for centering
437
+ if let x = call.getInt("x") {
438
+ self.posX = CGFloat(x)
439
+ } else {
440
+ self.posX = -1 // Use -1 to indicate auto-centering
441
+ }
442
+
443
+ // Set y position - use exact CSS pixel value from web view, or mark for centering
444
+ if let y = call.getInt("y") {
445
+ self.posY = CGFloat(y)
446
+ } else {
447
+ self.posY = -1 // Use -1 to indicate auto-centering
448
+ }
281
449
  if call.getInt("paddingBottom") != nil {
282
450
  self.paddingBottom = CGFloat(call.getInt("paddingBottom")!)
283
451
  }
@@ -286,7 +454,17 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
286
454
  self.toBack = call.getBool("toBack") ?? true
287
455
  self.storeToFile = call.getBool("storeToFile") ?? false
288
456
  self.enableZoom = call.getBool("enableZoom") ?? false
289
- self.disableAudio = call.getBool("disableAudio") ?? false
457
+ self.disableAudio = call.getBool("disableAudio") ?? true
458
+ self.aspectRatio = call.getString("aspectRatio")
459
+ self.gridMode = call.getString("gridMode") ?? "none"
460
+ if self.aspectRatio != nil && (call.getInt("width") != nil || call.getInt("height") != nil) {
461
+ call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
462
+ return
463
+ }
464
+
465
+ print("[CameraPreview] Camera start parameters - aspectRatio: \(String(describing: self.aspectRatio)), gridMode: \(self.gridMode)")
466
+ print("[CameraPreview] Screen dimensions: \(UIScreen.main.bounds.size)")
467
+ print("[CameraPreview] Final frame dimensions - width: \(self.width), height: \(self.height), x: \(self.posX), y: \(self.posY)")
290
468
 
291
469
  AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
292
470
  guard granted else {
@@ -298,44 +476,79 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
298
476
  if self.cameraController.captureSession?.isRunning ?? false {
299
477
  call.reject("camera already started")
300
478
  } else {
301
- self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode) {error in
479
+ self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio) {error in
302
480
  if let error = error {
303
481
  print(error)
304
482
  call.reject(error.localizedDescription)
305
483
  return
306
484
  }
307
- let height = self.paddingBottom != nil ? self.height! - self.paddingBottom!: self.height!
308
- self.previewView = UIView(frame: CGRect(x: self.posX ?? 0, y: self.posY ?? 0, width: self.width!, height: height))
309
-
310
- // Make webview transparent - comprehensive approach
311
- self.makeWebViewTransparent()
312
-
313
- self.webView?.superview?.addSubview(self.previewView)
314
- if self.toBack! {
315
- self.webView?.superview?.bringSubviewToFront(self.webView!)
316
- }
317
- try? self.cameraController.displayPreview(on: self.previewView)
318
-
319
- let frontView = self.toBack! ? self.webView : self.previewView
320
- self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
321
-
322
- if self.rotateWhenOrientationChanged == true {
323
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
324
- }
325
-
326
- // Add observers for app state changes to maintain transparency
327
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
328
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
329
-
330
- self.isInitializing = false
331
- self.isInitialized = true
332
- call.resolve()
333
-
485
+ self.completeStartCamera(call: call)
334
486
  }
335
487
  }
336
488
  }
337
489
  })
490
+ }
491
+
492
+ override public func load() {
493
+ super.load()
494
+ // Initialize camera session in background for faster startup
495
+ prepareBackgroundCamera()
496
+ }
497
+
498
+ private func prepareBackgroundCamera() {
499
+ DispatchQueue.global(qos: .background).async {
500
+ AVCaptureDevice.requestAccess(for: .video) { granted in
501
+ guard granted else { return }
502
+
503
+ // Pre-initialize camera controller for faster startup
504
+ DispatchQueue.main.async {
505
+ self.cameraController.prepareBasicSession()
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ private func completeStartCamera(call: CAPPluginCall) {
512
+ // Create and configure the preview view first
513
+ self.updateCameraFrame()
514
+
515
+ // Make webview transparent - comprehensive approach
516
+ self.makeWebViewTransparent()
517
+
518
+ // Add the preview view to the webview itself to use same coordinate system
519
+ self.webView?.addSubview(self.previewView)
520
+ if self.toBack! {
521
+ self.webView?.sendSubviewToBack(self.previewView)
522
+ }
338
523
 
524
+ // Display the camera preview on the configured view
525
+ try? self.cameraController.displayPreview(on: self.previewView)
526
+
527
+ let frontView = self.toBack! ? self.webView : self.previewView
528
+ self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
529
+
530
+ // Add grid overlay if enabled
531
+ if self.gridMode != "none" {
532
+ self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
533
+ }
534
+
535
+ if self.rotateWhenOrientationChanged == true {
536
+ NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
537
+ }
538
+
539
+ // Add observers for app state changes to maintain transparency
540
+ NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
541
+ NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
542
+
543
+ self.isInitializing = false
544
+ self.isInitialized = true
545
+
546
+ var returnedObject = JSObject()
547
+ returnedObject["width"] = self.previewView.frame.width as any JSValue
548
+ returnedObject["height"] = self.previewView.frame.height as any JSValue
549
+ returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
550
+ returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
551
+ call.resolve(returnedObject)
339
552
  }
340
553
 
341
554
  @objc func flip(_ call: CAPPluginCall) {
@@ -363,13 +576,20 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
363
576
  try self.cameraController.switchCameras()
364
577
 
365
578
  DispatchQueue.main.async {
579
+ // Update preview layer frame without animation
580
+ CATransaction.begin()
581
+ CATransaction.setDisableActions(true)
366
582
  self.cameraController.previewLayer?.frame = self.previewView.bounds
367
583
  self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
368
- self.previewView.isUserInteractionEnabled = true
584
+ CATransaction.commit()
369
585
 
586
+ self.previewView.isUserInteractionEnabled = true
587
+
588
+
370
589
  // Ensure webview remains transparent after flip
371
590
  self.makeWebViewTransparent()
372
-
591
+
592
+
373
593
  call.resolve()
374
594
  }
375
595
  } catch {
@@ -406,6 +626,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
406
626
  }
407
627
 
408
628
  // Always attempt to stop and clean up, regardless of captureSession state
629
+ self.cameraController.removeGridOverlay()
409
630
  if let previewView = self.previewView {
410
631
  previewView.removeFromSuperview()
411
632
  self.previewView = nil
@@ -415,7 +636,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
415
636
  self.isInitialized = false
416
637
  self.isInitializing = false
417
638
  self.cameraController.cleanup()
418
-
639
+
640
+
419
641
  // Remove notification observers
420
642
  NotificationCenter.default.removeObserver(self)
421
643
 
@@ -436,41 +658,181 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
436
658
  @objc func capture(_ call: CAPPluginCall) {
437
659
  DispatchQueue.main.async {
438
660
 
439
- let quality: Int? = call.getInt("quality", 85)
440
-
441
- self.cameraController.captureImage { (image, error) in
661
+ let quality = call.getFloat("quality", 85)
662
+ let saveToGallery = call.getBool("saveToGallery", false)
663
+ let withExifLocation = call.getBool("withExifLocation", false)
664
+ let width = call.getInt("width")
665
+ let height = call.getInt("height")
666
+
667
+ if withExifLocation {
668
+ self.locationManager = CLLocationManager()
669
+ self.locationManager?.delegate = self
670
+ self.locationManager?.requestWhenInUseAuthorization()
671
+ self.locationManager?.startUpdatingLocation()
672
+ }
442
673
 
443
- guard let image = image else {
444
- print(error ?? "Image capture error")
445
- guard let error = error else {
446
- call.reject("Image capture error")
447
- return
448
- }
674
+ self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
675
+ if let error = error {
449
676
  call.reject(error.localizedDescription)
450
677
  return
451
678
  }
452
- let imageData: Data?
453
- if self.cameraController.currentCameraPosition == .front {
454
- let flippedImage = image.withHorizontallyFlippedOrientation()
455
- imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
456
- } else {
457
- imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
679
+
680
+ var gallerySuccess = true
681
+ var galleryError: String?
682
+
683
+ let group = DispatchGroup()
684
+
685
+ group.notify(queue: .main) {
686
+ guard let imageDataWithExif = self.createImageDataWithExif(from: image!, quality: Int(quality), location: withExifLocation ? self.currentLocation : nil) else {
687
+ call.reject("Failed to create image data with EXIF")
688
+ return
458
689
  }
459
690
 
460
- if self.storeToFile == false {
461
- let imageBase64 = imageData?.base64EncodedString()
462
- call.resolve(["value": imageBase64!])
463
- } else {
464
- do {
465
- let fileUrl=self.getTempFilePath()
466
- try imageData?.write(to: fileUrl)
467
- call.resolve(["value": fileUrl.absoluteString])
468
- } catch {
469
- call.reject("error writing image to file")
691
+ if saveToGallery {
692
+ group.enter()
693
+ self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
694
+ gallerySuccess = success
695
+ if !success {
696
+ galleryError = error?.localizedDescription ?? "Unknown error"
697
+ print("CameraPreview: Error saving image to gallery: \(galleryError!)")
698
+ }
699
+ group.leave()
470
700
  }
701
+
702
+ group.notify(queue: .main) {
703
+ let exifData = self.getExifData(from: imageDataWithExif)
704
+ let base64Image = imageDataWithExif.base64EncodedString()
705
+
706
+ var result = JSObject()
707
+ result["value"] = base64Image
708
+ result["exif"] = exifData
709
+ result["gallerySaved"] = gallerySuccess
710
+ if !gallerySuccess, let error = galleryError {
711
+ result["galleryError"] = error
712
+ }
713
+
714
+ call.resolve(result)
715
+ }
716
+ } else {
717
+ let exifData = self.getExifData(from: imageDataWithExif)
718
+ let base64Image = imageDataWithExif.base64EncodedString()
719
+
720
+ var result = JSObject()
721
+ result["value"] = base64Image
722
+ result["exif"] = exifData
723
+
724
+ call.resolve(result)
471
725
  }
472
726
  }
727
+ }
728
+ }
729
+ }
730
+
731
+ private func getExifData(from imageData: Data) -> JSObject {
732
+ guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil),
733
+ let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
734
+ let exifDict = imageProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] else {
735
+ return [:]
736
+ }
737
+
738
+
739
+ var exifData = JSObject()
740
+ for (key, value) in exifDict {
741
+ // Convert value to JSValue-compatible type
742
+ if let stringValue = value as? String {
743
+ exifData[key] = stringValue
744
+ } else if let numberValue = value as? NSNumber {
745
+ exifData[key] = numberValue
746
+ } else if let boolValue = value as? Bool {
747
+ exifData[key] = boolValue
748
+ } else if let arrayValue = value as? [Any] {
749
+ exifData[key] = arrayValue
750
+ } else if let dictValue = value as? [String: Any] {
751
+ exifData[key] = JSObject(_immutableCocoaDictionary: NSMutableDictionary(dictionary: dictValue))
752
+ } else {
753
+ // Convert other types to string as fallback
754
+ exifData[key] = String(describing: value)
755
+ }
756
+ }
757
+
758
+
759
+ return exifData
760
+ }
761
+
762
+ private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?) -> Data? {
763
+ guard let originalImageData = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
764
+ return nil
765
+ }
766
+
767
+ guard let imageSource = CGImageSourceCreateWithData(originalImageData as CFData, nil),
768
+ let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
769
+ let cgImage = image.cgImage else {
770
+ return originalImageData
771
+ }
772
+
773
+ let mutableData = NSMutableData()
774
+ guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
775
+ return originalImageData
776
+ }
777
+
778
+ var finalProperties = imageProperties
779
+
780
+ // Add GPS location if available
781
+ if let location = location {
782
+ let formatter = DateFormatter()
783
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
784
+ formatter.timeZone = TimeZone(abbreviation: "UTC")
785
+
786
+ let gpsDict: [String: Any] = [
787
+ kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
788
+ kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
789
+ kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
790
+ kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
791
+ kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
792
+ kCGImagePropertyGPSAltitude as String: location.altitude,
793
+ kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
794
+ ]
795
+
796
+ finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
473
797
  }
798
+
799
+ // Add lens information
800
+ do {
801
+ let currentZoom = try self.cameraController.getZoom()
802
+ let lensInfo = try self.cameraController.getCurrentLensInfo()
803
+
804
+ // Create or update EXIF dictionary
805
+ var exifDict = finalProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] ?? [:]
806
+
807
+ // Add focal length (in mm)
808
+ exifDict[kCGImagePropertyExifFocalLength as String] = lensInfo.focalLength
809
+
810
+ // Add digital zoom ratio
811
+ let digitalZoom = Float(currentZoom.current) / lensInfo.baseZoomRatio
812
+ exifDict[kCGImagePropertyExifDigitalZoomRatio as String] = digitalZoom
813
+
814
+ // Add lens model info
815
+ exifDict[kCGImagePropertyExifLensModel as String] = lensInfo.deviceType
816
+
817
+ finalProperties[kCGImagePropertyExifDictionary as String] = exifDict
818
+
819
+ // Create or update TIFF dictionary for device info
820
+ var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
821
+ tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
822
+ tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
823
+ finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
824
+
825
+ } catch {
826
+ print("CameraPreview: Failed to get lens information: \(error)")
827
+ }
828
+
829
+ CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
830
+
831
+ if CGImageDestinationFinalize(destination) {
832
+ return mutableData as Data
833
+ }
834
+
835
+ return originalImageData
474
836
  }
475
837
 
476
838
  @objc func captureSample(_ call: CAPPluginCall) {
@@ -612,9 +974,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
612
974
  // Collect all devices by position
613
975
  for device in session.devices {
614
976
  var lenses: [[String: Any]] = []
615
-
977
+
978
+
616
979
  let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
617
-
980
+
981
+
618
982
  for lensDevice in constituentDevices {
619
983
  var deviceType: String
620
984
  switch lensDevice.deviceType {
@@ -634,7 +998,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
634
998
  } else if lensDevice.deviceType == .builtInTelephotoCamera {
635
999
  baseZoomRatio = 2.0 // A common value for telephoto lenses
636
1000
  }
637
-
1001
+
1002
+
638
1003
  let lensInfo: [String: Any] = [
639
1004
  "label": lensDevice.localizedName,
640
1005
  "deviceType": deviceType,
@@ -645,7 +1010,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
645
1010
  ]
646
1011
  lenses.append(lensInfo)
647
1012
  }
648
-
1013
+
1014
+
649
1015
  let deviceData: [String: Any] = [
650
1016
  "deviceId": device.uniqueID,
651
1017
  "label": device.localizedName,
@@ -655,7 +1021,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
655
1021
  "maxZoom": Float(device.maxAvailableVideoZoomFactor),
656
1022
  "isLogical": device.isVirtualDevice
657
1023
  ]
658
-
1024
+
1025
+
659
1026
  devices.append(deviceData)
660
1027
  }
661
1028
 
@@ -671,7 +1038,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
671
1038
  do {
672
1039
  let zoomInfo = try self.cameraController.getZoom()
673
1040
  let lensInfo = try self.cameraController.getCurrentLensInfo()
674
-
1041
+
1042
+
675
1043
  var minZoom = zoomInfo.min
676
1044
  var maxZoom = zoomInfo.max
677
1045
  var currentZoom = zoomInfo.current
@@ -764,13 +1132,20 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
764
1132
  try self.cameraController.swapToDevice(deviceId: deviceId)
765
1133
 
766
1134
  DispatchQueue.main.async {
1135
+ // Update preview layer frame without animation
1136
+ CATransaction.begin()
1137
+ CATransaction.setDisableActions(true)
767
1138
  self.cameraController.previewLayer?.frame = self.previewView.bounds
768
1139
  self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
769
- self.previewView.isUserInteractionEnabled = true
1140
+ CATransaction.commit()
770
1141
 
1142
+ self.previewView.isUserInteractionEnabled = true
1143
+
1144
+
771
1145
  // Ensure webview remains transparent after device switch
772
1146
  self.makeWebViewTransparent()
773
-
1147
+
1148
+
774
1149
  call.resolve()
775
1150
  }
776
1151
  } catch {
@@ -797,6 +1172,198 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
797
1172
  }
798
1173
  }
799
1174
 
1175
+ public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
1176
+ self.currentLocation = locations.last
1177
+ self.locationManager?.stopUpdatingLocation()
1178
+ }
1179
+
1180
+ public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
1181
+ print("CameraPreview: Failed to get location: \(error.localizedDescription)")
1182
+ }
1183
+
1184
+ private func saveImageDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
1185
+ // Check if NSPhotoLibraryUsageDescription is present in Info.plist
1186
+ guard Bundle.main.object(forInfoDictionaryKey: "NSPhotoLibraryUsageDescription") != nil else {
1187
+ let error = NSError(domain: "CameraPreview", code: 2, userInfo: [
1188
+ NSLocalizedDescriptionKey: "NSPhotoLibraryUsageDescription key missing from Info.plist. Add this key with a description of how your app uses photo library access."
1189
+ ])
1190
+ completion(false, error)
1191
+ return
1192
+ }
1193
+
1194
+ let status = PHPhotoLibrary.authorizationStatus()
1195
+
1196
+ switch status {
1197
+ case .authorized:
1198
+ performSaveDataToGallery(imageData: imageData, completion: completion)
1199
+ case .notDetermined:
1200
+ PHPhotoLibrary.requestAuthorization { newStatus in
1201
+ DispatchQueue.main.async {
1202
+ if newStatus == .authorized {
1203
+ self.performSaveDataToGallery(imageData: imageData, completion: completion)
1204
+ } else {
1205
+ completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
1206
+ }
1207
+ }
1208
+ }
1209
+ case .denied, .restricted:
1210
+ completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
1211
+ case .limited:
1212
+ performSaveDataToGallery(imageData: imageData, completion: completion)
1213
+ @unknown default:
1214
+ completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown photo library authorization status"]))
1215
+ }
1216
+ }
1217
+
1218
+ private func performSaveDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
1219
+ // Create a temporary file to write the JPEG data with EXIF
1220
+ let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".jpg")
1221
+
1222
+ do {
1223
+ try imageData.write(to: tempURL)
1224
+
1225
+ PHPhotoLibrary.shared().performChanges({
1226
+ PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: tempURL)
1227
+ }, completionHandler: { success, error in
1228
+ // Clean up temporary file
1229
+ try? FileManager.default.removeItem(at: tempURL)
1230
+
1231
+ DispatchQueue.main.async {
1232
+ completion(success, error)
1233
+ }
1234
+ })
1235
+ } catch {
1236
+ DispatchQueue.main.async {
1237
+ completion(false, error)
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ private func updateCameraFrame() {
1243
+ guard let width = self.width, var height = self.height, let posX = self.posX, let posY = self.posY else {
1244
+ return
1245
+ }
1246
+
1247
+ let paddingBottom = self.paddingBottom ?? 0
1248
+ height -= paddingBottom
1249
+
1250
+ // Cache webView dimensions for performance
1251
+ let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
1252
+ let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
800
1253
 
1254
+ var finalX = posX
1255
+ var finalY = posY
1256
+ var finalWidth = width
1257
+ var finalHeight = height
801
1258
 
1259
+ // Handle auto-centering when position is -1
1260
+ if posX == -1 || posY == -1 {
1261
+ finalWidth = webViewWidth
1262
+
1263
+ // Calculate height based on aspect ratio or use provided height
1264
+ if let aspectRatio = self.aspectRatio {
1265
+ let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
1266
+ if ratioParts.count == 2 {
1267
+ // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1268
+ let ratio = ratioParts[1] / ratioParts[0]
1269
+ finalHeight = finalWidth / CGFloat(ratio)
1270
+ }
1271
+ }
1272
+
1273
+ finalX = posX == -1 ? 0 : posX
1274
+
1275
+ if posY == -1 {
1276
+ let availableHeight = webViewHeight - paddingBottom
1277
+ finalY = finalHeight < availableHeight ? (availableHeight - finalHeight) / 2 : 0
1278
+ }
1279
+ }
1280
+
1281
+ var frame = CGRect(x: finalX, y: finalY, width: finalWidth, height: finalHeight)
1282
+
1283
+ // Apply aspect ratio adjustments only if not auto-centering
1284
+ if posX != -1 && posY != -1, let aspectRatio = self.aspectRatio {
1285
+ let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
1286
+ if ratioParts.count == 2 {
1287
+ // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1288
+ let ratio = ratioParts[1] / ratioParts[0]
1289
+ let currentRatio = Double(finalWidth) / Double(finalHeight)
1290
+
1291
+ if currentRatio > ratio {
1292
+ let newWidth = Double(finalHeight) * ratio
1293
+ frame.origin.x = finalX + (Double(finalWidth) - newWidth) / 2
1294
+ frame.size.width = CGFloat(newWidth)
1295
+ } else {
1296
+ let newHeight = Double(finalWidth) / ratio
1297
+ frame.origin.y = finalY + (Double(finalHeight) - newHeight) / 2
1298
+ frame.size.height = CGFloat(newHeight)
1299
+ }
1300
+ }
1301
+ }
1302
+
1303
+ // Disable ALL animations for frame updates - we want instant positioning
1304
+ CATransaction.begin()
1305
+ CATransaction.setDisableActions(true)
1306
+
1307
+ // Batch UI updates for better performance
1308
+ if self.previewView == nil {
1309
+ self.previewView = UIView(frame: frame)
1310
+ self.previewView.backgroundColor = UIColor.clear
1311
+ } else {
1312
+ self.previewView.frame = frame
1313
+ }
1314
+
1315
+ // Update preview layer frame efficiently
1316
+ if let previewLayer = self.cameraController.previewLayer {
1317
+ previewLayer.frame = self.previewView.bounds
1318
+ }
1319
+
1320
+ // Update grid overlay frame if it exists
1321
+ if let gridOverlay = self.cameraController.gridOverlayView {
1322
+ gridOverlay.frame = self.previewView.bounds
1323
+ }
1324
+
1325
+ CATransaction.commit()
1326
+ }
1327
+
1328
+ @objc func getPreviewSize(_ call: CAPPluginCall) {
1329
+ guard self.isInitialized else {
1330
+ call.reject("camera not started")
1331
+ return
1332
+ }
1333
+ var result = JSObject()
1334
+ result["x"] = Double(self.previewView.frame.origin.x)
1335
+ result["y"] = Double(self.previewView.frame.origin.y)
1336
+ result["width"] = Double(self.previewView.frame.width)
1337
+ result["height"] = Double(self.previewView.frame.height)
1338
+ call.resolve(result)
1339
+ }
1340
+
1341
+ @objc func setPreviewSize(_ call: CAPPluginCall) {
1342
+ guard self.isInitialized else {
1343
+ call.reject("camera not started")
1344
+ return
1345
+ }
1346
+
1347
+ // Only update position if explicitly provided, otherwise keep auto-centering
1348
+ if let x = call.getInt("x") {
1349
+ self.posX = CGFloat(x)
1350
+ }
1351
+ if let y = call.getInt("y") {
1352
+ self.posY = CGFloat(y)
1353
+ }
1354
+ if let width = call.getInt("width") { self.width = CGFloat(width) }
1355
+ if let height = call.getInt("height") { self.height = CGFloat(height) }
1356
+
1357
+ // Direct update without animation for better performance
1358
+ self.updateCameraFrame()
1359
+ self.makeWebViewTransparent()
1360
+
1361
+ // Return the actual preview bounds
1362
+ var result = JSObject()
1363
+ result["x"] = Double(self.previewView.frame.origin.x)
1364
+ result["y"] = Double(self.previewView.frame.origin.y)
1365
+ result["width"] = Double(self.previewView.frame.width)
1366
+ result["height"] = Double(self.previewView.frame.height)
1367
+ call.resolve(result)
1368
+ }
802
1369
  }