@capgo/camera-preview 7.4.0-alpha.1 → 7.4.0-alpha.4

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.
@@ -102,6 +102,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
102
102
  private GridOverlayView gridOverlayView;
103
103
  private FrameLayout previewContainer;
104
104
  private View focusIndicatorView;
105
+ private long focusIndicatorAnimationId = 0; // Incrementing token to invalidate previous animations
105
106
  private CameraSelector currentCameraSelector;
106
107
  private String currentDeviceId;
107
108
  private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
@@ -1040,12 +1041,40 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1040
1041
  saveImageToGallery(bytes);
1041
1042
  }
1042
1043
 
1043
- String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
1044
+ String resultValue;
1045
+ boolean returnFileUri =
1046
+ sessionConfig != null && sessionConfig.isStoreToFile();
1047
+ if (returnFileUri) {
1048
+ // Persist processed image to a file and return its URI to avoid heavy base64 bridging
1049
+ try {
1050
+ String fileName =
1051
+ "cpcp_" +
1052
+ new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
1053
+ new java.util.Date()
1054
+ ) +
1055
+ ".jpg";
1056
+ File outDir = context.getCacheDir();
1057
+ File outFile = new File(outDir, fileName);
1058
+ FileOutputStream outFos = new FileOutputStream(outFile);
1059
+ outFos.write(bytes);
1060
+ outFos.close();
1061
+
1062
+ // Return a file path; apps can convert via Capacitor.convertFileSrc on JS side
1063
+ resultValue = outFile.getAbsolutePath();
1064
+ } catch (IOException ioEx) {
1065
+ Log.e(TAG, "capturePhoto: Failed to write image file", ioEx);
1066
+ // Fallback to base64 if file write fails
1067
+ resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
1068
+ }
1069
+ } else {
1070
+ // Backward-compatible behavior
1071
+ resultValue = Base64.encodeToString(bytes, Base64.NO_WRAP);
1072
+ }
1044
1073
 
1045
1074
  tempFile.delete();
1046
1075
 
1047
1076
  if (listener != null) {
1048
- listener.onPictureTaken(base64, exifData);
1077
+ listener.onPictureTaken(resultValue, exifData);
1049
1078
  }
1050
1079
  } catch (Exception e) {
1051
1080
  Log.e(TAG, "capturePhoto: Error processing image", e);
@@ -1748,8 +1777,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1748
1777
  return;
1749
1778
  }
1750
1779
 
1751
- // Remove any existing focus indicator
1780
+ // Remove any existing focus indicator and cancel its animation
1752
1781
  if (focusIndicatorView != null) {
1782
+ try {
1783
+ focusIndicatorView.clearAnimation();
1784
+ } catch (Exception ignore) {}
1753
1785
  previewContainer.removeView(focusIndicatorView);
1754
1786
  focusIndicatorView = null;
1755
1787
  }
@@ -1783,6 +1815,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1783
1815
  container.setBackground(drawable);
1784
1816
 
1785
1817
  focusIndicatorView = container;
1818
+ // Bump animation token; everything after this must validate against this token
1819
+ final long thisAnimationId = ++focusIndicatorAnimationId;
1820
+ final View thisIndicatorView = focusIndicatorView;
1786
1821
 
1787
1822
  // Set initial state for smooth animation
1788
1823
  focusIndicatorView.setAlpha(1f); // Start visible
@@ -1835,7 +1870,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1835
1870
  new Runnable() {
1836
1871
  @Override
1837
1872
  public void run() {
1838
- if (focusIndicatorView != null) {
1873
+ // Ensure this runnable belongs to the latest indicator
1874
+ if (
1875
+ focusIndicatorView != null &&
1876
+ thisIndicatorView == focusIndicatorView &&
1877
+ thisAnimationId == focusIndicatorAnimationId
1878
+ ) {
1839
1879
  // Smooth fade to semi-transparent
1840
1880
  AlphaAnimation fadeToTransparent = new AlphaAnimation(1f, 0.4f);
1841
1881
  fadeToTransparent.setDuration(400);
@@ -1857,7 +1897,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1857
1897
  "showFocusIndicator: Fade to transparent ended, starting final fade out"
1858
1898
  );
1859
1899
  // Final smooth fade out and scale down
1860
- if (focusIndicatorView != null) {
1900
+ if (
1901
+ focusIndicatorView != null &&
1902
+ thisIndicatorView == focusIndicatorView &&
1903
+ thisAnimationId == focusIndicatorAnimationId
1904
+ ) {
1861
1905
  AnimationSet finalAnimation = new AnimationSet(false);
1862
1906
 
1863
1907
  AlphaAnimation finalFadeOut = new AlphaAnimation(0.4f, 0f);
@@ -1905,8 +1949,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1905
1949
  // Remove the focus indicator
1906
1950
  if (
1907
1951
  focusIndicatorView != null &&
1908
- previewContainer != null
1952
+ previewContainer != null &&
1953
+ thisIndicatorView == focusIndicatorView &&
1954
+ thisAnimationId == focusIndicatorAnimationId
1909
1955
  ) {
1956
+ try {
1957
+ focusIndicatorView.clearAnimation();
1958
+ } catch (Exception ignore) {}
1910
1959
  previewContainer.removeView(focusIndicatorView);
1911
1960
  focusIndicatorView = null;
1912
1961
  }
@@ -1917,7 +1966,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1917
1966
  }
1918
1967
  );
1919
1968
 
1920
- focusIndicatorView.startAnimation(finalAnimation);
1969
+ if (
1970
+ thisIndicatorView == focusIndicatorView &&
1971
+ thisAnimationId == focusIndicatorAnimationId
1972
+ ) {
1973
+ focusIndicatorView.startAnimation(finalAnimation);
1974
+ }
1921
1975
  }
1922
1976
  }
1923
1977
 
@@ -1926,7 +1980,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1926
1980
  }
1927
1981
  );
1928
1982
 
1929
- focusIndicatorView.startAnimation(fadeToTransparent);
1983
+ if (
1984
+ thisIndicatorView == focusIndicatorView &&
1985
+ thisAnimationId == focusIndicatorAnimationId
1986
+ ) {
1987
+ focusIndicatorView.startAnimation(fadeToTransparent);
1988
+ }
1930
1989
  }
1931
1990
  }
1932
1991
  },
@@ -25,7 +25,7 @@ class CameraController: NSObject {
25
25
  var focusIndicatorView: UIView?
26
26
 
27
27
  var flashMode = AVCaptureDevice.FlashMode.off
28
- var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
28
+ var photoCaptureCompletionBlock: ((UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void)?
29
29
 
30
30
  var sampleBufferCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
31
31
 
@@ -555,11 +555,11 @@ extension CameraController {
555
555
  self.updateVideoOrientation()
556
556
  }
557
557
 
558
- func captureImage(width: Int?, height: Int?, aspectRatio: String?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Error?) -> Void) {
558
+ func captureImage(width: Int?, height: Int?, aspectRatio: String?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
559
559
  print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil")")
560
560
 
561
561
  guard let photoOutput = self.photoOutput else {
562
- completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
562
+ completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
563
563
  return
564
564
  }
565
565
 
@@ -585,14 +585,14 @@ extension CameraController {
585
585
  }
586
586
  }
587
587
 
588
- self.photoCaptureCompletionBlock = { (image, error) in
588
+ self.photoCaptureCompletionBlock = { (image, photoData, metadata, error) in
589
589
  if let error = error {
590
- completion(nil, error)
590
+ completion(nil, nil, nil, error)
591
591
  return
592
592
  }
593
593
 
594
594
  guard let image = image else {
595
- completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
595
+ completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
596
596
  return
597
597
  }
598
598
 
@@ -646,7 +646,7 @@ extension CameraController {
646
646
  }
647
647
  }
648
648
 
649
- completion(finalImage, nil)
649
+ completion(finalImage, photoData, metadata, nil)
650
650
  }
651
651
 
652
652
  photoOutput.capturePhoto(with: settings, delegate: self)
@@ -1460,22 +1460,23 @@ extension CameraController: UIGestureRecognizerDelegate {
1460
1460
  extension CameraController: AVCapturePhotoCaptureDelegate {
1461
1461
  public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
1462
1462
  if let error = error {
1463
- self.photoCaptureCompletionBlock?(nil, error)
1463
+ self.photoCaptureCompletionBlock?(nil, nil, nil, error)
1464
1464
  return
1465
1465
  }
1466
1466
 
1467
1467
  // Get the photo data using the modern API
1468
1468
  guard let imageData = photo.fileDataRepresentation() else {
1469
- self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
1469
+ self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
1470
1470
  return
1471
1471
  }
1472
1472
 
1473
1473
  guard let image = UIImage(data: imageData) else {
1474
- self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
1474
+ self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
1475
1475
  return
1476
1476
  }
1477
1477
 
1478
- self.photoCaptureCompletionBlock?(image.fixedOrientation(), nil)
1478
+ // Pass through original file data and metadata so callers can preserve EXIF
1479
+ self.photoCaptureCompletionBlock?(image.fixedOrientation(), imageData, photo.metadata, nil)
1479
1480
  }
1480
1481
  }
1481
1482
 
@@ -705,7 +705,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
705
705
  print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
706
706
  print("[CameraPreview] Preview dimensions: \(self.previewView.frame.width)x\(self.previewView.frame.height)")
707
707
 
708
- self.cameraController.captureImage(width: width, height: height, aspectRatio: captureAspectRatio, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
708
+ self.cameraController.captureImage(width: width, height: height, aspectRatio: captureAspectRatio, quality: quality, gpsLocation: self.currentLocation) { (image, originalPhotoData, originalMetadata, error) in
709
709
  print("[CameraPreview] captureImage callback received")
710
710
  DispatchQueue.main.async {
711
711
  print("[CameraPreview] Processing capture on main thread")
@@ -719,7 +719,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
719
719
  let imageDataWithExif = self.createImageDataWithExif(
720
720
  from: image,
721
721
  quality: Int(quality),
722
- location: withExifLocation ? self.currentLocation : nil
722
+ location: withExifLocation ? self.currentLocation : nil,
723
+ originalPhotoData: originalPhotoData
723
724
  )
724
725
  else {
725
726
  print("[CameraPreview] Failed to create image data with EXIF")
@@ -792,24 +793,29 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
792
793
  return exifData
793
794
  }
794
795
 
795
- private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?) -> Data? {
796
- guard let originalImageData = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
796
+ private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?, originalPhotoData: Data?) -> Data? {
797
+ guard let jpegDataAtQuality = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
797
798
  return nil
798
799
  }
799
800
 
800
- guard let imageSource = CGImageSourceCreateWithData(originalImageData as CFData, nil),
801
+ // Prefer metadata from the original AVCapturePhoto file data to preserve lens/EXIF
802
+ let sourceDataForMetadata = (originalPhotoData ?? jpegDataAtQuality) as CFData
803
+ guard let imageSource = CGImageSourceCreateWithData(sourceDataForMetadata, nil),
801
804
  let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
802
805
  let cgImage = image.cgImage else {
803
- return originalImageData
806
+ return jpegDataAtQuality
804
807
  }
805
808
 
806
809
  let mutableData = NSMutableData()
807
810
  guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
808
- return originalImageData
811
+ return jpegDataAtQuality
809
812
  }
810
813
 
811
814
  var finalProperties = imageProperties
812
815
 
816
+ // Ensure orientation reflects the pixel data (we pass an orientation-fixed UIImage)
817
+ finalProperties[kCGImagePropertyOrientation as String] = 1
818
+
813
819
  // Add GPS location if available
814
820
  if let location = location {
815
821
  let formatter = DateFormatter()
@@ -829,10 +835,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
829
835
  finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
830
836
  }
831
837
 
832
- // Create or update TIFF dictionary for device info
838
+ // Create or update TIFF dictionary for device info and set orientation to Up
833
839
  var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
834
840
  tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
835
841
  tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
842
+ tiffDict[kCGImagePropertyTIFFOrientation as String] = 1
836
843
  finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
837
844
 
838
845
  CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
@@ -841,7 +848,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
841
848
  return mutableData as Data
842
849
  }
843
850
 
844
- return originalImageData
851
+ return jpegDataAtQuality
845
852
  }
846
853
 
847
854
  @objc func captureSample(_ call: CAPPluginCall) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.4.0-alpha.1",
3
+ "version": "7.4.0-alpha.4",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {