@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>
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
692
|
+
group.enter()
|
|
693
|
+
self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
|
|
694
|
+
gallerySuccess = success
|
|
683
695
|
if !success {
|
|
684
|
-
|
|
696
|
+
galleryError = error?.localizedDescription ?? "Unknown error"
|
|
697
|
+
print("CameraPreview: Error saving image to gallery: \(galleryError!)")
|
|
685
698
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|