@capgo/camera-preview 7.4.3 → 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.
@@ -1079,8 +1079,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1079
1079
  bytes = writeExifToImageBytes(bytes, exifInterface);
1080
1080
  }
1081
1081
 
1082
+ // Save to gallery asynchronously if requested
1082
1083
  if (saveToGallery) {
1083
- saveImageToGallery(bytes);
1084
+ final byte[] finalBytes = bytes;
1085
+ new Thread(() -> {
1086
+ saveImageToGallery(finalBytes);
1087
+ }).start();
1084
1088
  }
1085
1089
 
1086
1090
  String resultValue;
@@ -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()
@@ -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
@@ -684,16 +735,28 @@ extension CameraController {
684
735
 
685
736
  // Determine what to do based on parameters
686
737
  if width != nil || height != nil {
687
- // Resize to fit within maximum dimensions while maintaining aspect ratio
688
- finalImage = self.resizeImageToMaxDimensions(image: image, maxWidth: width, maxHeight: height)!
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)")
743
+ }
744
+ // Then resize to fit within maximum dimensions while maintaining aspect ratio
745
+ finalImage = self.resizeImageToMaxDimensions(image: finalImage, maxWidth: width, maxHeight: height)!
689
746
  print("[CameraPreview] Resized to max dimensions: \(finalImage.size.width)x\(finalImage.size.height)")
690
- } else {
691
- // No parameters specified - crop to match what's visible in the preview
692
- // This ensures we capture exactly what the user sees
693
- if let previewLayer = self.previewLayer,
694
- let previewCroppedImage = self.cropImageToMatchPreview(image: image, previewLayer: previewLayer) {
695
- finalImage = previewCroppedImage
696
- print("[CameraPreview] Cropped to match preview: \(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
+ }
697
760
  }
698
761
  }
699
762
 
@@ -774,7 +837,36 @@ extension CameraController {
774
837
  return resizeImage(image: image, to: targetSize)
775
838
  }
776
839
 
777
-
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
+
863
+ guard let cgImage = image.cgImage,
864
+ let croppedCGImage = cgImage.cropping(to: cropRect) else {
865
+ return nil
866
+ }
867
+
868
+ return UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: image.imageOrientation)
869
+ }
778
870
 
779
871
  func cropImageToMatchPreview(image: UIImage, previewLayer: AVCaptureVideoPreviewLayer) -> UIImage? {
780
872
  // When using resizeAspectFill, the preview layer shows a cropped portion of the video
@@ -878,62 +878,38 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
878
878
 
879
879
  print("[CameraPreview] Image data created, size: \(imageDataWithExif.count) bytes")
880
880
 
881
- if saveToGallery {
882
- print("[CameraPreview] Saving to gallery...")
883
- self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
884
- print("[CameraPreview] Save to gallery completed, success: \(success), error: \(error?.localizedDescription ?? "none")")
885
- let exifData = self.getExifData(from: imageDataWithExif)
886
-
887
- var result = JSObject()
888
- result["exif"] = exifData
889
- result["gallerySaved"] = success
890
- if !success, let error = error {
891
- result["galleryError"] = error.localizedDescription
892
- }
893
-
894
- if self.storeToFile == false {
895
- let base64Image = imageDataWithExif.base64EncodedString()
896
- result["value"] = base64Image
897
- } else {
898
- do {
899
- let fileUrl = self.getTempFilePath()
900
- try imageDataWithExif.write(to: fileUrl)
901
- result["value"] = fileUrl.absoluteString
902
- } catch {
903
- call.reject("Error writing image to file")
904
- }
905
- }
906
-
907
- print("[CameraPreview] Resolving capture call with gallery save")
908
- call.resolve(result)
909
- }
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
910
890
  } else {
911
- print("[CameraPreview] Not saving to gallery, returning image data")
912
- let exifData = self.getExifData(from: imageDataWithExif)
913
-
914
- if self.storeToFile == false {
915
- let base64Image = imageDataWithExif.base64EncodedString()
916
- var result = JSObject()
917
- result["value"] = base64Image
918
- 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
+ }
919
900
 
920
- print("[CameraPreview] base64 - Resolving capture call")
921
- call.resolve(result)
922
- } else {
923
- do {
924
- let fileUrl = self.getTempFilePath()
925
- try imageDataWithExif.write(to: fileUrl)
926
- var result = JSObject()
927
- result["value"] = fileUrl.absoluteString
928
- result["exif"] = exifData
929
- print("[CameraPreview] filePath - Resolving capture call")
930
- call.resolve(result)
931
- } catch {
932
- 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")")
933
907
  }
934
908
  }
935
-
936
909
  }
910
+
911
+ print("[CameraPreview] Resolving capture call immediately")
912
+ call.resolve(result)
937
913
  }
938
914
  }
939
915
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.4.3",
3
+ "version": "7.5.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {