@capgo/camera-preview 7.4.1 → 7.5.0

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.
@@ -70,6 +70,37 @@ class CameraController: NSObject {
70
70
 
71
71
  // Track whether an aspect ratio was explicitly requested
72
72
  var requestedAspectRatio: String?
73
+
74
+ private func calculateAspectRatioFrame(for aspectRatio: String, in bounds: CGRect) -> CGRect {
75
+ guard let ratio = parseAspectRatio(aspectRatio) else {
76
+ return bounds
77
+ }
78
+
79
+ let targetAspectRatio = ratio.width / ratio.height
80
+ let viewAspectRatio = bounds.width / bounds.height
81
+
82
+ var frame: CGRect
83
+
84
+ if viewAspectRatio > targetAspectRatio {
85
+ // View is wider than target - fit by height
86
+ let targetWidth = bounds.height * targetAspectRatio
87
+ let xOffset = (bounds.width - targetWidth) / 2
88
+ frame = CGRect(x: xOffset, y: 0, width: targetWidth, height: bounds.height)
89
+ } else {
90
+ // View is taller than target - fit by width
91
+ let targetHeight = bounds.width / targetAspectRatio
92
+ let yOffset = (bounds.height - targetHeight) / 2
93
+ frame = CGRect(x: 0, y: yOffset, width: bounds.width, height: targetHeight)
94
+ }
95
+
96
+ return frame
97
+ }
98
+
99
+ private func parseAspectRatio(_ aspectRatio: String) -> (width: CGFloat, height: CGFloat)? {
100
+ let components = aspectRatio.split(separator: ":").compactMap { Float(String($0)) }
101
+ guard components.count == 2 else { return nil }
102
+ return (width: CGFloat(components[0]), height: CGFloat(components[1]))
103
+ }
73
104
  }
74
105
 
75
106
  extension CameraController {
@@ -416,9 +447,17 @@ extension CameraController {
416
447
  }
417
448
 
418
449
  // Fast configuration without CATransaction overhead
419
- // Use resizeAspect to avoid crop when no aspect ratio is requested; otherwise fill
420
- previewLayer.videoGravity = (requestedAspectRatio == nil) ? .resizeAspect : .resizeAspectFill
421
- previewLayer.frame = view.bounds
450
+ // Configure video gravity and frame based on aspect ratio
451
+ if let aspectRatio = requestedAspectRatio {
452
+ // Calculate the frame based on requested aspect ratio
453
+ let frame = calculateAspectRatioFrame(for: aspectRatio, in: view.bounds)
454
+ previewLayer.frame = frame
455
+ previewLayer.videoGravity = .resizeAspectFill
456
+ } else {
457
+ // No specific aspect ratio requested - fill the entire view
458
+ previewLayer.frame = view.bounds
459
+ previewLayer.videoGravity = .resizeAspect
460
+ }
422
461
 
423
462
  // Insert layer immediately (only if new)
424
463
  if previewLayer.superlayer != view.layer {
@@ -433,7 +472,16 @@ extension CameraController {
433
472
  // Disable animation for grid overlay creation and positioning
434
473
  CATransaction.begin()
435
474
  CATransaction.setDisableActions(true)
436
- gridOverlayView = GridOverlayView(frame: view.bounds)
475
+
476
+ // Use preview layer frame if aspect ratio is specified, otherwise use full view bounds
477
+ let gridFrame: CGRect
478
+ if requestedAspectRatio != nil, let previewLayer = previewLayer {
479
+ gridFrame = previewLayer.frame
480
+ } else {
481
+ gridFrame = view.bounds
482
+ }
483
+
484
+ gridOverlayView = GridOverlayView(frame: gridFrame)
437
485
  gridOverlayView?.gridMode = gridMode
438
486
  view.addSubview(gridOverlayView!)
439
487
  CATransaction.commit()
@@ -628,8 +676,8 @@ extension CameraController {
628
676
  }
629
677
  }
630
678
 
631
- func captureImage(width: Int?, height: Int?, aspectRatio: String?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
632
- print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil")")
679
+ func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
680
+ print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1)")
633
681
 
634
682
  guard let photoOutput = self.photoOutput else {
635
683
  completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
@@ -637,9 +685,12 @@ extension CameraController {
637
685
  }
638
686
 
639
687
  let settings = AVCapturePhotoSettings()
640
- // Request highest quality photo capture
688
+ // Configure photo capture settings
641
689
  if #available(iOS 13.0, *) {
642
- settings.isHighResolutionPhotoEnabled = true
690
+ // Enable high resolution capture if max dimensions are specified or no aspect ratio constraint
691
+ // When aspect ratio is specified WITHOUT max dimensions, use session preset dimensions
692
+ let shouldUseHighRes = (width != nil || height != nil) || (self.requestedAspectRatio == nil)
693
+ settings.isHighResolutionPhotoEnabled = shouldUseHighRes
643
694
  }
644
695
  if #available(iOS 15.0, *) {
645
696
  settings.photoQualityPrioritization = .balanced
@@ -683,46 +734,29 @@ extension CameraController {
683
734
  var finalImage = image
684
735
 
685
736
  // Determine what to do based on parameters
686
- if let width = width, let height = height {
687
- // Specific dimensions requested - resize to exact size
688
- finalImage = self.resizeImage(image: image, to: CGSize(width: width, height: height))!
689
- print("[CameraPreview] Resized to exact dimensions: \(finalImage.size.width)x\(finalImage.size.height)")
690
- } else if let aspectRatio = aspectRatio {
691
- // Aspect ratio specified - crop to that ratio
692
- let components = aspectRatio.split(separator: ":").compactMap { Double($0) }
693
- if components.count == 2 {
694
- // For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
695
- let isPortrait = image.size.height > image.size.width
696
- let targetAspectRatio = isPortrait ? components[1] / components[0] : components[0] / components[1]
697
- let imageSize = image.size
698
- let originalAspectRatio = imageSize.width / imageSize.height
699
-
700
- // Only crop if the aspect ratios don't match
701
- if abs(originalAspectRatio - targetAspectRatio) > 0.01 {
702
- var targetSize = imageSize
703
-
704
- if originalAspectRatio > targetAspectRatio {
705
- // Original is wider than target - fit by height
706
- targetSize.width = imageSize.height * CGFloat(targetAspectRatio)
707
- } else {
708
- // Original is taller than target - fit by width
709
- targetSize.height = imageSize.width / CGFloat(targetAspectRatio)
710
- }
711
-
712
- // Center crop the image
713
- if let croppedImage = self.cropImageToAspectRatio(image: image, targetSize: targetSize) {
714
- finalImage = croppedImage
715
- print("[CameraPreview] Applied aspect ratio crop: \(finalImage.size.width)x\(finalImage.size.height)")
716
- }
717
- }
737
+ if width != nil || height != nil {
738
+ // When max dimensions are specified, we used high-res capture
739
+ // First crop to aspect ratio if needed, then resize to max dimensions
740
+ if let aspectRatio = self.requestedAspectRatio {
741
+ finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
742
+ print("[CameraPreview] Cropped high-res image to aspect ratio \(aspectRatio)")
718
743
  }
719
- } else {
720
- // No parameters specified - crop to match what's visible in the preview
721
- // This ensures we capture exactly what the user sees
722
- if let previewLayer = self.previewLayer,
723
- let previewCroppedImage = self.cropImageToMatchPreview(image: image, previewLayer: previewLayer) {
724
- finalImage = previewCroppedImage
725
- print("[CameraPreview] Cropped to match preview: \(finalImage.size.width)x\(finalImage.size.height)")
744
+ // Then resize to fit within maximum dimensions while maintaining aspect ratio
745
+ finalImage = self.resizeImageToMaxDimensions(image: finalImage, maxWidth: width, maxHeight: height)!
746
+ print("[CameraPreview] Resized to max dimensions: \(finalImage.size.width)x\(finalImage.size.height)")
747
+ } else if let aspectRatio = self.requestedAspectRatio {
748
+ // No max dimensions specified, but aspect ratio is specified
749
+ // If we used session preset (low-res), image should already be correct aspect ratio
750
+ // If we used high-res (shouldn't happen without max dimensions), crop it
751
+ let imageAspectRatio = image.size.width / image.size.height
752
+ if let targetRatio = self.parseAspectRatio(aspectRatio) {
753
+ let targetAspectRatio = targetRatio.width / targetRatio.height
754
+
755
+ // Allow small tolerance for aspect ratio comparison
756
+ if abs(imageAspectRatio - targetAspectRatio) > 0.01 {
757
+ finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
758
+ print("[CameraPreview] Cropped to match aspect ratio \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
759
+ }
726
760
  }
727
761
  }
728
762
 
@@ -762,27 +796,75 @@ extension CameraController {
762
796
  }
763
797
 
764
798
  func resizeImage(image: UIImage, to size: CGSize) -> UIImage? {
765
- let renderer = UIGraphicsImageRenderer(size: size)
799
+ // Create a renderer with scale 1.0 to ensure we get exact pixel dimensions
800
+ let format = UIGraphicsImageRendererFormat()
801
+ format.scale = 1.0
802
+ let renderer = UIGraphicsImageRenderer(size: size, format: format)
766
803
  let resizedImage = renderer.image { (_) in
767
804
  image.draw(in: CGRect(origin: .zero, size: size))
768
805
  }
769
806
  return resizedImage
770
807
  }
771
808
 
772
- func cropImageToAspectRatio(image: UIImage, targetSize: CGSize) -> UIImage? {
773
- let imageSize = image.size
774
-
775
- // Calculate the crop rect - center crop
776
- let xOffset = (imageSize.width - targetSize.width) / 2
777
- let yOffset = (imageSize.height - targetSize.height) / 2
778
- let cropRect = CGRect(x: xOffset, y: yOffset, width: targetSize.width, height: targetSize.height)
809
+ func resizeImageToMaxDimensions(image: UIImage, maxWidth: Int?, maxHeight: Int?) -> UIImage? {
810
+ let originalSize = image.size
811
+ let originalAspectRatio = originalSize.width / originalSize.height
812
+
813
+ var targetSize = originalSize
814
+
815
+ if let maxWidth = maxWidth, let maxHeight = maxHeight {
816
+ // Both dimensions specified - fit within both maximums
817
+ let maxAspectRatio = CGFloat(maxWidth) / CGFloat(maxHeight)
818
+ if originalAspectRatio > maxAspectRatio {
819
+ // Original is wider - fit by width
820
+ targetSize.width = CGFloat(maxWidth)
821
+ targetSize.height = CGFloat(maxWidth) / originalAspectRatio
822
+ } else {
823
+ // Original is taller - fit by height
824
+ targetSize.width = CGFloat(maxHeight) * originalAspectRatio
825
+ targetSize.height = CGFloat(maxHeight)
826
+ }
827
+ } else if let maxWidth = maxWidth {
828
+ // Only width specified - maintain aspect ratio
829
+ targetSize.width = CGFloat(maxWidth)
830
+ targetSize.height = CGFloat(maxWidth) / originalAspectRatio
831
+ } else if let maxHeight = maxHeight {
832
+ // Only height specified - maintain aspect ratio
833
+ targetSize.width = CGFloat(maxHeight) * originalAspectRatio
834
+ targetSize.height = CGFloat(maxHeight)
835
+ }
836
+
837
+ return resizeImage(image: image, to: targetSize)
838
+ }
779
839
 
780
- // Create the cropped image
840
+ func cropImageToAspectRatio(image: UIImage, aspectRatio: String) -> UIImage? {
841
+ guard let ratio = parseAspectRatio(aspectRatio) else {
842
+ return image
843
+ }
844
+
845
+ let imageSize = image.size
846
+ let imageAspectRatio = imageSize.width / imageSize.height
847
+ let targetAspectRatio = ratio.width / ratio.height
848
+
849
+ var cropRect: CGRect
850
+
851
+ if imageAspectRatio > targetAspectRatio {
852
+ // Image is wider than target - crop horizontally (center crop)
853
+ let targetWidth = imageSize.height * targetAspectRatio
854
+ let xOffset = (imageSize.width - targetWidth) / 2
855
+ cropRect = CGRect(x: xOffset, y: 0, width: targetWidth, height: imageSize.height)
856
+ } else {
857
+ // Image is taller than target - crop vertically (center crop)
858
+ let targetHeight = imageSize.width / targetAspectRatio
859
+ let yOffset = (imageSize.height - targetHeight) / 2
860
+ cropRect = CGRect(x: 0, y: yOffset, width: imageSize.width, height: targetHeight)
861
+ }
862
+
781
863
  guard let cgImage = image.cgImage,
782
864
  let croppedCGImage = cgImage.cropping(to: cropRect) else {
783
865
  return nil
784
866
  }
785
-
867
+
786
868
  return UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: image.imageOrientation)
787
869
  }
788
870
 
@@ -100,13 +100,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
100
100
 
101
101
  // MARK: - Helper Methods for Aspect Ratio
102
102
 
103
- /// Validates that aspectRatio and size (width/height) are not both set
104
- private func validateAspectRatioParameters(aspectRatio: String?, width: Int?, height: Int?) -> String? {
105
- if aspectRatio != nil && (width != nil || height != nil) {
106
- return "Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."
107
- }
108
- return nil
109
- }
103
+
110
104
 
111
105
  /// Parses aspect ratio string and returns the appropriate ratio for the current orientation
112
106
  private func parseAspectRatio(_ ratio: String, isPortrait: Bool) -> CGFloat {
@@ -579,12 +573,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
579
573
 
580
574
  let initialZoomLevel = call.getFloat("initialZoomLevel")
581
575
 
582
- // Validate aspect ratio parameters using centralized method
583
- if let validationError = validateAspectRatioParameters(aspectRatio: self.aspectRatio, width: call.getInt("width"), height: call.getInt("height")) {
584
- call.reject(validationError)
576
+ // Check for conflict between aspectRatio and size (width/height)
577
+ let hasAspectRatio = call.getString("aspectRatio") != nil
578
+ let hasWidth = call.getInt("width") != nil
579
+ let hasHeight = call.getInt("height") != nil
580
+
581
+ if hasAspectRatio && (hasWidth || hasHeight) {
582
+ call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.")
585
583
  return
586
584
  }
587
585
 
586
+
587
+
588
588
  AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
589
589
 
590
590
  guard granted else {
@@ -833,27 +833,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
833
833
  let withExifLocation = call.getBool("withExifLocation", false)
834
834
  let width = call.getInt("width")
835
835
  let height = call.getInt("height")
836
- let aspectRatio = call.getString("aspectRatio")
837
-
838
- print("[CameraPreview] Raw parameter values - width: \(String(describing: width)), height: \(String(describing: height)), aspectRatio: \(String(describing: aspectRatio))")
839
836
 
840
- // Check for conflicting parameters using centralized validation
841
- if let validationError = validateAspectRatioParameters(aspectRatio: aspectRatio, width: width, height: height) {
842
- print("[CameraPreview] Error: \(validationError)")
843
- call.reject(validationError)
844
- return
845
- }
846
-
847
- // When no dimensions are specified, we should capture exactly what's visible in the preview
848
- // Don't pass aspectRatio in this case - let the capture method handle preview matching
849
- print("[CameraPreview] Capture decision - width: \(width == nil), height: \(height == nil), aspectRatio param: \(aspectRatio == nil)")
850
- print("[CameraPreview] Stored aspectRatio: \(self.aspectRatio ?? "nil")")
837
+ print("[CameraPreview] Raw parameter values - width: \(String(describing: width)), height: \(String(describing: height))")
851
838
 
852
- // Only pass aspectRatio if explicitly provided in the capture call
853
- // Never use the stored aspectRatio when capturing without dimensions
854
- let captureAspectRatio: String? = aspectRatio
855
-
856
- print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil"), using aspectRatio: \(captureAspectRatio ?? "nil")")
839
+ print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), width: \(width ?? -1), height: \(height ?? -1)")
857
840
  print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
858
841
  // Safely read frame from main thread for logging
859
842
  let (previewWidth, previewHeight): (CGFloat, CGFloat) = {
@@ -870,7 +853,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
870
853
  }()
871
854
  print("[CameraPreview] Preview dimensions: \(previewWidth)x\(previewHeight)")
872
855
 
873
- self.cameraController.captureImage(width: width, height: height, aspectRatio: captureAspectRatio, quality: quality, gpsLocation: self.currentLocation) { (image, originalPhotoData, _, error) in
856
+ self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, originalPhotoData, _, error) in
874
857
  print("[CameraPreview] captureImage callback received")
875
858
  DispatchQueue.main.async {
876
859
  print("[CameraPreview] Processing capture on main thread")
@@ -895,62 +878,38 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
895
878
 
896
879
  print("[CameraPreview] Image data created, size: \(imageDataWithExif.count) bytes")
897
880
 
898
- if saveToGallery {
899
- print("[CameraPreview] Saving to gallery...")
900
- self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
901
- print("[CameraPreview] Save to gallery completed, success: \(success), error: \(error?.localizedDescription ?? "none")")
902
- let exifData = self.getExifData(from: imageDataWithExif)
903
-
904
- var result = JSObject()
905
- result["exif"] = exifData
906
- result["gallerySaved"] = success
907
- if !success, let error = error {
908
- result["galleryError"] = error.localizedDescription
909
- }
910
-
911
- if self.storeToFile == false {
912
- let base64Image = imageDataWithExif.base64EncodedString()
913
- result["value"] = base64Image
914
- } else {
915
- do {
916
- let fileUrl = self.getTempFilePath()
917
- try imageDataWithExif.write(to: fileUrl)
918
- result["value"] = fileUrl.absoluteString
919
- } catch {
920
- call.reject("Error writing image to file")
921
- }
922
- }
923
-
924
- print("[CameraPreview] Resolving capture call with gallery save")
925
- call.resolve(result)
926
- }
881
+ // Prepare the result first
882
+ let exifData = self.getExifData(from: imageDataWithExif)
883
+
884
+ var result = JSObject()
885
+ result["exif"] = exifData
886
+
887
+ if self.storeToFile == false {
888
+ let base64Image = imageDataWithExif.base64EncodedString()
889
+ result["value"] = base64Image
927
890
  } else {
928
- print("[CameraPreview] Not saving to gallery, returning image data")
929
- let exifData = self.getExifData(from: imageDataWithExif)
930
-
931
- if self.storeToFile == false {
932
- let base64Image = imageDataWithExif.base64EncodedString()
933
- var result = JSObject()
934
- result["value"] = base64Image
935
- result["exif"] = exifData
891
+ do {
892
+ let fileUrl = self.getTempFilePath()
893
+ try imageDataWithExif.write(to: fileUrl)
894
+ result["value"] = fileUrl.absoluteString
895
+ } catch {
896
+ call.reject("Error writing image to file")
897
+ return
898
+ }
899
+ }
936
900
 
937
- print("[CameraPreview] base64 - Resolving capture call")
938
- call.resolve(result)
939
- } else {
940
- do {
941
- let fileUrl = self.getTempFilePath()
942
- try imageDataWithExif.write(to: fileUrl)
943
- var result = JSObject()
944
- result["value"] = fileUrl.absoluteString
945
- result["exif"] = exifData
946
- print("[CameraPreview] filePath - Resolving capture call")
947
- call.resolve(result)
948
- } catch {
949
- call.reject("Error writing image to file")
901
+ // Save to gallery asynchronously if requested
902
+ if saveToGallery {
903
+ print("[CameraPreview] Saving to gallery asynchronously...")
904
+ DispatchQueue.global(qos: .utility).async {
905
+ self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
906
+ print("[CameraPreview] Save to gallery completed, success: \(success), error: \(error?.localizedDescription ?? "none")")
950
907
  }
951
908
  }
952
-
953
909
  }
910
+
911
+ print("[CameraPreview] Resolving capture call immediately")
912
+ call.resolve(result)
954
913
  }
955
914
  }
956
915
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.4.1",
3
+ "version": "7.5.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {