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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +246 -50
  2. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  3. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +1249 -143
  4. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +3400 -1432
  5. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +95 -58
  6. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
  7. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
  8. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +160 -72
  9. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
  10. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
  11. package/dist/docs.json +443 -42
  12. package/dist/esm/definitions.d.ts +173 -27
  13. package/dist/esm/definitions.js.map +1 -1
  14. package/dist/esm/index.d.ts +2 -0
  15. package/dist/esm/index.js +24 -1
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/web.d.ts +23 -3
  18. package/dist/esm/web.js +463 -65
  19. package/dist/esm/web.js.map +1 -1
  20. package/dist/plugin.cjs.js +485 -64
  21. package/dist/plugin.cjs.js.map +1 -1
  22. package/dist/plugin.js +485 -64
  23. package/dist/plugin.js.map +1 -1
  24. package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/CameraController.swift +731 -315
  25. package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1902 -0
  26. package/package.json +11 -3
  27. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  28. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  29. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  30. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  31. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  32. package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
  33. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  34. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  35. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  36. package/android/.gradle/8.14.2/gc.properties +0 -0
  37. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  38. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  39. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  40. package/android/.gradle/file-system.probe +0 -0
  41. package/android/.gradle/vcs-1/gc.properties +0 -0
  42. package/ios/Sources/CapgoCameraPreview/Plugin.swift +0 -1369
  43. /package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/GridOverlayView.swift +0 -0
@@ -1,1369 +0,0 @@
1
- import Foundation
2
- import Capacitor
3
- import AVFoundation
4
- import Photos
5
- import CoreImage
6
- import CoreLocation
7
- import MobileCoreServices
8
-
9
-
10
- extension UIWindow {
11
- static var isLandscape: Bool {
12
- if #available(iOS 13.0, *) {
13
- return UIApplication.shared.windows
14
- .first?
15
- .windowScene?
16
- .interfaceOrientation
17
- .isLandscape ?? false
18
- } else {
19
- return UIApplication.shared.statusBarOrientation.isLandscape
20
- }
21
- }
22
- static var isPortrait: Bool {
23
- if #available(iOS 13.0, *) {
24
- return UIApplication.shared.windows
25
- .first?
26
- .windowScene?
27
- .interfaceOrientation
28
- .isPortrait ?? false
29
- } else {
30
- return UIApplication.shared.statusBarOrientation.isPortrait
31
- }
32
- }
33
- }
34
-
35
- /**
36
- * Please read the Capacitor iOS Plugin Development Guide
37
- * here: https://capacitor.ionicframework.com/docs/plugins/ios
38
- */
39
- @objc(CameraPreview)
40
- public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
41
- public let identifier = "CameraPreviewPlugin"
42
- public let jsName = "CameraPreview"
43
- public let pluginMethods: [CAPPluginMethod] = [
44
- CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
45
- CAPPluginMethod(name: "flip", returnType: CAPPluginReturnPromise),
46
- CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
47
- CAPPluginMethod(name: "capture", returnType: CAPPluginReturnPromise),
48
- CAPPluginMethod(name: "captureSample", returnType: CAPPluginReturnPromise),
49
- CAPPluginMethod(name: "getSupportedFlashModes", returnType: CAPPluginReturnPromise),
50
- CAPPluginMethod(name: "getHorizontalFov", returnType: CAPPluginReturnPromise),
51
- CAPPluginMethod(name: "setFlashMode", returnType: CAPPluginReturnPromise),
52
- CAPPluginMethod(name: "startRecordVideo", returnType: CAPPluginReturnPromise),
53
- CAPPluginMethod(name: "stopRecordVideo", returnType: CAPPluginReturnPromise),
54
- CAPPluginMethod(name: "getTempFilePath", returnType: CAPPluginReturnPromise),
55
- CAPPluginMethod(name: "getSupportedPictureSizes", returnType: CAPPluginReturnPromise),
56
- CAPPluginMethod(name: "isRunning", returnType: CAPPluginReturnPromise),
57
- CAPPluginMethod(name: "getAvailableDevices", returnType: CAPPluginReturnPromise),
58
- CAPPluginMethod(name: "getZoom", returnType: CAPPluginReturnPromise),
59
- CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
60
- CAPPluginMethod(name: "getFlashMode", returnType: CAPPluginReturnPromise),
61
- CAPPluginMethod(name: "setDeviceId", 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)
69
- ]
70
- // Camera state tracking
71
- private var isInitializing: Bool = false
72
- private var isInitialized: Bool = false
73
- private var backgroundSession: AVCaptureSession?
74
-
75
- var previewView: UIView!
76
- var cameraPosition = String()
77
- let cameraController = CameraController()
78
- var posX: CGFloat?
79
- var posY: CGFloat?
80
- var width: CGFloat?
81
- var height: CGFloat?
82
- var paddingBottom: CGFloat?
83
- var rotateWhenOrientationChanged: Bool?
84
- var toBack: Bool?
85
- var storeToFile: Bool?
86
- var enableZoom: Bool?
87
- var highResolutionOutput: Bool = false
88
- var disableAudio: Bool = false
89
- var locationManager: CLLocationManager?
90
- var currentLocation: CLLocation?
91
- private var aspectRatio: String?
92
- private var gridMode: String = "none"
93
-
94
- // MARK: - Transparency Methods
95
-
96
-
97
- private func makeWebViewTransparent() {
98
- guard let webView = self.webView else { return }
99
-
100
-
101
- // Define a recursive function to traverse the view hierarchy
102
- func makeSubviewsTransparent(_ view: UIView) {
103
- // Set the background color to clear
104
- view.backgroundColor = .clear
105
-
106
-
107
- // Recurse for all subviews
108
- for subview in view.subviews {
109
- makeSubviewsTransparent(subview)
110
- }
111
- }
112
-
113
-
114
- // Set the main webView to be transparent
115
- webView.isOpaque = false
116
- webView.backgroundColor = .clear
117
-
118
-
119
- // Recursively make all subviews transparent
120
- makeSubviewsTransparent(webView)
121
-
122
-
123
- // Also ensure the webview's container is transparent
124
- webView.superview?.backgroundColor = .clear
125
-
126
-
127
- // Force a layout pass to apply changes
128
- DispatchQueue.main.async {
129
- webView.setNeedsLayout()
130
- webView.layoutIfNeeded()
131
- }
132
- }
133
-
134
- @objc func rotated() {
135
- guard let previewView = self.previewView,
136
- let posX = self.posX,
137
- let posY = self.posY,
138
- let width = self.width,
139
- let heightValue = self.height else {
140
- return
141
- }
142
- let paddingBottom = self.paddingBottom ?? 0
143
- let height = heightValue - paddingBottom
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
- }
158
-
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()
165
- }
166
-
167
- if let connection = self.cameraController.fileVideoOutput?.connection(with: .video) {
168
- switch UIDevice.current.orientation {
169
- case .landscapeRight:
170
- connection.videoOrientation = .landscapeLeft
171
- case .landscapeLeft:
172
- connection.videoOrientation = .landscapeRight
173
- case .portrait:
174
- connection.videoOrientation = .portrait
175
- case .portraitUpsideDown:
176
- connection.videoOrientation = .portraitUpsideDown
177
- default:
178
- connection.videoOrientation = .portrait
179
- }
180
- }
181
-
182
- cameraController.updateVideoOrientation()
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
-
194
- // Ensure webview remains transparent after rotation
195
- if self.isInitialized {
196
- self.makeWebViewTransparent()
197
- }
198
- }
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
-
307
- @objc func appDidBecomeActive() {
308
- if self.isInitialized {
309
- DispatchQueue.main.async {
310
- self.makeWebViewTransparent()
311
- }
312
- }
313
- }
314
-
315
-
316
- @objc func appWillEnterForeground() {
317
- if self.isInitialized {
318
- DispatchQueue.main.async {
319
- self.makeWebViewTransparent()
320
- }
321
- }
322
- }
323
-
324
- struct CameraInfo {
325
- let deviceID: String
326
- let position: String
327
- let pictureSizes: [CGSize]
328
- }
329
-
330
- func getSupportedPictureSizes() -> [CameraInfo] {
331
- var cameraInfos = [CameraInfo]()
332
-
333
- // Discover all available cameras
334
- let deviceTypes: [AVCaptureDevice.DeviceType] = [
335
- .builtInWideAngleCamera,
336
- .builtInUltraWideCamera,
337
- .builtInTelephotoCamera,
338
- .builtInDualCamera,
339
- .builtInDualWideCamera,
340
- .builtInTripleCamera,
341
- .builtInTrueDepthCamera
342
- ]
343
-
344
- let session = AVCaptureDevice.DiscoverySession(
345
- deviceTypes: deviceTypes,
346
- mediaType: .video,
347
- position: .unspecified
348
- )
349
-
350
- let devices = session.devices
351
-
352
- for device in devices {
353
- // Determine the position of the camera
354
- var position = "Unknown"
355
- switch device.position {
356
- case .front:
357
- position = "Front"
358
- case .back:
359
- position = "Back"
360
- case .unspecified:
361
- position = "Unspecified"
362
- @unknown default:
363
- position = "Unknown"
364
- }
365
-
366
- var pictureSizes = [CGSize]()
367
-
368
- // Get supported formats
369
- for format in device.formats {
370
- let description = format.formatDescription
371
- let dimensions = CMVideoFormatDescriptionGetDimensions(description)
372
- let size = CGSize(width: CGFloat(dimensions.width), height: CGFloat(dimensions.height))
373
- if !pictureSizes.contains(size) {
374
- pictureSizes.append(size)
375
- }
376
- }
377
-
378
- // Sort sizes in descending order (largest to smallest)
379
- pictureSizes.sort { $0.width * $0.height > $1.width * $1.height }
380
-
381
- let cameraInfo = CameraInfo(deviceID: device.uniqueID, position: position, pictureSizes: pictureSizes)
382
- cameraInfos.append(cameraInfo)
383
- }
384
-
385
- return cameraInfos
386
- }
387
-
388
- @objc func getSupportedPictureSizes(_ call: CAPPluginCall) {
389
- let cameraInfos = getSupportedPictureSizes()
390
- call.resolve([
391
- "supportedPictureSizes": cameraInfos.map {
392
- return [
393
- "facing": $0.position,
394
- "supportedPictureSizes": $0.pictureSizes.map { size in
395
- return [
396
- "width": String(describing: size.width),
397
- "height": String(describing: size.height)
398
- ]
399
- }
400
- ]
401
- }
402
- ])
403
- }
404
-
405
- @objc func start(_ call: CAPPluginCall) {
406
- if self.isInitializing {
407
- call.reject("camera initialization in progress")
408
- return
409
- }
410
- if self.isInitialized {
411
- call.reject("camera already started")
412
- return
413
- }
414
- self.isInitializing = true
415
-
416
- self.cameraPosition = call.getString("position") ?? "rear"
417
- let deviceId = call.getString("deviceId")
418
- let cameraMode = call.getBool("cameraMode") ?? false
419
- self.highResolutionOutput = call.getBool("enableHighResolution") ?? false
420
- self.cameraController.highResolutionOutput = self.highResolutionOutput
421
-
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)
425
- } else {
426
- self.width = UIScreen.main.bounds.size.width
427
- }
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)
432
- } else {
433
- self.height = UIScreen.main.bounds.size.height
434
- }
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
- }
449
- if call.getInt("paddingBottom") != nil {
450
- self.paddingBottom = CGFloat(call.getInt("paddingBottom")!)
451
- }
452
-
453
- self.rotateWhenOrientationChanged = call.getBool("rotateWhenOrientationChanged") ?? true
454
- self.toBack = call.getBool("toBack") ?? true
455
- self.storeToFile = call.getBool("storeToFile") ?? false
456
- self.enableZoom = call.getBool("enableZoom") ?? 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)")
468
-
469
- AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
470
- guard granted else {
471
- call.reject("permission failed")
472
- return
473
- }
474
-
475
- DispatchQueue.main.async {
476
- if self.cameraController.captureSession?.isRunning ?? false {
477
- call.reject("camera already started")
478
- } else {
479
- self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio) {error in
480
- if let error = error {
481
- print(error)
482
- call.reject(error.localizedDescription)
483
- return
484
- }
485
- self.completeStartCamera(call: call)
486
- }
487
- }
488
- }
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
- }
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)
552
- }
553
-
554
- @objc func flip(_ call: CAPPluginCall) {
555
- guard isInitialized else {
556
- call.reject("Camera not initialized")
557
- return
558
- }
559
-
560
- DispatchQueue.main.async { [weak self] in
561
- guard let self = self else {
562
- call.reject("Camera controller deallocated")
563
- return
564
- }
565
-
566
- // Disable user interaction during flip
567
- self.previewView.isUserInteractionEnabled = false
568
-
569
- // Perform camera switch on background thread
570
- DispatchQueue.global(qos: .userInitiated).async {
571
- var retryCount = 0
572
- let maxRetries = 3
573
-
574
- func attemptFlip() {
575
- do {
576
- try self.cameraController.switchCameras()
577
-
578
- DispatchQueue.main.async {
579
- // Update preview layer frame without animation
580
- CATransaction.begin()
581
- CATransaction.setDisableActions(true)
582
- self.cameraController.previewLayer?.frame = self.previewView.bounds
583
- self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
584
- CATransaction.commit()
585
-
586
- self.previewView.isUserInteractionEnabled = true
587
-
588
-
589
- // Ensure webview remains transparent after flip
590
- self.makeWebViewTransparent()
591
-
592
-
593
- call.resolve()
594
- }
595
- } catch {
596
- retryCount += 1
597
-
598
- if retryCount < maxRetries {
599
- DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) {
600
- attemptFlip()
601
- }
602
- } else {
603
- DispatchQueue.main.async {
604
- self.previewView.isUserInteractionEnabled = true
605
- print("Failed to flip camera after \(maxRetries) attempts: \(error.localizedDescription)")
606
- call.reject("Failed to flip camera: \(error.localizedDescription)")
607
- }
608
- }
609
- }
610
- }
611
-
612
- attemptFlip()
613
- }
614
- }
615
- }
616
-
617
- @objc func stop(_ call: CAPPluginCall) {
618
- DispatchQueue.main.async {
619
- if self.isInitializing {
620
- call.reject("cannot stop camera while initialization is in progress")
621
- return
622
- }
623
- if !self.isInitialized {
624
- call.reject("camera not initialized")
625
- return
626
- }
627
-
628
- // Always attempt to stop and clean up, regardless of captureSession state
629
- self.cameraController.removeGridOverlay()
630
- if let previewView = self.previewView {
631
- previewView.removeFromSuperview()
632
- self.previewView = nil
633
- }
634
-
635
- self.webView?.isOpaque = true
636
- self.isInitialized = false
637
- self.isInitializing = false
638
- self.cameraController.cleanup()
639
-
640
-
641
- // Remove notification observers
642
- NotificationCenter.default.removeObserver(self)
643
-
644
- call.resolve()
645
- }
646
- }
647
- // Get user's cache directory path
648
- @objc func getTempFilePath() -> URL {
649
- let path = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
650
- let identifier = UUID()
651
- let randomIdentifier = identifier.uuidString.replacingOccurrences(of: "-", with: "")
652
- let finalIdentifier = String(randomIdentifier.prefix(8))
653
- let fileName="cpcp_capture_"+finalIdentifier+".jpg"
654
- let fileUrl=path.appendingPathComponent(fileName)
655
- return fileUrl
656
- }
657
-
658
- @objc func capture(_ call: CAPPluginCall) {
659
- DispatchQueue.main.async {
660
-
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
- }
673
-
674
- self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
675
- if let error = error {
676
- call.reject(error.localizedDescription)
677
- return
678
- }
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
-
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()
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)
725
- }
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
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
-
838
- @objc func captureSample(_ call: CAPPluginCall) {
839
- DispatchQueue.main.async {
840
- let quality: Int? = call.getInt("quality", 85)
841
-
842
- self.cameraController.captureSample { image, error in
843
- guard let image = image else {
844
- print("Image capture error: \(String(describing: error))")
845
- call.reject("Image capture error: \(String(describing: error))")
846
- return
847
- }
848
-
849
- let imageData: Data?
850
- if self.cameraPosition == "front" {
851
- let flippedImage = image.withHorizontallyFlippedOrientation()
852
- imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
853
- } else {
854
- imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
855
- }
856
-
857
- if self.storeToFile == false {
858
- let imageBase64 = imageData?.base64EncodedString()
859
- call.resolve(["value": imageBase64!])
860
- } else {
861
- do {
862
- let fileUrl = self.getTempFilePath()
863
- try imageData?.write(to: fileUrl)
864
- call.resolve(["value": fileUrl.absoluteString])
865
- } catch {
866
- call.reject("Error writing image to file")
867
- }
868
- }
869
- }
870
- }
871
- }
872
-
873
- @objc func getSupportedFlashModes(_ call: CAPPluginCall) {
874
- do {
875
- let supportedFlashModes = try self.cameraController.getSupportedFlashModes()
876
- call.resolve(["result": supportedFlashModes])
877
- } catch {
878
- call.reject("failed to get supported flash modes")
879
- }
880
- }
881
-
882
- @objc func getHorizontalFov(_ call: CAPPluginCall) {
883
- do {
884
- let horizontalFov = try self.cameraController.getHorizontalFov()
885
- call.resolve(["result": horizontalFov])
886
- } catch {
887
- call.reject("failed to get FOV")
888
- }
889
- }
890
-
891
- @objc func setFlashMode(_ call: CAPPluginCall) {
892
- guard let flashMode = call.getString("flashMode") else {
893
- call.reject("failed to set flash mode. required parameter flashMode is missing")
894
- return
895
- }
896
- do {
897
- var flashModeAsEnum: AVCaptureDevice.FlashMode?
898
- switch flashMode {
899
- case "off":
900
- flashModeAsEnum = AVCaptureDevice.FlashMode.off
901
- case "on":
902
- flashModeAsEnum = AVCaptureDevice.FlashMode.on
903
- case "auto":
904
- flashModeAsEnum = AVCaptureDevice.FlashMode.auto
905
- default: break
906
- }
907
- if flashModeAsEnum != nil {
908
- try self.cameraController.setFlashMode(flashMode: flashModeAsEnum!)
909
- } else if flashMode == "torch" {
910
- try self.cameraController.setTorchMode()
911
- } else {
912
- call.reject("Flash Mode not supported")
913
- return
914
- }
915
- call.resolve()
916
- } catch {
917
- call.reject("failed to set flash mode")
918
- }
919
- }
920
-
921
- @objc func startRecordVideo(_ call: CAPPluginCall) {
922
- DispatchQueue.main.async {
923
- do {
924
- try self.cameraController.captureVideo()
925
- call.resolve()
926
- } catch {
927
- call.reject(error.localizedDescription)
928
- }
929
- }
930
- }
931
-
932
- @objc func stopRecordVideo(_ call: CAPPluginCall) {
933
- DispatchQueue.main.async {
934
- self.cameraController.stopRecording { (fileURL, error) in
935
- guard let fileURL = fileURL else {
936
- print(error ?? "Video capture error")
937
- guard let error = error else {
938
- call.reject("Video capture error")
939
- return
940
- }
941
- call.reject(error.localizedDescription)
942
- return
943
- }
944
-
945
- call.resolve(["videoFilePath": fileURL.absoluteString])
946
- }
947
- }
948
- }
949
-
950
- @objc func isRunning(_ call: CAPPluginCall) {
951
- let isRunning = self.isInitialized && (self.cameraController.captureSession?.isRunning ?? false)
952
- call.resolve(["isRunning": isRunning])
953
- }
954
-
955
- @objc func getAvailableDevices(_ call: CAPPluginCall) {
956
- let deviceTypes: [AVCaptureDevice.DeviceType] = [
957
- .builtInWideAngleCamera,
958
- .builtInUltraWideCamera,
959
- .builtInTelephotoCamera,
960
- .builtInDualCamera,
961
- .builtInDualWideCamera,
962
- .builtInTripleCamera,
963
- .builtInTrueDepthCamera
964
- ]
965
-
966
- let session = AVCaptureDevice.DiscoverySession(
967
- deviceTypes: deviceTypes,
968
- mediaType: .video,
969
- position: .unspecified
970
- )
971
-
972
- var devices: [[String: Any]] = []
973
-
974
- // Collect all devices by position
975
- for device in session.devices {
976
- var lenses: [[String: Any]] = []
977
-
978
-
979
- let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
980
-
981
-
982
- for lensDevice in constituentDevices {
983
- var deviceType: String
984
- switch lensDevice.deviceType {
985
- case .builtInWideAngleCamera: deviceType = "wideAngle"
986
- case .builtInUltraWideCamera: deviceType = "ultraWide"
987
- case .builtInTelephotoCamera: deviceType = "telephoto"
988
- case .builtInDualCamera: deviceType = "dual"
989
- case .builtInDualWideCamera: deviceType = "dualWide"
990
- case .builtInTripleCamera: deviceType = "triple"
991
- case .builtInTrueDepthCamera: deviceType = "trueDepth"
992
- default: deviceType = "unknown"
993
- }
994
-
995
- var baseZoomRatio: Float = 1.0
996
- if lensDevice.deviceType == .builtInUltraWideCamera {
997
- baseZoomRatio = 0.5
998
- } else if lensDevice.deviceType == .builtInTelephotoCamera {
999
- baseZoomRatio = 2.0 // A common value for telephoto lenses
1000
- }
1001
-
1002
-
1003
- let lensInfo: [String: Any] = [
1004
- "label": lensDevice.localizedName,
1005
- "deviceType": deviceType,
1006
- "focalLength": 4.25, // Placeholder
1007
- "baseZoomRatio": baseZoomRatio,
1008
- "minZoom": Float(lensDevice.minAvailableVideoZoomFactor),
1009
- "maxZoom": Float(lensDevice.maxAvailableVideoZoomFactor)
1010
- ]
1011
- lenses.append(lensInfo)
1012
- }
1013
-
1014
-
1015
- let deviceData: [String: Any] = [
1016
- "deviceId": device.uniqueID,
1017
- "label": device.localizedName,
1018
- "position": device.position == .front ? "front" : "rear",
1019
- "lenses": lenses,
1020
- "minZoom": Float(device.minAvailableVideoZoomFactor),
1021
- "maxZoom": Float(device.maxAvailableVideoZoomFactor),
1022
- "isLogical": device.isVirtualDevice
1023
- ]
1024
-
1025
-
1026
- devices.append(deviceData)
1027
- }
1028
-
1029
- call.resolve(["devices": devices])
1030
- }
1031
-
1032
- @objc func getZoom(_ call: CAPPluginCall) {
1033
- guard isInitialized else {
1034
- call.reject("Camera not initialized")
1035
- return
1036
- }
1037
-
1038
- do {
1039
- let zoomInfo = try self.cameraController.getZoom()
1040
- let lensInfo = try self.cameraController.getCurrentLensInfo()
1041
-
1042
-
1043
- var minZoom = zoomInfo.min
1044
- var maxZoom = zoomInfo.max
1045
- var currentZoom = zoomInfo.current
1046
-
1047
- // If using the multi-lens camera, translate the native zoom values for JS
1048
- if self.cameraController.isUsingMultiLensVirtualCamera {
1049
- minZoom -= 0.5
1050
- maxZoom -= 0.5
1051
- currentZoom -= 0.5
1052
- }
1053
-
1054
- call.resolve([
1055
- "min": minZoom,
1056
- "max": maxZoom,
1057
- "current": currentZoom,
1058
- "lens": [
1059
- "focalLength": lensInfo.focalLength,
1060
- "deviceType": lensInfo.deviceType,
1061
- "baseZoomRatio": lensInfo.baseZoomRatio,
1062
- "digitalZoom": Float(currentZoom) / lensInfo.baseZoomRatio
1063
- ]
1064
- ])
1065
- } catch {
1066
- call.reject("Failed to get zoom: \(error.localizedDescription)")
1067
- }
1068
- }
1069
-
1070
- @objc func setZoom(_ call: CAPPluginCall) {
1071
- guard isInitialized else {
1072
- call.reject("Camera not initialized")
1073
- return
1074
- }
1075
-
1076
- guard var level = call.getFloat("level") else {
1077
- call.reject("level parameter is required")
1078
- return
1079
- }
1080
-
1081
- // If using the multi-lens camera, translate the JS zoom value for the native layer
1082
- if self.cameraController.isUsingMultiLensVirtualCamera {
1083
- level += 0.5
1084
- }
1085
-
1086
- let ramp = call.getBool("ramp") ?? true
1087
-
1088
- do {
1089
- try self.cameraController.setZoom(level: CGFloat(level), ramp: ramp)
1090
- call.resolve()
1091
- } catch {
1092
- call.reject("Failed to set zoom: \(error.localizedDescription)")
1093
- }
1094
- }
1095
-
1096
- @objc func getFlashMode(_ call: CAPPluginCall) {
1097
- guard isInitialized else {
1098
- call.reject("Camera not initialized")
1099
- return
1100
- }
1101
-
1102
- do {
1103
- let flashMode = try self.cameraController.getFlashMode()
1104
- call.resolve(["flashMode": flashMode])
1105
- } catch {
1106
- call.reject("Failed to get flash mode: \(error.localizedDescription)")
1107
- }
1108
- }
1109
-
1110
- @objc func setDeviceId(_ call: CAPPluginCall) {
1111
- guard isInitialized else {
1112
- call.reject("Camera not initialized")
1113
- return
1114
- }
1115
-
1116
- guard let deviceId = call.getString("deviceId") else {
1117
- call.reject("deviceId parameter is required")
1118
- return
1119
- }
1120
-
1121
- DispatchQueue.main.async { [weak self] in
1122
- guard let self = self else {
1123
- call.reject("Camera controller deallocated")
1124
- return
1125
- }
1126
-
1127
- // Disable user interaction during device swap
1128
- self.previewView.isUserInteractionEnabled = false
1129
-
1130
- DispatchQueue.global(qos: .userInitiated).async {
1131
- do {
1132
- try self.cameraController.swapToDevice(deviceId: deviceId)
1133
-
1134
- DispatchQueue.main.async {
1135
- // Update preview layer frame without animation
1136
- CATransaction.begin()
1137
- CATransaction.setDisableActions(true)
1138
- self.cameraController.previewLayer?.frame = self.previewView.bounds
1139
- self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
1140
- CATransaction.commit()
1141
-
1142
- self.previewView.isUserInteractionEnabled = true
1143
-
1144
-
1145
- // Ensure webview remains transparent after device switch
1146
- self.makeWebViewTransparent()
1147
-
1148
-
1149
- call.resolve()
1150
- }
1151
- } catch {
1152
- DispatchQueue.main.async {
1153
- self.previewView.isUserInteractionEnabled = true
1154
- call.reject("Failed to swap to device \(deviceId): \(error.localizedDescription)")
1155
- }
1156
- }
1157
- }
1158
- }
1159
- }
1160
-
1161
- @objc func getDeviceId(_ call: CAPPluginCall) {
1162
- guard isInitialized else {
1163
- call.reject("Camera not initialized")
1164
- return
1165
- }
1166
-
1167
- do {
1168
- let deviceId = try self.cameraController.getCurrentDeviceId()
1169
- call.resolve(["deviceId": deviceId])
1170
- } catch {
1171
- call.reject("Failed to get device ID: \(error.localizedDescription)")
1172
- }
1173
- }
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
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
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
- }
1369
- }