@capgo/camera-preview 7.4.0-alpha.3 → 7.4.0-alpha.32

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.
@@ -55,6 +55,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
55
55
  CAPPluginMethod(name: "isRunning", returnType: CAPPluginReturnPromise),
56
56
  CAPPluginMethod(name: "getAvailableDevices", returnType: CAPPluginReturnPromise),
57
57
  CAPPluginMethod(name: "getZoom", returnType: CAPPluginReturnPromise),
58
+ CAPPluginMethod(name: "getZoomButtonValues", returnType: CAPPluginReturnPromise),
58
59
  CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
59
60
  CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
60
61
  CAPPluginMethod(name: "setDeviceId", returnType: CAPPluginReturnPromise),
@@ -65,7 +66,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
65
66
  CAPPluginMethod(name: "getGridMode", returnType: CAPPluginReturnPromise),
66
67
  CAPPluginMethod(name: "getPreviewSize", returnType: CAPPluginReturnPromise),
67
68
  CAPPluginMethod(name: "setPreviewSize", returnType: CAPPluginReturnPromise),
68
- CAPPluginMethod(name: "setFocus", returnType: CAPPluginReturnPromise)
69
+ CAPPluginMethod(name: "setFocus", returnType: CAPPluginReturnPromise),
70
+ CAPPluginMethod(name: "deleteFile", returnType: CAPPluginReturnPromise),
71
+ CAPPluginMethod(name: "getOrientation", returnType: CAPPluginReturnPromise),
72
+ CAPPluginMethod(name: "getSafeAreaInsets", returnType: CAPPluginReturnPromise)
73
+
69
74
  ]
70
75
  // Camera state tracking
71
76
  private var isInitializing: Bool = false
@@ -93,6 +98,44 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
93
98
  private var permissionCallID: String?
94
99
  private var waitingForLocation: Bool = false
95
100
 
101
+ // MARK: - Helper Methods for Aspect Ratio
102
+
103
+ /// Validates that aspectRatio and size (width/height) are not both set
104
+ private func validateAspectRatioParameters(aspectRatio: String?, width: Int?, height: Int?) -> String? {
105
+ if aspectRatio != nil && (width != nil || height != nil) {
106
+ return "Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."
107
+ }
108
+ return nil
109
+ }
110
+
111
+ /// Parses aspect ratio string and returns the appropriate ratio for the current orientation
112
+ private func parseAspectRatio(_ ratio: String, isPortrait: Bool) -> CGFloat {
113
+ let parts = ratio.split(separator: ":").compactMap { Double($0) }
114
+ guard parts.count == 2 else { return 1.0 }
115
+
116
+ // For camera (portrait), we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
117
+ return isPortrait ?
118
+ CGFloat(parts[1] / parts[0]) :
119
+ CGFloat(parts[0] / parts[1])
120
+ }
121
+
122
+ /// Calculates dimensions based on aspect ratio and available space
123
+ private func calculateDimensionsForAspectRatio(_ aspectRatio: String, availableWidth: CGFloat, availableHeight: CGFloat, isPortrait: Bool) -> (width: CGFloat, height: CGFloat) {
124
+ let ratio = parseAspectRatio(aspectRatio, isPortrait: isPortrait)
125
+
126
+ // Calculate maximum size that fits the aspect ratio in available space
127
+ let maxWidthByHeight = availableHeight * ratio
128
+ let maxHeightByWidth = availableWidth / ratio
129
+
130
+ if maxWidthByHeight <= availableWidth {
131
+ // Height is the limiting factor
132
+ return (width: maxWidthByHeight, height: availableHeight)
133
+ } else {
134
+ // Width is the limiting factor
135
+ return (width: availableWidth, height: maxHeightByWidth)
136
+ }
137
+ }
138
+
96
139
  // MARK: - Transparency Methods
97
140
 
98
141
  private func makeWebViewTransparent() {
@@ -126,6 +169,103 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
126
169
  }
127
170
  }
128
171
 
172
+ @objc func getZoomButtonValues(_ call: CAPPluginCall) {
173
+ guard isInitialized else {
174
+ call.reject("Camera not initialized")
175
+ return
176
+ }
177
+
178
+ // Determine current device based on active position
179
+ var currentDevice: AVCaptureDevice?
180
+ switch self.cameraController.currentCameraPosition {
181
+ case .front:
182
+ currentDevice = self.cameraController.frontCamera
183
+ case .rear:
184
+ currentDevice = self.cameraController.rearCamera
185
+ default:
186
+ currentDevice = nil
187
+ }
188
+
189
+ guard let device = currentDevice else {
190
+ call.reject("No active camera device")
191
+ return
192
+ }
193
+
194
+ var hasUltraWide = false
195
+ var hasWide = false
196
+ var hasTele = false
197
+
198
+ let lenses = device.isVirtualDevice ? device.constituentDevices : [device]
199
+ for lens in lenses {
200
+ switch lens.deviceType {
201
+ case .builtInUltraWideCamera:
202
+ hasUltraWide = true
203
+ case .builtInWideAngleCamera:
204
+ hasWide = true
205
+ case .builtInTelephotoCamera:
206
+ hasTele = true
207
+ default:
208
+ break
209
+ }
210
+ }
211
+
212
+ var values: [Float] = []
213
+ if hasUltraWide {
214
+ values.append(0.5)
215
+ }
216
+ if hasWide {
217
+ values.append(1.0)
218
+ if self.isProModelSupportingOptical2x() {
219
+ values.append(2.0)
220
+ }
221
+ }
222
+ if hasTele {
223
+ // Use the virtual device's switch-over zoom factors when available
224
+ let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
225
+ var teleStep: Float
226
+
227
+ if #available(iOS 13.0, *) {
228
+ let switchFactors = device.virtualDeviceSwitchOverVideoZoomFactors
229
+ if !switchFactors.isEmpty {
230
+ // Choose the highest switch-over factor (typically the wide->tele threshold)
231
+ let maxSwitch = switchFactors.map { $0.floatValue }.max() ?? Float(device.maxAvailableVideoZoomFactor)
232
+ teleStep = maxSwitch * displayMultiplier
233
+ } else {
234
+ teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
235
+ }
236
+ } else {
237
+ teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
238
+ }
239
+ values.append(teleStep)
240
+ }
241
+
242
+ // Deduplicate and sort
243
+ let uniqueSorted = Array(Set(values)).sorted()
244
+ call.resolve(["values": uniqueSorted])
245
+ }
246
+
247
+ private func isProModelSupportingOptical2x() -> Bool {
248
+ // Detects iPhone 14 Pro/Pro Max, 15 Pro/Pro Max, and 16 Pro/Pro Max
249
+ var systemInfo = utsname()
250
+ uname(&systemInfo)
251
+ let mirror = Mirror(reflecting: systemInfo.machine)
252
+ let identifier = mirror.children.reduce("") { partialResult, element in
253
+ guard let value = element.value as? Int8, value != 0 else { return partialResult }
254
+ return partialResult + String(UnicodeScalar(UInt8(value)))
255
+ }
256
+
257
+ // Known identifiers: 14 Pro (iPhone15,2), 14 Pro Max (iPhone15,3),
258
+ // 15 Pro (iPhone16,1), 15 Pro Max (iPhone16,2),
259
+ // 16 Pro (iPhone17,1), 16 Pro Max (iPhone17,2),
260
+ // 17 Pro (iPhone18,1), 17 Pro Max (iPhone18,2)
261
+ let supportedIdentifiers: Set<String> = [
262
+ "iPhone15,2", "iPhone15,3", // 14 Pro / 14 Pro Max
263
+ "iPhone16,1", "iPhone16,2", // 15 Pro / 15 Pro Max
264
+ "iPhone17,1", "iPhone17,2" // 16 Pro / 16 Pro Max
265
+ ]
266
+ return supportedIdentifiers.contains(identifier)
267
+ }
268
+
129
269
  @objc func rotated() {
130
270
  guard let previewView = self.previewView,
131
271
  let posX = self.posX,
@@ -141,23 +281,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
141
281
  // Always use the factorized method for consistent positioning
142
282
  self.updateCameraFrame()
143
283
 
144
- if let connection = self.cameraController.fileVideoOutput?.connection(with: .video) {
145
- switch UIDevice.current.orientation {
146
- case .landscapeRight:
147
- connection.videoOrientation = .landscapeLeft
148
- case .landscapeLeft:
149
- connection.videoOrientation = .landscapeRight
150
- case .portrait:
151
- connection.videoOrientation = .portrait
152
- case .portraitUpsideDown:
153
- connection.videoOrientation = .portraitUpsideDown
154
- default:
155
- connection.videoOrientation = .portrait
156
- }
157
- }
158
-
159
- cameraController.updateVideoOrientation()
160
-
284
+ // Centralize orientation update to use interface orientation consistently
161
285
  cameraController.updateVideoOrientation()
162
286
 
163
287
  // Update grid overlay frame if it exists - no animation
@@ -186,61 +310,54 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
186
310
  }
187
311
 
188
312
  self.aspectRatio = newAspectRatio
189
-
190
313
  DispatchQueue.main.async {
191
- // When aspect ratio changes, always auto-center the view
192
- // This ensures consistent behavior where changing aspect ratio recenters the view
193
- self.posX = -1
194
- self.posY = -1
195
-
196
- // Calculate maximum size based on aspect ratio
197
- let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
198
- let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
199
- let paddingBottom = self.paddingBottom ?? 0
200
-
201
- // Calculate available space
202
- let availableWidth: CGFloat
203
- let availableHeight: CGFloat
204
-
205
- if self.posX == -1 || self.posY == -1 {
206
- // Auto-centering mode - use full dimensions
207
- availableWidth = webViewWidth
208
- availableHeight = webViewHeight - paddingBottom
209
- } else {
210
- // Manual positioning - calculate remaining space
211
- availableWidth = webViewWidth - self.posX!
212
- availableHeight = webViewHeight - self.posY! - paddingBottom
213
- }
314
+ call.resolve(self.rawSetAspectRatio())
315
+ }
316
+ }
214
317
 
215
- // Parse aspect ratio - convert to portrait orientation for camera use
216
- let ratioParts = newAspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
217
- // For camera, we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
218
- let ratio = ratioParts[1] / ratioParts[0]
318
+ func rawSetAspectRatio() -> JSObject {
319
+ // When aspect ratio changes, always auto-center the view
320
+ // This ensures consistent behavior where changing aspect ratio recenters the view
321
+ self.posX = -1
322
+ self.posY = -1
219
323
 
220
- // Calculate maximum size that fits the aspect ratio in available space
221
- let maxWidthByHeight = availableHeight * CGFloat(ratio)
222
- let maxHeightByWidth = availableWidth / CGFloat(ratio)
324
+ // Calculate maximum size based on aspect ratio
325
+ let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
326
+ let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
327
+ let paddingBottom = self.paddingBottom ?? 0
328
+ let isPortrait = self.isPortrait()
223
329
 
224
- if maxWidthByHeight <= availableWidth {
225
- // Height is the limiting factor
226
- self.width = maxWidthByHeight
227
- self.height = availableHeight
228
- } else {
229
- // Width is the limiting factor
230
- self.width = availableWidth
231
- self.height = maxHeightByWidth
232
- }
330
+ // Calculate available space
331
+ let availableWidth: CGFloat
332
+ let availableHeight: CGFloat
233
333
 
234
- self.updateCameraFrame()
334
+ if self.posX == -1 || self.posY == -1 {
335
+ // Auto-centering mode - use full dimensions
336
+ availableWidth = webViewWidth
337
+ availableHeight = webViewHeight - paddingBottom
338
+ } else {
339
+ // Manual positioning - calculate remaining space
340
+ availableWidth = webViewWidth - self.posX!
341
+ availableHeight = webViewHeight - self.posY! - paddingBottom
342
+ }
235
343
 
236
- // Return the actual preview bounds
237
- var result = JSObject()
238
- result["x"] = Double(self.previewView.frame.origin.x)
239
- result["y"] = Double(self.previewView.frame.origin.y)
240
- result["width"] = Double(self.previewView.frame.width)
241
- result["height"] = Double(self.previewView.frame.height)
242
- call.resolve(result)
344
+ // Parse aspect ratio - convert to portrait orientation for camera use
345
+ // Use the centralized calculation method
346
+ if let aspectRatio = self.aspectRatio {
347
+ let dimensions = calculateDimensionsForAspectRatio(aspectRatio, availableWidth: availableWidth, availableHeight: availableHeight, isPortrait: isPortrait)
348
+ self.width = dimensions.width
349
+ self.height = dimensions.height
243
350
  }
351
+
352
+ self.updateCameraFrame()
353
+
354
+ // Return the actual preview bounds
355
+ var result = JSObject()
356
+ result["x"] = Double(self.previewView.frame.origin.x)
357
+ result["y"] = Double(self.previewView.frame.origin.y)
358
+ result["width"] = Double(self.previewView.frame.width)
359
+ result["height"] = Double(self.previewView.frame.height)
360
+ return result
244
361
  }
245
362
 
246
363
  @objc func getAspectRatio(_ call: CAPPluginCall) {
@@ -385,6 +502,26 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
385
502
  let startTime = CFAbsoluteTimeGetCurrent()
386
503
  print("[CameraPreview] 🚀 START CALLED at \(Date())")
387
504
 
505
+ // Log all received settings
506
+ print("[CameraPreview] 📋 Settings received:")
507
+ print(" - position: \(call.getString("position") ?? "rear")")
508
+ print(" - deviceId: \(call.getString("deviceId") ?? "nil")")
509
+ print(" - cameraMode: \(call.getBool("cameraMode") ?? false)")
510
+ print(" - width: \(call.getInt("width") ?? 0)")
511
+ print(" - height: \(call.getInt("height") ?? 0)")
512
+ print(" - x: \(call.getInt("x") ?? -1)")
513
+ print(" - y: \(call.getInt("y") ?? -1)")
514
+ print(" - paddingBottom: \(call.getInt("paddingBottom") ?? 0)")
515
+ print(" - rotateWhenOrientationChanged: \(call.getBool("rotateWhenOrientationChanged") ?? true)")
516
+ print(" - toBack: \(call.getBool("toBack") ?? true)")
517
+ print(" - storeToFile: \(call.getBool("storeToFile") ?? false)")
518
+ print(" - enableZoom: \(call.getBool("enableZoom") ?? false)")
519
+ print(" - disableAudio: \(call.getBool("disableAudio") ?? true)")
520
+ print(" - aspectRatio: \(call.getString("aspectRatio") ?? "4:3")")
521
+ print(" - gridMode: \(call.getString("gridMode") ?? "none")")
522
+ print(" - positioning: \(call.getString("positioning") ?? "top")")
523
+ print(" - initialZoomLevel: \(call.getFloat("initialZoomLevel") ?? 1.0)")
524
+
388
525
  if self.isInitializing {
389
526
  call.reject("camera initialization in progress")
390
527
  return
@@ -435,15 +572,16 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
435
572
  self.storeToFile = call.getBool("storeToFile") ?? false
436
573
  self.enableZoom = call.getBool("enableZoom") ?? false
437
574
  self.disableAudio = call.getBool("disableAudio") ?? true
438
- self.aspectRatio = call.getString("aspectRatio")
575
+ // Default to 4:3 aspect ratio if not provided
576
+ self.aspectRatio = call.getString("aspectRatio") ?? "4:3"
439
577
  self.gridMode = call.getString("gridMode") ?? "none"
440
578
  self.positioning = call.getString("positioning") ?? "top"
441
579
 
442
- let userProvidedZoom = call.getFloat("initialZoomLevel")
443
- let initialZoomLevel = userProvidedZoom ?? 1.5
580
+ let initialZoomLevel = call.getFloat("initialZoomLevel")
444
581
 
445
- if self.aspectRatio != nil && (call.getInt("width") != nil || call.getInt("height") != nil) {
446
- call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
582
+ // Validate aspect ratio parameters using centralized method
583
+ if let validationError = validateAspectRatioParameters(aspectRatio: self.aspectRatio, width: call.getInt("width"), height: call.getInt("height")) {
584
+ call.reject(validationError)
447
585
  return
448
586
  }
449
587
 
@@ -457,13 +595,19 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
457
595
  if self.cameraController.captureSession?.isRunning ?? false {
458
596
  call.reject("camera already started")
459
597
  } else {
460
- self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio, initialZoomLevel: Float(initialZoomLevel)) {error in
598
+ self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio, initialZoomLevel: initialZoomLevel) {error in
461
599
  if let error = error {
462
600
  print(error)
463
601
  call.reject(error.localizedDescription)
464
602
  return
465
603
  }
604
+
466
605
  DispatchQueue.main.async {
606
+ UIDevice.current.beginGeneratingDeviceOrientationNotifications()
607
+ NotificationCenter.default.addObserver(self,
608
+ selector: #selector(self.handleOrientationChange),
609
+ name: UIDevice.orientationDidChangeNotification,
610
+ object: nil)
467
611
  self.completeStartCamera(call: call)
468
612
  }
469
613
  }
@@ -487,6 +631,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
487
631
  // Display the camera preview on the configured view
488
632
  try? self.cameraController.displayPreview(on: self.previewView)
489
633
 
634
+ // Ensure the preview orientation matches the current interface orientation at startup
635
+ self.cameraController.updateVideoOrientation()
636
+
490
637
  self.cameraController.setupGestures(target: self.previewView, enableZoom: self.enableZoom!)
491
638
 
492
639
  // Add grid overlay if enabled
@@ -519,14 +666,16 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
519
666
  }
520
667
  }
521
668
 
522
- // If already received first frame (unlikely but possible), resolve immediately
669
+ // If already received first frame (unlikely but possible), resolve immediately on main thread
523
670
  if self.cameraController.hasReceivedFirstFrame {
524
- var returnedObject = JSObject()
525
- returnedObject["width"] = self.previewView.frame.width as any JSValue
526
- returnedObject["height"] = self.previewView.frame.height as any JSValue
527
- returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
528
- returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
529
- call.resolve(returnedObject)
671
+ DispatchQueue.main.async {
672
+ var returnedObject = JSObject()
673
+ returnedObject["width"] = self.previewView.frame.width as any JSValue
674
+ returnedObject["height"] = self.previewView.frame.height as any JSValue
675
+ returnedObject["x"] = self.previewView.frame.origin.x as any JSValue
676
+ returnedObject["y"] = self.previewView.frame.origin.y as any JSValue
677
+ call.resolve(returnedObject)
678
+ }
530
679
  }
531
680
  }
532
681
 
@@ -589,6 +738,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
589
738
  // Remove notification observers
590
739
  NotificationCenter.default.removeObserver(self)
591
740
 
741
+ NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
742
+ UIDevice.current.endGeneratingDeviceOrientationNotifications()
743
+
592
744
  call.resolve()
593
745
  }
594
746
  }
@@ -685,10 +837,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
685
837
 
686
838
  print("[CameraPreview] Raw parameter values - width: \(String(describing: width)), height: \(String(describing: height)), aspectRatio: \(String(describing: aspectRatio))")
687
839
 
688
- // Check for conflicting parameters
689
- if aspectRatio != nil && (width != nil || height != nil) {
690
- print("[CameraPreview] Error: Cannot set both aspectRatio and size (width/height)")
691
- call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
840
+ // Check for conflicting parameters using centralized validation
841
+ if let validationError = validateAspectRatioParameters(aspectRatio: aspectRatio, width: width, height: height) {
842
+ print("[CameraPreview] Error: \(validationError)")
843
+ call.reject(validationError)
692
844
  return
693
845
  }
694
846
 
@@ -703,9 +855,22 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
703
855
 
704
856
  print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil"), using aspectRatio: \(captureAspectRatio ?? "nil")")
705
857
  print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
706
- print("[CameraPreview] Preview dimensions: \(self.previewView.frame.width)x\(self.previewView.frame.height)")
858
+ // Safely read frame from main thread for logging
859
+ let (previewWidth, previewHeight): (CGFloat, CGFloat) = {
860
+ if Thread.isMainThread {
861
+ return (self.previewView.frame.width, self.previewView.frame.height)
862
+ }
863
+ var w: CGFloat = 0
864
+ var h: CGFloat = 0
865
+ DispatchQueue.main.sync {
866
+ w = self.previewView.frame.width
867
+ h = self.previewView.frame.height
868
+ }
869
+ return (w, h)
870
+ }()
871
+ print("[CameraPreview] Preview dimensions: \(previewWidth)x\(previewHeight)")
707
872
 
708
- self.cameraController.captureImage(width: width, height: height, aspectRatio: captureAspectRatio, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
873
+ self.cameraController.captureImage(width: width, height: height, aspectRatio: captureAspectRatio, quality: quality, gpsLocation: self.currentLocation) { (image, originalPhotoData, _, error) in
709
874
  print("[CameraPreview] captureImage callback received")
710
875
  DispatchQueue.main.async {
711
876
  print("[CameraPreview] Processing capture on main thread")
@@ -719,7 +884,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
719
884
  let imageDataWithExif = self.createImageDataWithExif(
720
885
  from: image,
721
886
  quality: Int(quality),
722
- location: withExifLocation ? self.currentLocation : nil
887
+ location: withExifLocation ? self.currentLocation : nil,
888
+ originalPhotoData: originalPhotoData
723
889
  )
724
890
  else {
725
891
  print("[CameraPreview] Failed to create image data with EXIF")
@@ -734,30 +900,56 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
734
900
  self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
735
901
  print("[CameraPreview] Save to gallery completed, success: \(success), error: \(error?.localizedDescription ?? "none")")
736
902
  let exifData = self.getExifData(from: imageDataWithExif)
737
- let base64Image = imageDataWithExif.base64EncodedString()
738
903
 
739
904
  var result = JSObject()
740
- result["value"] = base64Image
741
905
  result["exif"] = exifData
742
906
  result["gallerySaved"] = success
743
907
  if !success, let error = error {
744
908
  result["galleryError"] = error.localizedDescription
745
909
  }
746
910
 
911
+ if self.storeToFile == false {
912
+ let base64Image = imageDataWithExif.base64EncodedString()
913
+ result["value"] = base64Image
914
+ } else {
915
+ do {
916
+ let fileUrl = self.getTempFilePath()
917
+ try imageDataWithExif.write(to: fileUrl)
918
+ result["value"] = fileUrl.absoluteString
919
+ } catch {
920
+ call.reject("Error writing image to file")
921
+ }
922
+ }
923
+
747
924
  print("[CameraPreview] Resolving capture call with gallery save")
748
925
  call.resolve(result)
749
926
  }
750
927
  } else {
751
928
  print("[CameraPreview] Not saving to gallery, returning image data")
752
929
  let exifData = self.getExifData(from: imageDataWithExif)
753
- let base64Image = imageDataWithExif.base64EncodedString()
754
930
 
755
- var result = JSObject()
756
- result["value"] = base64Image
757
- result["exif"] = exifData
931
+ if self.storeToFile == false {
932
+ let base64Image = imageDataWithExif.base64EncodedString()
933
+ var result = JSObject()
934
+ result["value"] = base64Image
935
+ result["exif"] = exifData
936
+
937
+ print("[CameraPreview] base64 - Resolving capture call")
938
+ call.resolve(result)
939
+ } else {
940
+ do {
941
+ let fileUrl = self.getTempFilePath()
942
+ try imageDataWithExif.write(to: fileUrl)
943
+ var result = JSObject()
944
+ result["value"] = fileUrl.absoluteString
945
+ result["exif"] = exifData
946
+ print("[CameraPreview] filePath - Resolving capture call")
947
+ call.resolve(result)
948
+ } catch {
949
+ call.reject("Error writing image to file")
950
+ }
951
+ }
758
952
 
759
- print("[CameraPreview] Resolving capture call")
760
- call.resolve(result)
761
953
  }
762
954
  }
763
955
  }
@@ -792,24 +984,85 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
792
984
  return exifData
793
985
  }
794
986
 
795
- private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?) -> Data? {
796
- guard let originalImageData = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
987
+ @objc func getSafeAreaInsets(_ call: CAPPluginCall) {
988
+ DispatchQueue.main.async {
989
+ var notchInset: CGFloat = 0
990
+ var orientation: Int = 0
991
+
992
+ // Get the current interface orientation
993
+ let interfaceOrientation: UIInterfaceOrientation? = {
994
+ return (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
995
+ }()
996
+
997
+ // Convert to orientation number (matching Android values for consistency)
998
+ switch interfaceOrientation {
999
+ case .portrait, .portraitUpsideDown:
1000
+ orientation = 1 // Portrait
1001
+ case .landscapeLeft, .landscapeRight:
1002
+ orientation = 2 // Landscape
1003
+ default:
1004
+ orientation = 0 // Unknown
1005
+ }
1006
+
1007
+ // Get safe area insets
1008
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
1009
+ let window = windowScene.windows.first {
1010
+ let safeAreaInsets = window.safeAreaInsets
1011
+
1012
+ switch interfaceOrientation {
1013
+ case .portrait:
1014
+ // Portrait: notch is at the top
1015
+ notchInset = safeAreaInsets.top
1016
+ case .portraitUpsideDown:
1017
+ // Portrait upside down: notch is at the bottom (but we still call it "top" for consistency)
1018
+ notchInset = safeAreaInsets.bottom
1019
+ case .landscapeLeft:
1020
+ // Landscape left: notch is typically on the left
1021
+ notchInset = safeAreaInsets.left
1022
+ case .landscapeRight:
1023
+ // Landscape right: notch is typically on the right (but we use left for consistency with Android)
1024
+ notchInset = safeAreaInsets.right
1025
+ default:
1026
+ // Unknown orientation, default to top
1027
+ notchInset = safeAreaInsets.top
1028
+ }
1029
+ } else {
1030
+ // Fallback: use status bar height as approximation
1031
+ notchInset = UIApplication.shared.statusBarFrame.height
1032
+ }
1033
+
1034
+ let result: [String: Any] = [
1035
+ "orientation": orientation,
1036
+ "top": Double(notchInset)
1037
+ ]
1038
+
1039
+ call.resolve(result)
1040
+ }
1041
+ }
1042
+
1043
+ private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?, originalPhotoData: Data?) -> Data? {
1044
+ guard let jpegDataAtQuality = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
797
1045
  return nil
798
1046
  }
799
1047
 
800
- guard let imageSource = CGImageSourceCreateWithData(originalImageData as CFData, nil),
1048
+ // Prefer metadata from the original AVCapturePhoto file data to preserve lens/EXIF
1049
+ let sourceDataForMetadata = (originalPhotoData ?? jpegDataAtQuality) as CFData
1050
+ guard let imageSource = CGImageSourceCreateWithData(sourceDataForMetadata, nil),
801
1051
  let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
802
1052
  let cgImage = image.cgImage else {
803
- return originalImageData
1053
+ return jpegDataAtQuality
804
1054
  }
805
1055
 
806
1056
  let mutableData = NSMutableData()
807
1057
  guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
808
- return originalImageData
1058
+ return jpegDataAtQuality
809
1059
  }
810
1060
 
811
1061
  var finalProperties = imageProperties
812
1062
 
1063
+ // Ensure orientation reflects the pixel data (we pass an orientation-fixed UIImage)
1064
+ finalProperties[kCGImagePropertyOrientation as String] = 1
1065
+
813
1066
  // Add GPS location if available
814
1067
  if let location = location {
815
1068
  let formatter = DateFormatter()
@@ -829,10 +1082,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
829
1082
  finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
830
1083
  }
831
1084
 
832
- // Create or update TIFF dictionary for device info
1085
+ // Create or update TIFF dictionary for device info and set orientation to Up
833
1086
  var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
834
1087
  tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
835
1088
  tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
1089
+ tiffDict[kCGImagePropertyTIFFOrientation as String] = 1
836
1090
  finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
837
1091
 
838
1092
  CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
@@ -841,7 +1095,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
841
1095
  return mutableData as Data
842
1096
  }
843
1097
 
844
- return originalImageData
1098
+ return jpegDataAtQuality
845
1099
  }
846
1100
 
847
1101
  @objc func captureSample(_ call: CAPPluginCall) {
@@ -1036,16 +1290,17 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1036
1290
  do {
1037
1291
  let zoomInfo = try self.cameraController.getZoom()
1038
1292
  let lensInfo = try self.cameraController.getCurrentLensInfo()
1293
+ let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
1039
1294
 
1040
1295
  var minZoom = zoomInfo.min
1041
1296
  var maxZoom = zoomInfo.max
1042
1297
  var currentZoom = zoomInfo.current
1043
1298
 
1044
- // If using the multi-lens camera, translate the native zoom values for JS
1045
- if self.cameraController.isUsingMultiLensVirtualCamera {
1046
- minZoom -= 0.5
1047
- maxZoom -= 0.5
1048
- currentZoom -= 0.5
1299
+ // Apply iOS 18+ display multiplier so UI sees the expected values
1300
+ if displayMultiplier != 1.0 {
1301
+ minZoom *= displayMultiplier
1302
+ maxZoom *= displayMultiplier
1303
+ currentZoom *= displayMultiplier
1049
1304
  }
1050
1305
 
1051
1306
  call.resolve([
@@ -1076,12 +1331,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1076
1331
  }
1077
1332
 
1078
1333
  // If using the multi-lens camera, translate the JS zoom value for the native layer
1079
- if self.cameraController.isUsingMultiLensVirtualCamera {
1080
- level += 0.5
1334
+ // First, convert from UI/display zoom to native zoom using the iOS 18 multiplier
1335
+ let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
1336
+ if displayMultiplier != 1.0 {
1337
+ level = level / displayMultiplier
1081
1338
  }
1082
1339
 
1083
1340
  let ramp = call.getBool("ramp") ?? true
1084
- let autoFocus = call.getBool("autoFocus") ?? true
1341
+ let autoFocus = call.getBool("autoFocus") ?? false
1085
1342
 
1086
1343
  do {
1087
1344
  try self.cameraController.setZoom(level: CGFloat(level), ramp: ramp, autoFocus: autoFocus)
@@ -1326,6 +1583,26 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1326
1583
  }
1327
1584
  }
1328
1585
 
1586
+ private func isPortrait() -> Bool {
1587
+ let orientation = UIDevice.current.orientation
1588
+ if orientation.isValidInterfaceOrientation {
1589
+ return orientation.isPortrait
1590
+ } else {
1591
+ let interfaceOrientation: UIInterfaceOrientation? = {
1592
+ if Thread.isMainThread {
1593
+ return (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
1594
+ } else {
1595
+ var value: UIInterfaceOrientation?
1596
+ DispatchQueue.main.sync {
1597
+ value = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
1598
+ }
1599
+ return value
1600
+ }
1601
+ }()
1602
+ return interfaceOrientation?.isPortrait ?? false
1603
+ }
1604
+ }
1605
+
1329
1606
  private func calculateCameraFrame(x: CGFloat? = nil, y: CGFloat? = nil, width: CGFloat? = nil, height: CGFloat? = nil, aspectRatio: String? = nil) -> CGRect {
1330
1607
  // Use provided values or existing ones
1331
1608
  let currentWidth = width ?? self.width ?? UIScreen.main.bounds.size.width
@@ -1341,6 +1618,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1341
1618
  let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
1342
1619
  let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
1343
1620
 
1621
+ let isPortrait = self.isPortrait()
1622
+
1344
1623
  var finalX = currentX
1345
1624
  var finalY = currentY
1346
1625
  var finalWidth = currentWidth
@@ -1354,12 +1633,20 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1354
1633
  currentHeight == UIScreen.main.bounds.size.height {
1355
1634
  finalWidth = webViewWidth
1356
1635
 
1357
- // Calculate height based on aspect ratio
1358
- let ratioParts = ratio.split(separator: ":").compactMap { Double($0) }
1359
- if ratioParts.count == 2 {
1360
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1361
- let ratioValue = ratioParts[1] / ratioParts[0]
1362
- finalHeight = finalWidth / CGFloat(ratioValue)
1636
+ // width: 428.0 height: 926.0 - portrait
1637
+
1638
+ print("[CameraPreview] width: \(UIScreen.main.bounds.size.width) height: \(UIScreen.main.bounds.size.height)")
1639
+
1640
+ // Calculate dimensions using centralized method
1641
+ let dimensions = calculateDimensionsForAspectRatio(ratio, availableWidth: finalWidth, availableHeight: webViewHeight - paddingBottom, isPortrait: isPortrait)
1642
+ if isPortrait {
1643
+ finalHeight = dimensions.height
1644
+ finalWidth = dimensions.width
1645
+ } else {
1646
+ // In landscape, recalculate based on available space
1647
+ let landscapeDimensions = calculateDimensionsForAspectRatio(ratio, availableWidth: webViewWidth, availableHeight: webViewHeight - paddingBottom, isPortrait: isPortrait)
1648
+ finalWidth = landscapeDimensions.width
1649
+ finalHeight = landscapeDimensions.height
1363
1650
  }
1364
1651
  }
1365
1652
 
@@ -1371,9 +1658,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1371
1658
  }
1372
1659
 
1373
1660
  // Position vertically if y is -1
1661
+ // TODO: fix top, bottom for landscape
1374
1662
  if currentY == -1 {
1375
1663
  // Use full screen height for positioning
1376
1664
  let screenHeight = UIScreen.main.bounds.size.height
1665
+ let screenWidth = UIScreen.main.bounds.size.width
1377
1666
  switch self.positioning {
1378
1667
  case "top":
1379
1668
  finalY = 0
@@ -1382,8 +1671,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1382
1671
  finalY = screenHeight - finalHeight
1383
1672
  print("[CameraPreview] Positioning at bottom: screenHeight=\(screenHeight), finalHeight=\(finalHeight), finalY=\(finalY)")
1384
1673
  default: // "center"
1385
- finalY = (screenHeight - finalHeight) / 2
1386
- print("[CameraPreview] Centering vertically: screenHeight=\(screenHeight), finalHeight=\(finalHeight), finalY=\(finalY)")
1674
+ if isPortrait {
1675
+ finalY = (screenHeight - finalHeight) / 2
1676
+ print("[CameraPreview] Centering vertically: screenHeight=\(screenHeight), finalHeight=\(finalHeight), finalY=\(finalY)")
1677
+ } else {
1678
+ // In landscape, center both horizontally and vertically
1679
+ finalY = (screenHeight - finalHeight) / 2
1680
+ finalX = (screenWidth - finalWidth) / 2
1681
+ }
1387
1682
  }
1388
1683
  } else {
1389
1684
  finalY = currentY
@@ -1411,21 +1706,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1411
1706
 
1412
1707
  // Apply aspect ratio adjustments only if not auto-centering
1413
1708
  if posX != -1 && posY != -1, let aspectRatio = self.aspectRatio {
1414
- let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
1415
- if ratioParts.count == 2 {
1416
- // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1417
- let ratio = ratioParts[1] / ratioParts[0]
1418
- let currentRatio = Double(frame.width) / Double(frame.height)
1419
-
1420
- if currentRatio > ratio {
1421
- let newWidth = Double(frame.height) * ratio
1422
- frame.origin.x = frame.origin.x + (frame.width - CGFloat(newWidth)) / 2
1423
- frame.size.width = CGFloat(newWidth)
1424
- } else {
1425
- let newHeight = Double(frame.width) / ratio
1426
- frame.origin.y = frame.origin.y + (frame.height - CGFloat(newHeight)) / 2
1427
- frame.size.height = CGFloat(newHeight)
1428
- }
1709
+ let isPortrait = self.isPortrait()
1710
+ let ratio = parseAspectRatio(aspectRatio, isPortrait: isPortrait)
1711
+ let currentRatio = frame.width / frame.height
1712
+
1713
+ if currentRatio > ratio {
1714
+ let newWidth = frame.height * ratio
1715
+ frame.origin.x = frame.origin.x + (frame.width - newWidth) / 2
1716
+ frame.size.width = newWidth
1717
+ } else {
1718
+ let newHeight = frame.width / ratio
1719
+ frame.origin.y = frame.origin.y + (frame.height - newHeight) / 2
1720
+ frame.size.height = newHeight
1429
1721
  }
1430
1722
  }
1431
1723
 
@@ -1545,4 +1837,66 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1545
1837
  }
1546
1838
  }
1547
1839
  }
1840
+
1841
+ @objc private func handleOrientationChange() {
1842
+ DispatchQueue.main.async {
1843
+ let result = self.rawSetAspectRatio()
1844
+ self.notifyListeners("screenResize", data: result)
1845
+ self.notifyListeners("orientationChange", data: ["orientation": self.currentOrientationString()])
1846
+ }
1847
+ }
1848
+
1849
+ @objc func deleteFile(_ call: CAPPluginCall) {
1850
+ guard let path = call.getString("path"), !path.isEmpty else {
1851
+ call.reject("path parameter is required")
1852
+ return
1853
+ }
1854
+ let url: URL?
1855
+ if path.hasPrefix("file://") {
1856
+ url = URL(string: path)
1857
+ } else {
1858
+ url = URL(fileURLWithPath: path)
1859
+ }
1860
+ guard let fileURL = url else {
1861
+ call.reject("Invalid path")
1862
+ return
1863
+ }
1864
+ do {
1865
+ if FileManager.default.fileExists(atPath: fileURL.path) {
1866
+ try FileManager.default.removeItem(at: fileURL)
1867
+ call.resolve(["success": true])
1868
+ } else {
1869
+ call.resolve(["success": false])
1870
+ }
1871
+ } catch {
1872
+ call.reject("Failed to delete file: \(error.localizedDescription)")
1873
+ }
1874
+ }
1875
+
1876
+ // MARK: - Orientation
1877
+ private func currentOrientationString() -> String {
1878
+ // Prefer interface orientation for UI-consistent results
1879
+ let orientation: UIInterfaceOrientation? = {
1880
+ if Thread.isMainThread {
1881
+ return (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
1882
+ } else {
1883
+ var value: UIInterfaceOrientation?
1884
+ DispatchQueue.main.sync {
1885
+ value = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.interfaceOrientation
1886
+ }
1887
+ return value
1888
+ }
1889
+ }()
1890
+ switch orientation {
1891
+ case .portrait: return "portrait"
1892
+ case .portraitUpsideDown: return "portrait-upside-down"
1893
+ case .landscapeLeft: return "landscape-left"
1894
+ case .landscapeRight: return "landscape-right"
1895
+ default: return "unknown"
1896
+ }
1897
+ }
1898
+
1899
+ @objc func getOrientation(_ call: CAPPluginCall) {
1900
+ call.resolve(["orientation": self.currentOrientationString()])
1901
+ }
1548
1902
  }