@capgo/camera-preview 7.6.1-alpha.2 → 7.6.1-alpha.3

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.
@@ -72,10 +72,76 @@ class CameraController: NSObject {
72
72
  // Track whether an aspect ratio was explicitly requested
73
73
  var requestedAspectRatio: String?
74
74
 
75
+ private func calculateAspectRatioFrame(for aspectRatio: String, in bounds: CGRect) -> CGRect {
76
+ guard let ratio = parseAspectRatio(aspectRatio) else {
77
+ return bounds
78
+ }
79
+
80
+ let targetAspectRatio = ratio.width / ratio.height
81
+ let viewAspectRatio = bounds.width / bounds.height
82
+
83
+ var frame: CGRect
84
+
85
+ if viewAspectRatio > targetAspectRatio {
86
+ // View is wider than target - fit by height
87
+ let targetWidth = bounds.height * targetAspectRatio
88
+ let xOffset = (bounds.width - targetWidth) / 2
89
+ frame = CGRect(x: xOffset, y: 0, width: targetWidth, height: bounds.height)
90
+ } else {
91
+ // View is taller than target - fit by width
92
+ let targetHeight = bounds.width / targetAspectRatio
93
+ let yOffset = (bounds.height - targetHeight) / 2
94
+ frame = CGRect(x: 0, y: yOffset, width: bounds.width, height: targetHeight)
95
+ }
96
+
97
+ return frame
98
+ }
99
+
75
100
  private func parseAspectRatio(_ aspectRatio: String) -> (width: CGFloat, height: CGFloat)? {
76
101
  let components = aspectRatio.split(separator: ":").compactMap { Float(String($0)) }
77
102
  guard components.count == 2 else { return nil }
78
- return (width: CGFloat(components[0]), height: CGFloat(components[1]))
103
+
104
+ // Check if device is in portrait orientation by looking at the current interface orientation
105
+ var isPortrait = false
106
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
107
+ print("[CameraPreview] parseAspectRatio - windowScene.interfaceOrientation: \(windowScene.interfaceOrientation)")
108
+ switch windowScene.interfaceOrientation {
109
+ case .portrait, .portraitUpsideDown:
110
+ isPortrait = true
111
+ case .landscapeLeft, .landscapeRight:
112
+ isPortrait = false
113
+ case .unknown:
114
+ // Fallback to device orientation
115
+ isPortrait = UIDevice.current.orientation.isPortrait
116
+ @unknown default:
117
+ isPortrait = UIDevice.current.orientation.isPortrait
118
+ }
119
+ } else {
120
+ // Fallback to device orientation
121
+ isPortrait = UIDevice.current.orientation.isPortrait
122
+ }
123
+
124
+ let originalWidth = CGFloat(components[0])
125
+ let originalHeight = CGFloat(components[1])
126
+ print("[CameraPreview] parseAspectRatio - isPortrait: \(isPortrait) originalWidth: \(originalWidth) originalHeight: \(originalHeight)")
127
+
128
+ let finalWidth: CGFloat
129
+ let finalHeight: CGFloat
130
+
131
+ if isPortrait {
132
+ // For portrait mode, swap width and height to maintain portrait orientation
133
+ // 4:3 becomes 3:4, 16:9 becomes 9:16
134
+ finalWidth = originalHeight
135
+ finalHeight = originalWidth
136
+ print("[CameraPreview] parseAspectRatio - Portrait mode: \(aspectRatio) -> \(finalWidth):\(finalHeight) (ratio: \(finalWidth/finalHeight))")
137
+ } else {
138
+ // For landscape mode, keep original orientation
139
+ finalWidth = originalWidth
140
+ finalHeight = originalHeight
141
+ print("[CameraPreview] parseAspectRatio - Landscape mode: \(aspectRatio) -> \(finalWidth):\(finalHeight) (ratio: \(finalWidth/finalHeight))")
142
+ }
143
+
144
+ return (width: finalWidth, height: finalHeight)
79
145
  }
80
146
  }
81
147
 
@@ -315,6 +381,38 @@ extension CameraController {
315
381
  }
316
382
  }
317
383
 
384
+ /// Update the requested aspect ratio at runtime and reconfigure session/preview accordingly
385
+ func updateAspectRatio(_ aspectRatio: String?) {
386
+ // Update internal state
387
+ self.requestedAspectRatio = aspectRatio
388
+
389
+ // Reconfigure session preset to match the new ratio for optimal capture resolution
390
+ if let captureSession = self.captureSession {
391
+ captureSession.beginConfiguration()
392
+ self.configureSessionPreset(for: aspectRatio)
393
+ captureSession.commitConfiguration()
394
+ }
395
+
396
+ // Update preview layer geometry on the main thread
397
+ DispatchQueue.main.async { [weak self] in
398
+ guard let self = self, let previewLayer = self.previewLayer else { return }
399
+ if let superlayer = previewLayer.superlayer {
400
+ let bounds = superlayer.bounds
401
+ if let aspect = aspectRatio {
402
+ let frame = self.calculateAspectRatioFrame(for: aspect, in: bounds)
403
+ previewLayer.frame = frame
404
+ previewLayer.videoGravity = .resizeAspectFill
405
+ } else {
406
+ previewLayer.frame = bounds
407
+ previewLayer.videoGravity = .resizeAspect
408
+ }
409
+
410
+ // Keep grid overlay in sync with preview
411
+ self.gridOverlayView?.frame = previewLayer.frame
412
+ }
413
+ }
414
+ }
415
+
318
416
  private func setInitialZoom(level: Float?) {
319
417
  let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
320
418
  guard let device = device else {
@@ -429,9 +527,12 @@ extension CameraController {
429
527
  // Configure video gravity and frame based on aspect ratio
430
528
  if let aspectRatio = requestedAspectRatio {
431
529
  // Calculate the frame based on requested aspect ratio
530
+ let frame = calculateAspectRatioFrame(for: aspectRatio, in: view.bounds)
531
+ previewLayer.frame = frame
432
532
  previewLayer.videoGravity = .resizeAspectFill
433
533
  } else {
434
534
  // No specific aspect ratio requested - fill the entire view
535
+ previewLayer.frame = view.bounds
435
536
  previewLayer.videoGravity = .resizeAspect
436
537
  }
437
538
 
@@ -523,8 +624,6 @@ extension CameraController {
523
624
  } else {
524
625
  videoOrientation = .portrait
525
626
  }
526
-
527
- videoOrientation = .landscapeLeft
528
627
 
529
628
  previewLayer?.connection?.videoOrientation = videoOrientation
530
629
  dataOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
@@ -655,7 +754,7 @@ extension CameraController {
655
754
  }
656
755
 
657
756
  func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
658
- print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1)")
757
+ print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), requestedAspectRatio: \(self.requestedAspectRatio ?? "nil")")
659
758
 
660
759
  guard let photoOutput = self.photoOutput else {
661
760
  completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
@@ -724,18 +823,9 @@ extension CameraController {
724
823
  print("[CameraPreview] Resized to max dimensions: \(finalImage.size.width)x\(finalImage.size.height)")
725
824
  } else if let aspectRatio = self.requestedAspectRatio {
726
825
  // No max dimensions specified, but aspect ratio is specified
727
- // If we used session preset (low-res), image should already be correct aspect ratio
728
- // If we used high-res (shouldn't happen without max dimensions), crop it
729
- let imageAspectRatio = image.size.width / image.size.height
730
- if let targetRatio = self.parseAspectRatio(aspectRatio) {
731
- let targetAspectRatio = targetRatio.width / targetRatio.height
732
-
733
- // Allow small tolerance for aspect ratio comparison
734
- if abs(imageAspectRatio - targetAspectRatio) > 0.01 {
735
- finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
736
- print("[CameraPreview] Cropped to match aspect ratio \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
737
- }
738
- }
826
+ // Always apply aspect ratio cropping to ensure correct orientation
827
+ finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
828
+ print("[CameraPreview] Applied aspect ratio cropping for \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
739
829
  }
740
830
 
741
831
  completion(finalImage, photoData, metadata, nil)
@@ -817,13 +907,27 @@ extension CameraController {
817
907
 
818
908
  func cropImageToAspectRatio(image: UIImage, aspectRatio: String) -> UIImage? {
819
909
  guard let ratio = parseAspectRatio(aspectRatio) else {
910
+ print("[CameraPreview] cropImageToAspectRatio - Failed to parse aspect ratio: \(aspectRatio)")
820
911
  return image
821
912
  }
822
913
 
823
- let imageSize = image.size
914
+ // Only normalize the image orientation if it's not already correct
915
+ let normalizedImage: UIImage
916
+ if image.imageOrientation == .up {
917
+ normalizedImage = image
918
+ print("[CameraPreview] cropImageToAspectRatio - Image already has correct orientation")
919
+ } else {
920
+ normalizedImage = image.fixedOrientation() ?? image
921
+ print("[CameraPreview] cropImageToAspectRatio - Normalized image orientation from \(image.imageOrientation.rawValue) to .up")
922
+ }
923
+
924
+ let imageSize = normalizedImage.size
824
925
  let imageAspectRatio = imageSize.width / imageSize.height
825
926
  let targetAspectRatio = ratio.width / ratio.height
826
927
 
928
+ print("[CameraPreview] cropImageToAspectRatio - Original image: \(imageSize.width)x\(imageSize.height) (ratio: \(imageAspectRatio))")
929
+ print("[CameraPreview] cropImageToAspectRatio - Target ratio: \(ratio.width):\(ratio.height) (ratio: \(targetAspectRatio))")
930
+
827
931
  var cropRect: CGRect
828
932
 
829
933
  if imageAspectRatio > targetAspectRatio {
@@ -831,19 +935,36 @@ extension CameraController {
831
935
  let targetWidth = imageSize.height * targetAspectRatio
832
936
  let xOffset = (imageSize.width - targetWidth) / 2
833
937
  cropRect = CGRect(x: xOffset, y: 0, width: targetWidth, height: imageSize.height)
938
+ print("[CameraPreview] cropImageToAspectRatio - Horizontal crop: \(cropRect)")
834
939
  } else {
835
940
  // Image is taller than target - crop vertically (center crop)
836
941
  let targetHeight = imageSize.width / targetAspectRatio
837
942
  let yOffset = (imageSize.height - targetHeight) / 2
838
943
  cropRect = CGRect(x: 0, y: yOffset, width: imageSize.width, height: targetHeight)
944
+ print("[CameraPreview] cropImageToAspectRatio - Vertical crop: \(cropRect) - Target height: \(targetHeight)")
839
945
  }
840
946
 
841
- guard let cgImage = image.cgImage,
947
+ // Validate crop rect is within image bounds
948
+ if cropRect.minX < 0 || cropRect.minY < 0 ||
949
+ cropRect.maxX > imageSize.width || cropRect.maxY > imageSize.height {
950
+ print("[CameraPreview] cropImageToAspectRatio - Warning: Crop rect \(cropRect) exceeds image bounds \(imageSize)")
951
+ // Adjust crop rect to fit within image bounds
952
+ cropRect = cropRect.intersection(CGRect(origin: .zero, size: imageSize))
953
+ print("[CameraPreview] cropImageToAspectRatio - Adjusted crop rect: \(cropRect)")
954
+ }
955
+
956
+ guard let cgImage = normalizedImage.cgImage,
842
957
  let croppedCGImage = cgImage.cropping(to: cropRect) else {
958
+ print("[CameraPreview] cropImageToAspectRatio - Failed to crop image")
843
959
  return nil
844
960
  }
845
961
 
846
- return UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: image.imageOrientation)
962
+ let croppedImage = UIImage(cgImage: croppedCGImage, scale: normalizedImage.scale, orientation: .up)
963
+ let finalAspectRatio = croppedImage.size.width / croppedImage.size.height
964
+ print("[CameraPreview] cropImageToAspectRatio - Final cropped image: \(croppedImage.size.width)x\(croppedImage.size.height) (ratio: \(finalAspectRatio))")
965
+
966
+ // Create the cropped image with normalized orientation
967
+ return croppedImage
847
968
  }
848
969
 
849
970
  func cropImageToMatchPreview(image: UIImage, previewLayer: AVCaptureVideoPreviewLayer) -> UIImage? {
@@ -1644,7 +1765,8 @@ extension CameraController: AVCapturePhotoCaptureDelegate {
1644
1765
  }
1645
1766
 
1646
1767
  // Pass through original file data and metadata so callers can preserve EXIF
1647
- self.photoCaptureCompletionBlock?(image.fixedOrientation(), imageData, photo.metadata, nil)
1768
+ // Don't call fixedOrientation() here - let the completion block handle it after cropping
1769
+ self.photoCaptureCompletionBlock?(image, imageData, photo.metadata, nil)
1648
1770
  }
1649
1771
  }
1650
1772
 
@@ -305,6 +305,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
305
305
  }
306
306
 
307
307
  self.aspectRatio = newAspectRatio
308
+
309
+ // Propagate to camera controller so capture output and preview align
310
+ self.cameraController.updateAspectRatio(newAspectRatio)
311
+
308
312
  DispatchQueue.main.async {
309
313
  call.resolve(self.rawSetAspectRatio())
310
314
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.6.1-alpha.2",
3
+ "version": "7.6.1-alpha.3",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {