@capgo/camera-preview 7.4.0-beta.7 → 7.4.0-beta.9

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 (41) hide show
  1. package/README.md +63 -29
  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/buildOutputCleanup/cache.properties +1 -1
  12. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  13. package/android/.gradle/file-system.probe +0 -0
  14. package/android/build.gradle +1 -0
  15. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +127 -14
  16. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +529 -29
  17. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +2 -0
  18. package/dist/docs.json +46 -7
  19. package/dist/esm/definitions.d.ts +42 -5
  20. package/dist/esm/definitions.js.map +1 -1
  21. package/dist/esm/web.d.ts +25 -1
  22. package/dist/esm/web.js +81 -9
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +81 -9
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +81 -9
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Sources/CapgoCameraPreview/CameraController.swift +95 -18
  29. package/ios/Sources/CapgoCameraPreview/Plugin.swift +449 -111
  30. package/package.json +1 -1
  31. package/android/.gradle/config.properties +0 -2
  32. package/android/.idea/AndroidProjectSystem.xml +0 -6
  33. package/android/.idea/caches/deviceStreaming.xml +0 -811
  34. package/android/.idea/compiler.xml +0 -6
  35. package/android/.idea/gradle.xml +0 -18
  36. package/android/.idea/migrations.xml +0 -10
  37. package/android/.idea/misc.xml +0 -10
  38. package/android/.idea/runConfigurations.xml +0 -17
  39. package/android/.idea/vcs.xml +0 -6
  40. package/android/.idea/workspace.xml +0 -55
  41. package/android/local.properties +0 -8
@@ -4,6 +4,7 @@ import AVFoundation
4
4
  import Photos
5
5
  import CoreImage
6
6
  import CoreLocation
7
+ import MobileCoreServices
7
8
 
8
9
 
9
10
  extension UIWindow {
@@ -62,11 +63,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
62
63
  CAPPluginMethod(name: "setAspectRatio", returnType: CAPPluginReturnPromise),
63
64
  CAPPluginMethod(name: "getAspectRatio", returnType: CAPPluginReturnPromise),
64
65
  CAPPluginMethod(name: "setGridMode", returnType: CAPPluginReturnPromise),
65
- CAPPluginMethod(name: "getGridMode", returnType: CAPPluginReturnPromise)
66
+ CAPPluginMethod(name: "getGridMode", returnType: CAPPluginReturnPromise),
67
+ CAPPluginMethod(name: "getPreviewSize", returnType: CAPPluginReturnPromise),
68
+ CAPPluginMethod(name: "setPreviewSize", returnType: CAPPluginReturnPromise)
66
69
  ]
67
70
  // Camera state tracking
68
71
  private var isInitializing: Bool = false
69
72
  private var isInitialized: Bool = false
73
+ private var backgroundSession: AVCaptureSession?
70
74
 
71
75
  var previewView: UIView!
72
76
  var cameraPosition = String()
@@ -138,14 +142,26 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
138
142
  let paddingBottom = self.paddingBottom ?? 0
139
143
  let height = heightValue - paddingBottom
140
144
 
141
- if UIWindow.isLandscape {
142
- previewView.frame = CGRect(x: posY, y: posX, width: max(height, width), height: min(height, width))
143
- self.cameraController.previewLayer?.frame = previewView.frame
144
- }
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
+ }
145
158
 
146
- if UIWindow.isPortrait {
147
- previewView.frame = CGRect(x: posX, y: posY, width: min(height, width), height: max(height, width))
148
- 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()
149
165
  }
150
166
 
151
167
  if let connection = self.cameraController.fileVideoOutput?.connection(with: .video) {
@@ -165,11 +181,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
165
181
 
166
182
  cameraController.updateVideoOrientation()
167
183
 
168
- cameraController.updateVideoOrientation()
184
+ cameraController.updateVideoOrientation()
169
185
 
170
- // Update grid overlay frame if it exists
186
+ // Update grid overlay frame if it exists - no animation
171
187
  if let gridOverlay = self.cameraController.gridOverlayView {
188
+ CATransaction.begin()
189
+ CATransaction.setDisableActions(true)
172
190
  gridOverlay.frame = previewView.bounds
191
+ CATransaction.commit()
173
192
  }
174
193
 
175
194
  // Ensure webview remains transparent after rotation
@@ -183,9 +202,65 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
183
202
  call.reject("camera not started")
184
203
  return
185
204
  }
186
- self.aspectRatio = call.getString("aspectRatio")
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
+
187
255
  self.updateCameraFrame()
188
- call.resolve()
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)
189
264
  }
190
265
 
191
266
  @objc func getAspectRatio(_ call: CAPPluginCall) {
@@ -358,18 +433,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
358
433
  self.height = UIScreen.main.bounds.size.height
359
434
  }
360
435
 
361
- // Set x position - use 0 if not provided
362
- if let x = call.getInt("x"), x > 0 {
363
- self.posX = CGFloat(x) / UIScreen.main.scale
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)
364
439
  } else {
365
- self.posX = 0
440
+ self.posX = -1 // Use -1 to indicate auto-centering
366
441
  }
367
442
 
368
- // Set y position - use 0 if not provided
369
- if let y = call.getInt("y"), y > 0 {
370
- self.posY = CGFloat(y) / (call.getBool("includeSafeAreaInsets") ?? false ? 1.0 : UIScreen.main.scale) + (call.getBool("includeSafeAreaInsets") ?? false ? UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0 : 0)
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)
371
446
  } else {
372
- self.posY = 0
447
+ self.posY = -1 // Use -1 to indicate auto-centering
373
448
  }
374
449
  if call.getInt("paddingBottom") != nil {
375
450
  self.paddingBottom = CGFloat(call.getInt("paddingBottom")!)
@@ -382,6 +457,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
382
457
  self.disableAudio = call.getBool("disableAudio") ?? true
383
458
  self.aspectRatio = call.getString("aspectRatio")
384
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
+ }
385
464
 
386
465
  print("[CameraPreview] Camera start parameters - aspectRatio: \(String(describing: self.aspectRatio)), gridMode: \(self.gridMode)")
387
466
  print("[CameraPreview] Screen dimensions: \(UIScreen.main.bounds.size)")
@@ -403,54 +482,73 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
403
482
  call.reject(error.localizedDescription)
404
483
  return
405
484
  }
485
+ self.completeStartCamera(call: call)
486
+ }
487
+ }
488
+ }
489
+ })
490
+ }
406
491
 
407
- // Create and configure the preview view first
408
- self.updateCameraFrame()
409
-
410
- // Make webview transparent - comprehensive approach
411
- self.makeWebViewTransparent()
492
+ override public func load() {
493
+ super.load()
494
+ // Initialize camera session in background for faster startup
495
+ prepareBackgroundCamera()
496
+ }
412
497
 
413
- // Add the preview view to the webview's superview
414
- self.webView?.superview?.addSubview(self.previewView)
415
- if self.toBack! {
416
- self.webView?.superview?.bringSubviewToFront(self.webView!)
417
- }
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
+ }
418
510
 
419
- // Display the camera preview on the configured view
420
- try? self.cameraController.displayPreview(on: self.previewView)
511
+ private func completeStartCamera(call: CAPPluginCall) {
512
+ // Create and configure the preview view first
513
+ self.updateCameraFrame()
421
514
 
422
- let frontView = self.toBack! ? self.webView : self.previewView
423
- self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
515
+ // Make webview transparent - comprehensive approach
516
+ self.makeWebViewTransparent()
424
517
 
425
- // Add grid overlay if enabled
426
- if self.gridMode != "none" {
427
- self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
428
- }
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
+ }
429
523
 
430
- if self.rotateWhenOrientationChanged == true {
431
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
432
- }
524
+ // Display the camera preview on the configured view
525
+ try? self.cameraController.displayPreview(on: self.previewView)
433
526
 
527
+ let frontView = self.toBack! ? self.webView : self.previewView
528
+ self.cameraController.setupGestures(target: frontView ?? self.previewView, enableZoom: self.enableZoom!)
434
529
 
435
- // Add observers for app state changes to maintain transparency
436
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
437
- NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
530
+ // Add grid overlay if enabled
531
+ if self.gridMode != "none" {
532
+ self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
533
+ }
438
534
 
439
- self.isInitializing = false
440
- self.isInitialized = true
535
+ if self.rotateWhenOrientationChanged == true {
536
+ NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
537
+ }
441
538
 
442
- var returnedObject = JSObject()
443
- returnedObject["width"] = self.previewView.frame.width as any JSValue
444
- returnedObject["height"] = self.previewView.frame.height as any JSValue
445
- returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
446
- returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
447
- call.resolve(returnedObject)
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)
448
542
 
449
- }
450
- }
451
- }
452
- })
543
+ self.isInitializing = false
544
+ self.isInitialized = true
453
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)
454
552
  }
455
553
 
456
554
  @objc func flip(_ call: CAPPluginCall) {
@@ -478,8 +576,13 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
478
576
  try self.cameraController.switchCameras()
479
577
 
480
578
  DispatchQueue.main.async {
579
+ // Update preview layer frame without animation
580
+ CATransaction.begin()
581
+ CATransaction.setDisableActions(true)
481
582
  self.cameraController.previewLayer?.frame = self.previewView.bounds
482
583
  self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
584
+ CATransaction.commit()
585
+
483
586
  self.previewView.isUserInteractionEnabled = true
484
587
 
485
588
 
@@ -574,30 +677,53 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
574
677
  return
575
678
  }
576
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
689
+ }
690
+
577
691
  if saveToGallery {
578
- PHPhotoLibrary.shared().performChanges({
579
- PHAssetChangeRequest.creationRequestForAsset(from: image!)
580
- }, completionHandler: { (success, error) in
692
+ group.enter()
693
+ self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
694
+ gallerySuccess = success
581
695
  if !success {
582
- print("CameraPreview: Error saving image to gallery: \(error?.localizedDescription ?? "Unknown error")")
696
+ galleryError = error?.localizedDescription ?? "Unknown error"
697
+ print("CameraPreview: Error saving image to gallery: \(galleryError!)")
583
698
  }
584
- })
585
- }
586
-
587
- guard let imageData = image?.jpegData(compressionQuality: CGFloat(quality / 100.0)) else {
588
- call.reject("Failed to get JPEG data from image")
589
- return
699
+ group.leave()
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)
590
725
  }
591
-
592
-
593
- let exifData = self.getExifData(from: imageData)
594
- let base64Image = imageData.base64EncodedString()
595
-
596
-
597
- var result = JSObject()
598
- result["value"] = base64Image
599
- result["exif"] = exifData
600
- call.resolve(result)
726
+ }
601
727
  }
602
728
  }
603
729
  }
@@ -633,6 +759,82 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
633
759
  return exifData
634
760
  }
635
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
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
836
+ }
837
+
636
838
  @objc func captureSample(_ call: CAPPluginCall) {
637
839
  DispatchQueue.main.async {
638
840
  let quality: Int? = call.getInt("quality", 85)
@@ -930,8 +1132,13 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
930
1132
  try self.cameraController.swapToDevice(deviceId: deviceId)
931
1133
 
932
1134
  DispatchQueue.main.async {
1135
+ // Update preview layer frame without animation
1136
+ CATransaction.begin()
1137
+ CATransaction.setDisableActions(true)
933
1138
  self.cameraController.previewLayer?.frame = self.previewView.bounds
934
1139
  self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
1140
+ CATransaction.commit()
1141
+
935
1142
  self.previewView.isUserInteractionEnabled = true
936
1143
 
937
1144
 
@@ -974,58 +1181,189 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
974
1181
  print("CameraPreview: Failed to get location: \(error.localizedDescription)")
975
1182
  }
976
1183
 
977
- private func updateCameraFrame() {
978
- print("[CameraPreview] updateCameraFrame called")
979
- print("[CameraPreview] width: \(String(describing: self.width)), height: \(String(describing: self.height)), posX: \(String(describing: self.posX)), posY: \(String(describing: self.posY))")
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
+ }
980
1241
 
1242
+ private func updateCameraFrame() {
981
1243
  guard let width = self.width, var height = self.height, let posX = self.posX, let posY = self.posY else {
982
- print("[CameraPreview] Missing required frame parameters")
983
1244
  return
984
1245
  }
985
1246
 
986
1247
  let paddingBottom = self.paddingBottom ?? 0
987
1248
  height -= paddingBottom
988
1249
 
989
- var frame = CGRect(x: posX, y: posY, width: width, height: height)
990
- print("[CameraPreview] Initial frame: \(frame)")
991
-
992
- // Temporarily disable aspect ratio frame adjustment to debug black preview issue
993
- /*
994
- if let aspectRatio = self.aspectRatio {
995
- let ratioParts = aspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
996
- let ratio = ratioParts[0] / ratioParts[1]
997
- let viewWidth = Double(width)
998
- let viewHeight = Double(height)
999
-
1000
- print("[CameraPreview] Calculating aspect ratio frame: \(aspectRatio), ratio: \(ratio), viewSize: \(viewWidth)x\(viewHeight)")
1001
-
1002
- if viewWidth / ratio > viewHeight {
1003
- let newWidth = viewHeight * ratio
1004
- frame.origin.x += (viewWidth - newWidth) / 2
1005
- frame.size.width = newWidth
1006
- print("[CameraPreview] Adjusted width: \(newWidth)")
1007
- } else {
1008
- let newHeight = viewWidth / ratio
1009
- frame.origin.y += (viewHeight - newHeight) / 2
1010
- frame.size.height = newHeight
1011
- print("[CameraPreview] Adjusted height: \(newHeight)")
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
1253
+
1254
+ var finalX = posX
1255
+ var finalY = posY
1256
+ var finalWidth = width
1257
+ var finalHeight = height
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
1012
1278
  }
1013
1279
  }
1014
- */
1015
1280
 
1016
- print("[CameraPreview] Final calculated frame: \(frame)")
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
+ }
1017
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
1018
1308
  if self.previewView == nil {
1019
- print("[CameraPreview] Creating new preview view with frame: \(frame)")
1020
1309
  self.previewView = UIView(frame: frame)
1021
- self.previewView.backgroundColor = UIColor.black // Add background color for debugging
1310
+ self.previewView.backgroundColor = UIColor.clear
1022
1311
  } else {
1023
- print("[CameraPreview] Updating existing preview view frame to: \(frame)")
1024
1312
  self.previewView.frame = frame
1025
1313
  }
1026
1314
 
1027
- // Update the preview layer frame to match the preview view
1028
- self.cameraController.previewLayer?.frame = frame
1029
- print("[CameraPreview] Set preview layer frame to: \(frame)")
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)
1030
1368
  }
1031
1369
  }