@capgo/camera-preview 7.4.0-beta.8 → 7.4.0-beta.9

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.
package/README.md CHANGED
@@ -128,7 +128,7 @@ In your `ios/App/App/Info.plist`, you must provide descriptions for the permissi
128
128
 
129
129
  - **Saving to Gallery** (`saveToGallery: true`):
130
130
  ```xml
131
- <key>NSPhotoLibraryAddUsageDescription</key>
131
+ <key>NSPhotoLibraryUsageDescription</key>
132
132
  <string>To save photos to your gallery</string>
133
133
  ```
134
134
 
@@ -492,6 +492,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
492
492
  ByteArrayOutputStream stream = new ByteArrayOutputStream();
493
493
  resizedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
494
494
  bytes = stream.toByteArray();
495
+
496
+ // Write EXIF data back to resized image
497
+ bytes = writeExifToImageBytes(bytes, exifInterface);
498
+ } else {
499
+ // For non-resized images, ensure EXIF is saved
500
+ exifInterface.saveAttributes();
501
+ bytes = new byte[(int) tempFile.length()];
502
+ java.io.FileInputStream fis2 = new java.io.FileInputStream(tempFile);
503
+ fis2.read(bytes);
504
+ fis2.close();
495
505
  }
496
506
 
497
507
  if (saveToGallery) {
@@ -680,6 +690,46 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
680
690
  {ExifInterface.TAG_Y_RESOLUTION, "YResolution"}
681
691
  };
682
692
 
693
+ private byte[] writeExifToImageBytes(byte[] imageBytes, ExifInterface sourceExif) {
694
+ try {
695
+ // Create a temporary file to write the image with EXIF
696
+ File tempExifFile = File.createTempFile("temp_exif", ".jpg", context.getCacheDir());
697
+
698
+ // Write the image bytes to temp file
699
+ java.io.FileOutputStream fos = new java.io.FileOutputStream(tempExifFile);
700
+ fos.write(imageBytes);
701
+ fos.close();
702
+
703
+ // Create new ExifInterface for the temp file and copy all EXIF data
704
+ ExifInterface newExif = new ExifInterface(tempExifFile.getAbsolutePath());
705
+
706
+ // Copy all EXIF attributes from source to new
707
+ for (String[] tag : EXIF_TAGS) {
708
+ String value = sourceExif.getAttribute(tag[0]);
709
+ if (value != null) {
710
+ newExif.setAttribute(tag[0], value);
711
+ }
712
+ }
713
+
714
+ // Save the EXIF data
715
+ newExif.saveAttributes();
716
+
717
+ // Read the file back with EXIF embedded
718
+ byte[] result = new byte[(int) tempExifFile.length()];
719
+ java.io.FileInputStream fis = new java.io.FileInputStream(tempExifFile);
720
+ fis.read(result);
721
+ fis.close();
722
+
723
+ // Clean up temp file
724
+ tempExifFile.delete();
725
+
726
+ return result;
727
+ } catch (Exception e) {
728
+ Log.e(TAG, "writeExifToImageBytes: Error writing EXIF data", e);
729
+ return imageBytes; // Return original bytes if error
730
+ }
731
+ }
732
+
683
733
  public void captureSample(int quality) {
684
734
  Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality);
685
735
 
@@ -4,6 +4,7 @@ import AVFoundation
4
4
  import Photos
5
5
  import CoreImage
6
6
  import CoreLocation
7
+ import MobileCoreServices
7
8
 
8
9
 
9
10
  extension UIWindow {
@@ -676,30 +677,53 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
676
677
  return
677
678
  }
678
679
 
680
+ var gallerySuccess = true
681
+ var galleryError: String?
682
+
683
+ let group = DispatchGroup()
684
+
685
+ group.notify(queue: .main) {
686
+ guard let imageDataWithExif = self.createImageDataWithExif(from: image!, quality: Int(quality), location: withExifLocation ? self.currentLocation : nil) else {
687
+ call.reject("Failed to create image data with EXIF")
688
+ return
689
+ }
690
+
679
691
  if saveToGallery {
680
- PHPhotoLibrary.shared().performChanges({
681
- PHAssetChangeRequest.creationRequestForAsset(from: image!)
682
- }, completionHandler: { (success, error) in
692
+ group.enter()
693
+ self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
694
+ gallerySuccess = success
683
695
  if !success {
684
- print("CameraPreview: Error saving image to gallery: \(error?.localizedDescription ?? "Unknown error")")
696
+ galleryError = error?.localizedDescription ?? "Unknown error"
697
+ print("CameraPreview: Error saving image to gallery: \(galleryError!)")
685
698
  }
686
- })
687
- }
688
-
689
- guard let imageData = image?.jpegData(compressionQuality: CGFloat(quality / 100.0)) else {
690
- call.reject("Failed to get JPEG data from image")
691
- return
699
+ group.leave()
700
+ }
701
+
702
+ group.notify(queue: .main) {
703
+ let exifData = self.getExifData(from: imageDataWithExif)
704
+ let base64Image = imageDataWithExif.base64EncodedString()
705
+
706
+ var result = JSObject()
707
+ result["value"] = base64Image
708
+ result["exif"] = exifData
709
+ result["gallerySaved"] = gallerySuccess
710
+ if !gallerySuccess, let error = galleryError {
711
+ result["galleryError"] = error
712
+ }
713
+
714
+ call.resolve(result)
715
+ }
716
+ } else {
717
+ let exifData = self.getExifData(from: imageDataWithExif)
718
+ let base64Image = imageDataWithExif.base64EncodedString()
719
+
720
+ var result = JSObject()
721
+ result["value"] = base64Image
722
+ result["exif"] = exifData
723
+
724
+ call.resolve(result)
692
725
  }
693
-
694
-
695
- let exifData = self.getExifData(from: imageData)
696
- let base64Image = imageData.base64EncodedString()
697
-
698
-
699
- var result = JSObject()
700
- result["value"] = base64Image
701
- result["exif"] = exifData
702
- call.resolve(result)
726
+ }
703
727
  }
704
728
  }
705
729
  }
@@ -735,6 +759,82 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
735
759
  return exifData
736
760
  }
737
761
 
762
+ private func createImageDataWithExif(from image: UIImage, quality: Int, location: CLLocation?) -> Data? {
763
+ guard let originalImageData = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
764
+ return nil
765
+ }
766
+
767
+ guard let imageSource = CGImageSourceCreateWithData(originalImageData as CFData, nil),
768
+ let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
769
+ let cgImage = image.cgImage else {
770
+ return originalImageData
771
+ }
772
+
773
+ let mutableData = NSMutableData()
774
+ guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
775
+ return originalImageData
776
+ }
777
+
778
+ var finalProperties = imageProperties
779
+
780
+ // Add GPS location if available
781
+ if let location = location {
782
+ let formatter = DateFormatter()
783
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
784
+ formatter.timeZone = TimeZone(abbreviation: "UTC")
785
+
786
+ let gpsDict: [String: Any] = [
787
+ kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
788
+ kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
789
+ kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
790
+ kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
791
+ kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
792
+ kCGImagePropertyGPSAltitude as String: location.altitude,
793
+ kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
794
+ ]
795
+
796
+ finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
797
+ }
798
+
799
+ // Add lens information
800
+ do {
801
+ let currentZoom = try self.cameraController.getZoom()
802
+ let lensInfo = try self.cameraController.getCurrentLensInfo()
803
+
804
+ // Create or update EXIF dictionary
805
+ var exifDict = finalProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] ?? [:]
806
+
807
+ // Add focal length (in mm)
808
+ exifDict[kCGImagePropertyExifFocalLength as String] = lensInfo.focalLength
809
+
810
+ // Add digital zoom ratio
811
+ let digitalZoom = Float(currentZoom.current) / lensInfo.baseZoomRatio
812
+ exifDict[kCGImagePropertyExifDigitalZoomRatio as String] = digitalZoom
813
+
814
+ // Add lens model info
815
+ exifDict[kCGImagePropertyExifLensModel as String] = lensInfo.deviceType
816
+
817
+ finalProperties[kCGImagePropertyExifDictionary as String] = exifDict
818
+
819
+ // Create or update TIFF dictionary for device info
820
+ var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
821
+ tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
822
+ tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
823
+ finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
824
+
825
+ } catch {
826
+ print("CameraPreview: Failed to get lens information: \(error)")
827
+ }
828
+
829
+ CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
830
+
831
+ if CGImageDestinationFinalize(destination) {
832
+ return mutableData as Data
833
+ }
834
+
835
+ return originalImageData
836
+ }
837
+
738
838
  @objc func captureSample(_ call: CAPPluginCall) {
739
839
  DispatchQueue.main.async {
740
840
  let quality: Int? = call.getInt("quality", 85)
@@ -1081,6 +1181,64 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1081
1181
  print("CameraPreview: Failed to get location: \(error.localizedDescription)")
1082
1182
  }
1083
1183
 
1184
+ private func saveImageDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
1185
+ // Check if NSPhotoLibraryUsageDescription is present in Info.plist
1186
+ guard Bundle.main.object(forInfoDictionaryKey: "NSPhotoLibraryUsageDescription") != nil else {
1187
+ let error = NSError(domain: "CameraPreview", code: 2, userInfo: [
1188
+ NSLocalizedDescriptionKey: "NSPhotoLibraryUsageDescription key missing from Info.plist. Add this key with a description of how your app uses photo library access."
1189
+ ])
1190
+ completion(false, error)
1191
+ return
1192
+ }
1193
+
1194
+ let status = PHPhotoLibrary.authorizationStatus()
1195
+
1196
+ switch status {
1197
+ case .authorized:
1198
+ performSaveDataToGallery(imageData: imageData, completion: completion)
1199
+ case .notDetermined:
1200
+ PHPhotoLibrary.requestAuthorization { newStatus in
1201
+ DispatchQueue.main.async {
1202
+ if newStatus == .authorized {
1203
+ self.performSaveDataToGallery(imageData: imageData, completion: completion)
1204
+ } else {
1205
+ completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
1206
+ }
1207
+ }
1208
+ }
1209
+ case .denied, .restricted:
1210
+ completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
1211
+ case .limited:
1212
+ performSaveDataToGallery(imageData: imageData, completion: completion)
1213
+ @unknown default:
1214
+ completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown photo library authorization status"]))
1215
+ }
1216
+ }
1217
+
1218
+ private func performSaveDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
1219
+ // Create a temporary file to write the JPEG data with EXIF
1220
+ let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".jpg")
1221
+
1222
+ do {
1223
+ try imageData.write(to: tempURL)
1224
+
1225
+ PHPhotoLibrary.shared().performChanges({
1226
+ PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: tempURL)
1227
+ }, completionHandler: { success, error in
1228
+ // Clean up temporary file
1229
+ try? FileManager.default.removeItem(at: tempURL)
1230
+
1231
+ DispatchQueue.main.async {
1232
+ completion(success, error)
1233
+ }
1234
+ })
1235
+ } catch {
1236
+ DispatchQueue.main.async {
1237
+ completion(false, error)
1238
+ }
1239
+ }
1240
+ }
1241
+
1084
1242
  private func updateCameraFrame() {
1085
1243
  guard let width = self.width, var height = self.height, let posX = self.posX, let posY = self.posY else {
1086
1244
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.4.0-beta.8",
3
+ "version": "7.4.0-beta.9",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {