@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
|
-
|
|
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
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
688
|
+
// Configure photo capture settings
|
|
641
689
|
if #available(iOS 13.0, *) {
|
|
642
|
-
|
|
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
|
-
//
|
|
688
|
-
|
|
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
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
}
|