@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.
- package/README.md +63 -29
- package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +1 -1
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +127 -14
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +529 -29
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +2 -0
- package/dist/docs.json +46 -7
- package/dist/esm/definitions.d.ts +42 -5
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +25 -1
- package/dist/esm/web.js +81 -9
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +81 -9
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +81 -9
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +95 -18
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +449 -111
- package/package.json +1 -1
- package/android/.gradle/config.properties +0 -2
- package/android/.idea/AndroidProjectSystem.xml +0 -6
- package/android/.idea/caches/deviceStreaming.xml +0 -811
- package/android/.idea/compiler.xml +0 -6
- package/android/.idea/gradle.xml +0 -18
- package/android/.idea/migrations.xml +0 -10
- package/android/.idea/misc.xml +0 -10
- package/android/.idea/runConfigurations.xml +0 -17
- package/android/.idea/vcs.xml +0 -6
- package/android/.idea/workspace.xml +0 -55
- 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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
362
|
-
if let x = call.getInt("x")
|
|
363
|
-
self.posX = CGFloat(x)
|
|
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 =
|
|
440
|
+
self.posX = -1 // Use -1 to indicate auto-centering
|
|
366
441
|
}
|
|
367
442
|
|
|
368
|
-
// Set y position - use
|
|
369
|
-
if let y = call.getInt("y")
|
|
370
|
-
self.posY = CGFloat(y)
|
|
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 =
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
492
|
+
override public func load() {
|
|
493
|
+
super.load()
|
|
494
|
+
// Initialize camera session in background for faster startup
|
|
495
|
+
prepareBackgroundCamera()
|
|
496
|
+
}
|
|
412
497
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
420
|
-
|
|
511
|
+
private func completeStartCamera(call: CAPPluginCall) {
|
|
512
|
+
// Create and configure the preview view first
|
|
513
|
+
self.updateCameraFrame()
|
|
421
514
|
|
|
422
|
-
|
|
423
|
-
|
|
515
|
+
// Make webview transparent - comprehensive approach
|
|
516
|
+
self.makeWebViewTransparent()
|
|
424
517
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
431
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
440
|
-
|
|
535
|
+
if self.rotateWhenOrientationChanged == true {
|
|
536
|
+
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
537
|
+
}
|
|
441
538
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
692
|
+
group.enter()
|
|
693
|
+
self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
|
|
694
|
+
gallerySuccess = success
|
|
581
695
|
if !success {
|
|
582
|
-
|
|
696
|
+
galleryError = error?.localizedDescription ?? "Unknown error"
|
|
697
|
+
print("CameraPreview: Error saving image to gallery: \(galleryError!)")
|
|
583
698
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1028
|
-
self.cameraController.previewLayer
|
|
1029
|
-
|
|
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
|
}
|