@connectedbits/react-native-image-picker 2.4.16-rc2 → 2.4.17
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/android/build.gradle +7 -12
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/imagepicker/ImagePickerModule.java +227 -243
- package/ios/ImagePickerManager.h +1 -1
- package/ios/ImagePickerManager.m +659 -443
- package/package.json +2 -2
- package/react-native-image-picker.podspec +2 -1
- package/android/.idea/.gitignore +0 -3
package/ios/ImagePickerManager.m
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
#import "ImagePickerManager.h"
|
|
2
2
|
#import <React/RCTConvert.h>
|
|
3
|
-
#import <
|
|
3
|
+
#import <React/RCTUtils.h>
|
|
4
4
|
#import <AVFoundation/AVFoundation.h>
|
|
5
|
+
#import <CoreLocation/CoreLocation.h>
|
|
6
|
+
#import <ImageIO/ImageIO.h>
|
|
5
7
|
#import <Photos/Photos.h>
|
|
6
|
-
#import <
|
|
8
|
+
#import <PhotosUI/PhotosUI.h>
|
|
9
|
+
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
// NOTE: This module deliberately does not use AssetsLibrary (ALAssetsLibrary). It was
|
|
12
|
+
// deprecated in iOS 9 and, as of iOS 27.2, crashes on first use
|
|
13
|
+
// (-[PLManagedObjectContext setIsBackingALAssetsLibrary:]: unrecognized selector).
|
|
14
|
+
//
|
|
15
|
+
// - Camera capture uses UIImagePickerController (still the supported system camera UI).
|
|
16
|
+
// - Library selection uses PHPickerViewController, which needs no photo library permission.
|
|
17
|
+
// Location is read from the picked file's EXIF first, then from the PHAsset when the app
|
|
18
|
+
// already has (full or limited) photo library access.
|
|
9
19
|
|
|
10
|
-
@interface ImagePickerManager ()
|
|
20
|
+
@interface ImagePickerManager () <PHPickerViewControllerDelegate>
|
|
11
21
|
|
|
12
22
|
@property (nonatomic, strong) UIImagePickerController *picker;
|
|
13
23
|
@property (nonatomic, strong) RCTResponseSenderBlock callback;
|
|
14
|
-
@property (nonatomic, strong) NSDictionary *
|
|
15
|
-
@property (nonatomic, retain) NSDictionary *options;
|
|
16
|
-
@property (nonatomic, retain) NSMutableDictionary *response;
|
|
24
|
+
@property (nonatomic, strong) NSDictionary *options;
|
|
17
25
|
@property (nonatomic, strong) NSArray *customButtons;
|
|
18
26
|
|
|
19
27
|
@end
|
|
@@ -22,6 +30,11 @@
|
|
|
22
30
|
|
|
23
31
|
RCT_EXPORT_MODULE();
|
|
24
32
|
|
|
33
|
+
+ (BOOL)requiresMainQueueSetup
|
|
34
|
+
{
|
|
35
|
+
return NO;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
RCT_EXPORT_METHOD(launchCamera:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback)
|
|
26
39
|
{
|
|
27
40
|
self.callback = callback;
|
|
@@ -45,29 +58,29 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
45
58
|
|
|
46
59
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
47
60
|
|
|
48
|
-
NSString *title = [self
|
|
49
|
-
if (
|
|
61
|
+
NSString *title = [self stringOption:@"title"];
|
|
62
|
+
if (title.length == 0) {
|
|
50
63
|
title = nil; // A more visually appealing UIAlertControl is displayed with a nil title rather than title = @""
|
|
51
64
|
}
|
|
52
|
-
NSString *cancelTitle = [self
|
|
53
|
-
NSString *takePhotoButtonTitle = [self
|
|
54
|
-
NSString *chooseFromLibraryButtonTitle = [self
|
|
65
|
+
NSString *cancelTitle = [self stringOption:@"cancelButtonTitle"];
|
|
66
|
+
NSString *takePhotoButtonTitle = [self stringOption:@"takePhotoButtonTitle"];
|
|
67
|
+
NSString *chooseFromLibraryButtonTitle = [self stringOption:@"chooseFromLibraryButtonTitle"];
|
|
55
68
|
|
|
56
69
|
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
|
57
70
|
alertController.view.tintColor = [RCTConvert UIColor:options[@"tintColor"]];
|
|
58
71
|
|
|
59
72
|
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:cancelTitle style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {
|
|
60
|
-
self
|
|
73
|
+
[self sendResponse:@{@"didCancel": @YES}];
|
|
61
74
|
}];
|
|
62
75
|
[alertController addAction:cancelAction];
|
|
63
76
|
|
|
64
|
-
if (
|
|
77
|
+
if (takePhotoButtonTitle.length > 0) {
|
|
65
78
|
UIAlertAction *takePhotoAction = [UIAlertAction actionWithTitle:takePhotoButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
66
79
|
[self actionHandler:action];
|
|
67
80
|
}];
|
|
68
81
|
[alertController addAction:takePhotoAction];
|
|
69
82
|
}
|
|
70
|
-
if (
|
|
83
|
+
if (chooseFromLibraryButtonTitle.length > 0) {
|
|
71
84
|
UIAlertAction *chooseFromLibraryAction = [UIAlertAction actionWithTitle:chooseFromLibraryButtonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
72
85
|
[self actionHandler:action];
|
|
73
86
|
}];
|
|
@@ -75,11 +88,11 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
// Add custom buttons to action sheet
|
|
78
|
-
if ([
|
|
91
|
+
if ([[self.options objectForKey:@"customButtons"] isKindOfClass:[NSArray class]]) {
|
|
79
92
|
self.customButtons = [self.options objectForKey:@"customButtons"];
|
|
80
|
-
for (
|
|
81
|
-
NSString *
|
|
82
|
-
UIAlertAction *customAction = [UIAlertAction actionWithTitle:
|
|
93
|
+
for (NSDictionary *button in self.customButtons) {
|
|
94
|
+
NSString *buttonTitle = [button valueForKey:@"title"];
|
|
95
|
+
UIAlertAction *customAction = [UIAlertAction actionWithTitle:buttonTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {
|
|
83
96
|
[self actionHandler:action];
|
|
84
97
|
}];
|
|
85
98
|
[alertController addAction:customAction];
|
|
@@ -94,7 +107,7 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
94
107
|
alertController.popoverPresentationController.sourceView = root.view;
|
|
95
108
|
alertController.popoverPresentationController.sourceRect = CGRectMake(root.view.bounds.size.width / 2.0, root.view.bounds.size.height, 1.0, 1.0);
|
|
96
109
|
|
|
97
|
-
if (
|
|
110
|
+
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
|
98
111
|
alertController.popoverPresentationController.permittedArrowDirections = 0;
|
|
99
112
|
for (id subview in alertController.view.subviews) {
|
|
100
113
|
if ([subview isMemberOfClass:[UIView class]]) {
|
|
@@ -115,19 +128,21 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
115
128
|
if (results.count > 0) {
|
|
116
129
|
NSString *customButtonStr = [[results objectAtIndex:0] objectForKey:@"name"];
|
|
117
130
|
if (customButtonStr) {
|
|
118
|
-
self
|
|
131
|
+
[self sendResponse:@{@"customButton": customButtonStr}];
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
if ([action.title isEqualToString:[self
|
|
136
|
+
if ([action.title isEqualToString:[self stringOption:@"takePhotoButtonTitle"]]) { // Take photo
|
|
124
137
|
[self launchImagePicker:RNImagePickerTargetCamera];
|
|
125
138
|
}
|
|
126
|
-
else if ([action.title isEqualToString:[self
|
|
139
|
+
else if ([action.title isEqualToString:[self stringOption:@"chooseFromLibraryButtonTitle"]]) { // Choose from library
|
|
127
140
|
[self launchImagePicker:RNImagePickerTargetLibrarySingleImage];
|
|
128
141
|
}
|
|
129
142
|
}
|
|
130
143
|
|
|
144
|
+
#pragma mark - Launching
|
|
145
|
+
|
|
131
146
|
- (void)launchImagePicker:(RNImagePickerTarget)target options:(NSDictionary *)options
|
|
132
147
|
{
|
|
133
148
|
self.options = options;
|
|
@@ -136,508 +151,596 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
136
151
|
|
|
137
152
|
- (void)launchImagePicker:(RNImagePickerTarget)target
|
|
138
153
|
{
|
|
139
|
-
self.picker = [[UIImagePickerController alloc] init];
|
|
140
|
-
|
|
141
154
|
if (target == RNImagePickerTargetCamera) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
#else
|
|
146
|
-
self.picker.sourceType = UIImagePickerControllerSourceTypeCamera;
|
|
147
|
-
if ([[self.options objectForKey:@"cameraType"] isEqualToString:@"front"]) {
|
|
148
|
-
self.picker.cameraDevice = UIImagePickerControllerCameraDeviceFront;
|
|
149
|
-
}
|
|
150
|
-
else { // "back"
|
|
151
|
-
self.picker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
|
|
152
|
-
}
|
|
153
|
-
#endif
|
|
155
|
+
[self launchCameraPicker];
|
|
156
|
+
} else {
|
|
157
|
+
[self launchLibraryPicker];
|
|
154
158
|
}
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
- (void)launchCameraPicker
|
|
162
|
+
{
|
|
163
|
+
#if TARGET_OS_SIMULATOR
|
|
164
|
+
[self sendResponse:@{@"error": @"Camera not available on simulator"}];
|
|
165
|
+
#else
|
|
166
|
+
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
|
|
167
|
+
[self sendResponse:@{@"error": @"Camera not available"}];
|
|
168
|
+
return;
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
|
172
|
+
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
|
|
173
|
+
if ([[self stringOption:@"cameraType"] isEqualToString:@"front"]) {
|
|
174
|
+
picker.cameraDevice = UIImagePickerControllerCameraDeviceFront;
|
|
175
|
+
} else {
|
|
176
|
+
picker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
|
|
177
|
+
}
|
|
161
178
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
NSString *mediaType = [self stringOption:@"mediaType"];
|
|
180
|
+
if ([mediaType isEqualToString:@"video"] || [mediaType isEqualToString:@"mixed"]) {
|
|
181
|
+
NSString *videoQuality = [self stringOption:@"videoQuality"];
|
|
182
|
+
if ([videoQuality isEqualToString:@"high"]) {
|
|
183
|
+
picker.videoQuality = UIImagePickerControllerQualityTypeHigh;
|
|
184
|
+
} else if ([videoQuality isEqualToString:@"low"]) {
|
|
185
|
+
picker.videoQuality = UIImagePickerControllerQualityTypeLow;
|
|
186
|
+
} else {
|
|
187
|
+
picker.videoQuality = UIImagePickerControllerQualityTypeMedium;
|
|
170
188
|
}
|
|
171
189
|
|
|
172
190
|
id durationLimit = [self.options objectForKey:@"durationLimit"];
|
|
173
|
-
if (durationLimit) {
|
|
174
|
-
|
|
175
|
-
self.picker.allowsEditing = NO;
|
|
191
|
+
if ([durationLimit isKindOfClass:[NSNumber class]]) {
|
|
192
|
+
picker.videoMaximumDuration = [durationLimit doubleValue];
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
|
-
if ([
|
|
179
|
-
|
|
180
|
-
} else if ([
|
|
181
|
-
|
|
195
|
+
if ([mediaType isEqualToString:@"video"]) {
|
|
196
|
+
picker.mediaTypes = @[UTTypeMovie.identifier];
|
|
197
|
+
} else if ([mediaType isEqualToString:@"mixed"]) {
|
|
198
|
+
picker.mediaTypes = @[UTTypeMovie.identifier, UTTypeImage.identifier];
|
|
182
199
|
} else {
|
|
183
|
-
|
|
200
|
+
picker.mediaTypes = @[UTTypeImage.identifier];
|
|
184
201
|
}
|
|
185
202
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
self.picker
|
|
190
|
-
self.picker.delegate = self;
|
|
203
|
+
picker.allowsEditing = [[self.options objectForKey:@"allowsEditing"] boolValue];
|
|
204
|
+
picker.modalPresentationStyle = UIModalPresentationFullScreen;
|
|
205
|
+
picker.delegate = self;
|
|
206
|
+
self.picker = picker;
|
|
191
207
|
|
|
192
|
-
|
|
193
|
-
|
|
208
|
+
[self checkCameraPermissions:^(BOOL granted) {
|
|
209
|
+
if (!granted) {
|
|
210
|
+
[self sendResponse:@{@"error": @"Camera permissions not granted"}];
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
194
213
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
195
|
-
|
|
196
|
-
[root presentViewController:self.picker animated:YES completion:nil];
|
|
214
|
+
[RCTPresentedViewController() presentViewController:picker animated:YES completion:nil];
|
|
197
215
|
});
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
[self checkCameraPermissions:^(BOOL granted) {
|
|
202
|
-
if (!granted) {
|
|
203
|
-
self.callback(@[@{@"error": @"Camera permissions not granted"}]);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
216
|
+
}];
|
|
217
|
+
#endif
|
|
218
|
+
}
|
|
206
219
|
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
- (void)launchLibraryPicker
|
|
221
|
+
{
|
|
222
|
+
// Passing the shared library gives us assetIdentifiers on results, which lets us fall back to
|
|
223
|
+
// PHAsset metadata (location, creation date, original filename) when the app has library access.
|
|
224
|
+
// PHPicker itself needs no permission, so we never block selection on authorization.
|
|
225
|
+
PHPickerConfiguration *configuration = [[PHPickerConfiguration alloc] initWithPhotoLibrary:[PHPhotoLibrary sharedPhotoLibrary]];
|
|
226
|
+
configuration.selectionLimit = 1;
|
|
227
|
+
// Hand back the asset as stored (no transcoding) so EXIF/GPS metadata and GIF frames are preserved.
|
|
228
|
+
configuration.preferredAssetRepresentationMode = PHPickerConfigurationAssetRepresentationModeCurrent;
|
|
229
|
+
|
|
230
|
+
NSString *mediaType = [self stringOption:@"mediaType"];
|
|
231
|
+
if ([mediaType isEqualToString:@"video"]) {
|
|
232
|
+
configuration.filter = [PHPickerFilter videosFilter];
|
|
233
|
+
} else if ([mediaType isEqualToString:@"mixed"]) {
|
|
234
|
+
configuration.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[[PHPickerFilter imagesFilter], [PHPickerFilter videosFilter]]];
|
|
235
|
+
} else {
|
|
236
|
+
configuration.filter = [PHPickerFilter imagesFilter];
|
|
209
237
|
}
|
|
210
|
-
else { // RNImagePickerTargetLibrarySingleImage
|
|
211
|
-
[self checkPhotosPermissions:^(BOOL granted) {
|
|
212
|
-
if (!granted) {
|
|
213
|
-
self.callback(@[@{@"error": @"Photo library permissions not granted"}]);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
238
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
}
|
|
239
|
+
PHPickerViewController *pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:configuration];
|
|
240
|
+
pickerViewController.delegate = self;
|
|
221
241
|
|
|
222
|
-
|
|
223
|
-
|
|
242
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
243
|
+
[RCTPresentedViewController() presentViewController:pickerViewController animated:YES completion:nil];
|
|
244
|
+
});
|
|
245
|
+
}
|
|
224
246
|
|
|
225
|
-
|
|
226
|
-
// Get the underlying resources for the PHAsset (PhotoKit)
|
|
227
|
-
NSArray<PHAssetResource *> *pickedAssetResources = [PHAssetResource assetResourcesForAsset:asset];
|
|
247
|
+
#pragma mark - PHPickerViewControllerDelegate (library)
|
|
228
248
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
249
|
+
- (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray<PHPickerResult *> *)results
|
|
250
|
+
{
|
|
251
|
+
NSDictionary *options = self.options;
|
|
252
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
253
|
+
PHPickerResult *result = results.firstObject;
|
|
254
|
+
if (!result) {
|
|
255
|
+
[self sendResponse:@{@"didCancel": @YES}];
|
|
256
|
+
return;
|
|
233
257
|
}
|
|
258
|
+
[self handlePickerResult:result options:options];
|
|
259
|
+
}];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
- (void)handlePickerResult:(PHPickerResult *)result options:(NSDictionary *)options
|
|
263
|
+
{
|
|
264
|
+
NSItemProvider *provider = result.itemProvider;
|
|
265
|
+
NSString *mediaType = [ImagePickerManager stringValue:options[@"mediaType"]];
|
|
266
|
+
|
|
267
|
+
BOOL isImage = [provider hasItemConformingToTypeIdentifier:UTTypeImage.identifier];
|
|
268
|
+
BOOL isMovie = [provider hasItemConformingToTypeIdentifier:UTTypeMovie.identifier];
|
|
269
|
+
BOOL treatAsVideo = isMovie && (!isImage || [mediaType isEqualToString:@"video"]);
|
|
270
|
+
|
|
271
|
+
if (!isImage && !isMovie) {
|
|
272
|
+
[self sendResponse:@{@"error": @"Unsupported media type"}];
|
|
273
|
+
return;
|
|
234
274
|
}
|
|
235
275
|
|
|
236
|
-
|
|
276
|
+
if (treatAsVideo) {
|
|
277
|
+
[provider loadFileRepresentationForTypeIdentifier:UTTypeMovie.identifier completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
|
|
278
|
+
// The file at `url` is deleted once this handler returns, so copy it out synchronously.
|
|
279
|
+
[self handleLibraryVideoAtURL:url error:error assetIdentifier:result.assetIdentifier suggestedName:provider.suggestedName options:options];
|
|
280
|
+
}];
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
BOOL isGIF = [provider hasItemConformingToTypeIdentifier:UTTypeGIF.identifier];
|
|
285
|
+
NSString *typeIdentifier = isGIF ? UTTypeGIF.identifier : UTTypeImage.identifier;
|
|
286
|
+
[provider loadFileRepresentationForTypeIdentifier:typeIdentifier completionHandler:^(NSURL * _Nullable url, NSError * _Nullable error) {
|
|
287
|
+
NSData *data = url ? [NSData dataWithContentsOfURL:url] : nil;
|
|
288
|
+
if (!data) {
|
|
289
|
+
[self sendResponse:@{@"error": error.localizedDescription ?: @"Could not read photo"}];
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
NSString *fileName = [self fileNameFromSuggestedName:provider.suggestedName extension:url.pathExtension];
|
|
293
|
+
[self handleLibraryImageData:data isGIF:isGIF assetIdentifier:result.assetIdentifier fileName:fileName options:options];
|
|
294
|
+
}];
|
|
237
295
|
}
|
|
238
296
|
|
|
239
|
-
- (void)
|
|
297
|
+
- (void)handleLibraryImageData:(NSData *)data
|
|
298
|
+
isGIF:(BOOL)isGIF
|
|
299
|
+
assetIdentifier:(NSString *)assetIdentifier
|
|
300
|
+
fileName:(NSString *)fileName
|
|
301
|
+
options:(NSDictionary *)options
|
|
240
302
|
{
|
|
241
|
-
|
|
303
|
+
UIImage *image = [UIImage imageWithData:data];
|
|
304
|
+
if (!image) {
|
|
305
|
+
[self sendResponse:@{@"error": @"Could not read photo"}];
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
242
308
|
|
|
243
|
-
|
|
244
|
-
|
|
309
|
+
NSDictionary *properties = [ImagePickerManager imagePropertiesFromData:data];
|
|
310
|
+
PHAsset *asset = [ImagePickerManager fetchAssetWithIdentifier:assetIdentifier];
|
|
245
311
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
|
|
261
|
-
fileName = videoURL.lastPathComponent;
|
|
262
|
-
}
|
|
312
|
+
NSString *error = nil;
|
|
313
|
+
NSMutableDictionary *response;
|
|
314
|
+
if (isGIF) {
|
|
315
|
+
// GIFs break when resized, so pass the original bytes through untouched.
|
|
316
|
+
response = [self writeGIFData:data image:image options:options error:&error];
|
|
317
|
+
} else {
|
|
318
|
+
response = [self writeImage:image options:options error:&error];
|
|
319
|
+
}
|
|
320
|
+
if (!response) {
|
|
321
|
+
[self sendResponse:@{@"error": error ?: @"Could not save photo"}];
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
263
324
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// If storage options are provided, we use the documents directory which is persisted
|
|
268
|
-
if ([self.options objectForKey:@"storageOptions"] && [[self.options objectForKey:@"storageOptions"] isKindOfClass:[NSDictionary class]]) {
|
|
269
|
-
NSDictionary *storageOptions = [self.options objectForKey:@"storageOptions"];
|
|
270
|
-
|
|
271
|
-
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
|
272
|
-
NSString *documentsDirectory = [paths objectAtIndex:0];
|
|
273
|
-
path = [documentsDirectory stringByAppendingPathComponent:fileName];
|
|
274
|
-
|
|
275
|
-
// Creates documents subdirectory, if provided
|
|
276
|
-
if ([storageOptions objectForKey:@"path"]) {
|
|
277
|
-
NSString *newPath = [documentsDirectory stringByAppendingPathComponent:[storageOptions objectForKey:@"path"]];
|
|
278
|
-
NSError *error;
|
|
279
|
-
[[NSFileManager defaultManager] createDirectoryAtPath:newPath withIntermediateDirectories:YES attributes:nil error:&error];
|
|
280
|
-
if (error) {
|
|
281
|
-
NSLog(@"Error creating documents subdirectory: %@", error);
|
|
282
|
-
self.callback(@[@{@"error": error.localizedFailureReason}]);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
path = [newPath stringByAppendingPathComponent:fileName];
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
325
|
+
// Location: prefer the file's own EXIF GPS, then fall back to the PHAsset.
|
|
326
|
+
CLLocation *location = [ImagePickerManager locationFromImageProperties:properties] ?: asset.location;
|
|
327
|
+
[ImagePickerManager applyLocation:location toResponse:response];
|
|
290
328
|
|
|
291
|
-
|
|
292
|
-
|
|
329
|
+
NSDate *creationDate = [ImagePickerManager dateFromImageProperties:properties] ?: asset.creationDate;
|
|
330
|
+
if (creationDate) {
|
|
331
|
+
response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:creationDate];
|
|
332
|
+
}
|
|
293
333
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (imageURL) {
|
|
304
|
-
PHAsset *pickedAsset;
|
|
305
|
-
if (@available(iOS 11.0, *)) {
|
|
306
|
-
pickedAsset = [info objectForKey: UIImagePickerControllerPHAsset];
|
|
307
|
-
} else {
|
|
308
|
-
pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[imageURL] options:nil].lastObject;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
NSString *originalFilename = [self originalFilenameForAsset:pickedAsset assetType:PHAssetResourceTypePhoto];
|
|
312
|
-
self.response[@"fileName"] = originalFilename ?: [NSNull null];
|
|
313
|
-
if (pickedAsset.location) {
|
|
314
|
-
self.response[@"latitude"] = @(pickedAsset.location.coordinate.latitude);
|
|
315
|
-
self.response[@"longitude"] = @(pickedAsset.location.coordinate.longitude);
|
|
316
|
-
}
|
|
317
|
-
if (pickedAsset.creationDate) {
|
|
318
|
-
self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:pickedAsset.creationDate];
|
|
319
|
-
}
|
|
320
|
-
}
|
|
334
|
+
// fileName being present signals "picked from the library" (vs. a fresh camera capture) to callers.
|
|
335
|
+
NSString *originalFilename = [self originalFilenameForAsset:asset assetType:PHAssetResourceTypePhoto] ?: fileName;
|
|
336
|
+
response[@"fileName"] = originalFilename ?: [NSNull null];
|
|
337
|
+
if (asset) {
|
|
338
|
+
response[@"origURL"] = [@"ph://" stringByAppendingString:asset.localIdentifier];
|
|
339
|
+
} else if (assetIdentifier) {
|
|
340
|
+
response[@"origURL"] = [@"ph://" stringByAppendingString:assetIdentifier];
|
|
341
|
+
}
|
|
321
342
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init];
|
|
325
|
-
[assetsLibrary assetForURL:imageURL resultBlock:^(ALAsset *asset) {
|
|
326
|
-
ALAssetRepresentation *rep = [asset defaultRepresentation];
|
|
327
|
-
const NSUInteger repSize = (NSUInteger)[rep size];
|
|
328
|
-
Byte *buffer = (Byte*)malloc(repSize);
|
|
329
|
-
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:repSize error:nil];
|
|
330
|
-
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES];
|
|
331
|
-
[data writeToFile:path atomically:YES];
|
|
332
|
-
|
|
333
|
-
NSMutableDictionary *gifResponse = [[NSMutableDictionary alloc] init];
|
|
334
|
-
[gifResponse setObject:@(originalImage.size.width) forKey:@"width"];
|
|
335
|
-
[gifResponse setObject:@(originalImage.size.height) forKey:@"height"];
|
|
336
|
-
|
|
337
|
-
[self populateResponse:gifResponse fromAsset:asset];
|
|
338
|
-
|
|
339
|
-
BOOL vertical = (originalImage.size.width < originalImage.size.height) ? YES : NO;
|
|
340
|
-
[gifResponse setObject:@(vertical) forKey:@"isVertical"];
|
|
341
|
-
|
|
342
|
-
if (![[self.options objectForKey:@"noData"] boolValue]) {
|
|
343
|
-
NSString *dataString = [data base64EncodedStringWithOptions:0];
|
|
344
|
-
[gifResponse setObject:dataString forKey:@"data"];
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
NSURL *fileURL = [NSURL fileURLWithPath:path];
|
|
348
|
-
[gifResponse setObject:[fileURL absoluteString] forKey:@"uri"];
|
|
349
|
-
|
|
350
|
-
NSNumber *fileSizeValue = nil;
|
|
351
|
-
NSError *fileSizeError = nil;
|
|
352
|
-
[fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError];
|
|
353
|
-
if (fileSizeValue){
|
|
354
|
-
[gifResponse setObject:fileSizeValue forKey:@"fileSize"];
|
|
355
|
-
}
|
|
356
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
357
|
-
self.callback(@[gifResponse]);
|
|
358
|
-
});
|
|
359
|
-
} failureBlock:^(NSError *error) {
|
|
360
|
-
dispatch_async(dispatch_get_main_queue(), ^{
|
|
361
|
-
self.callback(@[@{@"error": error.localizedFailureReason}]);
|
|
362
|
-
});
|
|
363
|
-
}];
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
343
|
+
[self sendResponse:response];
|
|
344
|
+
}
|
|
366
345
|
|
|
367
|
-
|
|
346
|
+
- (void)handleLibraryVideoAtURL:(NSURL *)url
|
|
347
|
+
error:(NSError *)loadError
|
|
348
|
+
assetIdentifier:(NSString *)assetIdentifier
|
|
349
|
+
suggestedName:(NSString *)suggestedName
|
|
350
|
+
options:(NSDictionary *)options
|
|
351
|
+
{
|
|
352
|
+
if (!url) {
|
|
353
|
+
[self sendResponse:@{@"error": loadError.localizedDescription ?: @"Could not read video"}];
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
368
356
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
maxHeight = [[self.options valueForKey:@"maxHeight"] floatValue];
|
|
377
|
-
}
|
|
378
|
-
editedImage = [self downscaleImageIfNecessary:editedImage maxWidth:maxWidth maxHeight:maxHeight];
|
|
357
|
+
NSString *error = nil;
|
|
358
|
+
NSString *extension = url.pathExtension.length > 0 ? url.pathExtension : @"mov";
|
|
359
|
+
NSString *path = [self destinationPathWithExtension:extension options:options error:&error];
|
|
360
|
+
if (!path) {
|
|
361
|
+
[self sendResponse:@{@"error": error}];
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
379
364
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
mimeType = @"image/jpeg";
|
|
389
|
-
}
|
|
390
|
-
[self.response setObject:mimeType forKey:@"type"];
|
|
391
|
-
[data writeToFile:path atomically:YES];
|
|
365
|
+
NSURL *destinationURL = [NSURL fileURLWithPath:path];
|
|
366
|
+
NSError *copyError = nil;
|
|
367
|
+
[[NSFileManager defaultManager] removeItemAtURL:destinationURL error:nil];
|
|
368
|
+
if (![[NSFileManager defaultManager] copyItemAtURL:url toURL:destinationURL error:©Error]) {
|
|
369
|
+
[self sendResponse:@{@"error": copyError.localizedDescription ?: @"Could not save video"}];
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
[self applySkipBackupIfNeeded:path options:options];
|
|
392
373
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
[self.response setObject:dataString forKey:@"data"];
|
|
396
|
-
}
|
|
374
|
+
NSMutableDictionary *response = [NSMutableDictionary dictionary];
|
|
375
|
+
response[@"uri"] = destinationURL.absoluteString;
|
|
397
376
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
377
|
+
PHAsset *asset = [ImagePickerManager fetchAssetWithIdentifier:assetIdentifier];
|
|
378
|
+
[ImagePickerManager applyLocation:asset.location toResponse:response];
|
|
379
|
+
if (asset.creationDate) {
|
|
380
|
+
response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:asset.creationDate];
|
|
381
|
+
}
|
|
382
|
+
NSString *originalFilename = [self originalFilenameForAsset:asset assetType:PHAssetResourceTypeVideo]
|
|
383
|
+
?: [self fileNameFromSuggestedName:suggestedName extension:url.pathExtension];
|
|
384
|
+
response[@"fileName"] = originalFilename ?: [NSNull null];
|
|
385
|
+
if (assetIdentifier) {
|
|
386
|
+
response[@"origURL"] = [@"ph://" stringByAppendingString:assetIdentifier];
|
|
387
|
+
}
|
|
403
388
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (origURL) {
|
|
407
|
-
[self.response setObject:origURL forKey:@"origURL"];
|
|
408
|
-
}
|
|
389
|
+
[self sendResponse:response];
|
|
390
|
+
}
|
|
409
391
|
|
|
410
|
-
|
|
411
|
-
NSError *fileSizeError = nil;
|
|
412
|
-
[fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError];
|
|
413
|
-
if (fileSizeValue){
|
|
414
|
-
[self.response setObject:fileSizeValue forKey:@"fileSize"];
|
|
415
|
-
}
|
|
392
|
+
#pragma mark - UIImagePickerControllerDelegate (camera)
|
|
416
393
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// when the image has been saved in the photo album
|
|
429
|
-
if (assetURL) {
|
|
430
|
-
PHAsset *capturedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[assetURL] options:nil].lastObject;
|
|
431
|
-
NSString *originalFilename = [self originalFilenameForAsset:capturedAsset assetType:PHAssetResourceTypePhoto];
|
|
432
|
-
self.response[@"fileName"] = originalFilename ?: [NSNull null];
|
|
433
|
-
// This implementation will never have a location for the captured image, it needs to be added manually with CoreLocation code here.
|
|
434
|
-
if (capturedAsset.creationDate) {
|
|
435
|
-
self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:capturedAsset.creationDate];
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
[self dispatchCallbackForUrl:assetURL withResponse:self.response];
|
|
439
|
-
}
|
|
440
|
-
}];
|
|
441
|
-
} else {
|
|
442
|
-
[library writeImageToSavedPhotosAlbum:originalImage.CGImage metadata:[info valueForKey:UIImagePickerControllerMediaMetadata] completionBlock:nil];
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
else { // VIDEO
|
|
447
|
-
NSURL *videoRefURL = info[UIImagePickerControllerReferenceURL];
|
|
448
|
-
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
|
|
449
|
-
NSURL *videoDestinationURL = [NSURL fileURLWithPath:path];
|
|
450
|
-
|
|
451
|
-
if (videoRefURL) {
|
|
452
|
-
PHAsset *pickedAsset = [PHAsset fetchAssetsWithALAssetURLs:@[videoRefURL] options:nil].lastObject;
|
|
453
|
-
NSString *originalFilename = [self originalFilenameForAsset:pickedAsset assetType:PHAssetResourceTypeVideo];
|
|
454
|
-
self.response[@"fileName"] = originalFilename ?: [NSNull null];
|
|
455
|
-
if (pickedAsset.location) {
|
|
456
|
-
self.response[@"latitude"] = @(pickedAsset.location.coordinate.latitude);
|
|
457
|
-
self.response[@"longitude"] = @(pickedAsset.location.coordinate.longitude);
|
|
458
|
-
}
|
|
459
|
-
if (pickedAsset.creationDate) {
|
|
460
|
-
self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:pickedAsset.creationDate];
|
|
461
|
-
}
|
|
394
|
+
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey, id> *)info
|
|
395
|
+
{
|
|
396
|
+
NSDictionary *options = self.options;
|
|
397
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
398
|
+
// Encoding a full-resolution capture is slow; keep it off the main thread.
|
|
399
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
400
|
+
NSString *mediaType = info[UIImagePickerControllerMediaType];
|
|
401
|
+
if ([mediaType isEqualToString:UTTypeMovie.identifier]) {
|
|
402
|
+
[self handleCapturedVideoWithInfo:info options:options];
|
|
403
|
+
} else {
|
|
404
|
+
[self handleCapturedImageWithInfo:info options:options];
|
|
462
405
|
}
|
|
406
|
+
});
|
|
407
|
+
}];
|
|
408
|
+
}
|
|
463
409
|
|
|
464
|
-
|
|
465
|
-
|
|
410
|
+
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
|
|
411
|
+
{
|
|
412
|
+
[picker dismissViewControllerAnimated:YES completion:^{
|
|
413
|
+
[self sendResponse:@{@"didCancel": @YES}];
|
|
414
|
+
}];
|
|
415
|
+
}
|
|
466
416
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
417
|
+
- (void)handleCapturedImageWithInfo:(NSDictionary<UIImagePickerControllerInfoKey, id> *)info options:(NSDictionary *)options
|
|
418
|
+
{
|
|
419
|
+
UIImage *originalImage = info[UIImagePickerControllerOriginalImage];
|
|
420
|
+
UIImage *image = originalImage;
|
|
421
|
+
if ([options[@"allowsEditing"] boolValue] && info[UIImagePickerControllerEditedImage]) {
|
|
422
|
+
image = info[UIImagePickerControllerEditedImage];
|
|
423
|
+
}
|
|
424
|
+
if (!image) {
|
|
425
|
+
[self sendResponse:@{@"error": @"Could not read photo"}];
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
471
428
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
} else {
|
|
479
|
-
[fileManager copyItemAtURL:videoURL toURL:videoDestinationURL error:&error];
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (error) {
|
|
483
|
-
self.callback(@[@{@"error": error.localizedFailureReason}]);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
429
|
+
NSString *error = nil;
|
|
430
|
+
NSMutableDictionary *response = [self writeImage:image options:options error:&error];
|
|
431
|
+
if (!response) {
|
|
432
|
+
[self sendResponse:@{@"error": error ?: @"Could not save photo"}];
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
488
435
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
436
|
+
// The camera's metadata normally has no GPS (UIImagePickerController doesn't geotag), in which
|
|
437
|
+
// case callers fall back to the device's current location. fileName is intentionally left unset
|
|
438
|
+
// for captures so callers can tell them apart from library picks.
|
|
439
|
+
NSDictionary *metadata = info[UIImagePickerControllerMediaMetadata];
|
|
440
|
+
[ImagePickerManager applyLocation:[ImagePickerManager locationFromImageProperties:metadata] toResponse:response];
|
|
441
|
+
NSDate *captureDate = [ImagePickerManager dateFromImageProperties:metadata];
|
|
442
|
+
if (captureDate) {
|
|
443
|
+
response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:captureDate];
|
|
444
|
+
}
|
|
493
445
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
self.response[@"fileName"] = originalFilename ?: [NSNull null];
|
|
510
|
-
// This implementation will never have a location for the captured image, it needs to be added manually with CoreLocation code here.
|
|
511
|
-
if (capturedAsset.creationDate) {
|
|
512
|
-
self.response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:capturedAsset.creationDate];
|
|
513
|
-
}
|
|
514
|
-
[self dispatchCallbackForUrl:assetURL withResponse:self.response];
|
|
515
|
-
} else {
|
|
516
|
-
[self dispatchCallbackForUrl:[info valueForKey:UIImagePickerControllerReferenceURL] withResponse:self.response];
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}];
|
|
523
|
-
}
|
|
446
|
+
NSDictionary *storageOptions = [ImagePickerManager storageOptionsFrom:options];
|
|
447
|
+
if (![storageOptions[@"cameraRoll"] boolValue]) {
|
|
448
|
+
[self sendResponse:response];
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
NSData *photoData = [ImagePickerManager JPEGDataForImage:originalImage metadata:metadata];
|
|
453
|
+
BOOL waitUntilSaved = [storageOptions[@"waitUntilSaved"] boolValue];
|
|
454
|
+
[self saveToCameraRollPhotoData:photoData videoURL:nil completion:^(PHAsset *asset, NSError *saveError) {
|
|
455
|
+
if (saveError) {
|
|
456
|
+
NSLog(@"ImagePickerManager: error saving photo to camera roll: %@", saveError);
|
|
457
|
+
}
|
|
458
|
+
if (waitUntilSaved) {
|
|
459
|
+
[self applySavedAsset:asset assetType:PHAssetResourceTypePhoto toResponse:response];
|
|
460
|
+
[self sendResponse:response];
|
|
524
461
|
}
|
|
462
|
+
}];
|
|
463
|
+
if (!waitUntilSaved) {
|
|
464
|
+
[self sendResponse:response];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
525
467
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
468
|
+
- (void)handleCapturedVideoWithInfo:(NSDictionary<UIImagePickerControllerInfoKey, id> *)info options:(NSDictionary *)options
|
|
469
|
+
{
|
|
470
|
+
NSURL *videoURL = info[UIImagePickerControllerMediaURL];
|
|
471
|
+
if (!videoURL) {
|
|
472
|
+
[self sendResponse:@{@"error": @"Could not read video"}];
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
529
475
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
476
|
+
NSString *error = nil;
|
|
477
|
+
NSString *extension = videoURL.pathExtension.length > 0 ? videoURL.pathExtension : @"mov";
|
|
478
|
+
NSString *path = [self destinationPathWithExtension:extension options:options error:&error];
|
|
479
|
+
if (!path) {
|
|
480
|
+
[self sendResponse:@{@"error": error}];
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
NSURL *destinationURL = [NSURL fileURLWithPath:path];
|
|
484
|
+
|
|
485
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
486
|
+
[fileManager removeItemAtURL:destinationURL error:nil];
|
|
487
|
+
NSError *moveError = nil;
|
|
488
|
+
BOOL moved;
|
|
489
|
+
if ([fileManager isWritableFileAtPath:videoURL.path]) {
|
|
490
|
+
moved = [fileManager moveItemAtURL:videoURL toURL:destinationURL error:&moveError];
|
|
491
|
+
} else {
|
|
492
|
+
moved = [fileManager copyItemAtURL:videoURL toURL:destinationURL error:&moveError];
|
|
493
|
+
}
|
|
494
|
+
if (!moved) {
|
|
495
|
+
[self sendResponse:@{@"error": moveError.localizedDescription ?: @"Could not save video"}];
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
[self applySkipBackupIfNeeded:path options:options];
|
|
533
499
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
500
|
+
NSMutableDictionary *response = [NSMutableDictionary dictionary];
|
|
501
|
+
response[@"uri"] = destinationURL.absoluteString;
|
|
502
|
+
|
|
503
|
+
NSDictionary *storageOptions = [ImagePickerManager storageOptionsFrom:options];
|
|
504
|
+
if (![storageOptions[@"cameraRoll"] boolValue]) {
|
|
505
|
+
[self sendResponse:response];
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
BOOL waitUntilSaved = [storageOptions[@"waitUntilSaved"] boolValue];
|
|
510
|
+
[self saveToCameraRollPhotoData:nil videoURL:destinationURL completion:^(PHAsset *asset, NSError *saveError) {
|
|
511
|
+
if (saveError) {
|
|
512
|
+
NSLog(@"ImagePickerManager: error saving video to camera roll: %@", saveError);
|
|
540
513
|
}
|
|
541
|
-
|
|
542
|
-
[self
|
|
514
|
+
if (waitUntilSaved) {
|
|
515
|
+
[self applySavedAsset:asset assetType:PHAssetResourceTypeVideo toResponse:response];
|
|
516
|
+
[self sendResponse:response];
|
|
543
517
|
}
|
|
544
|
-
};
|
|
518
|
+
}];
|
|
519
|
+
if (!waitUntilSaved) {
|
|
520
|
+
[self sendResponse:response];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
545
523
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
524
|
+
#pragma mark - Camera roll
|
|
525
|
+
|
|
526
|
+
- (void)saveToCameraRollPhotoData:(NSData *)photoData
|
|
527
|
+
videoURL:(NSURL *)videoURL
|
|
528
|
+
completion:(void (^)(PHAsset *asset, NSError *error))completion
|
|
529
|
+
{
|
|
530
|
+
__block NSString *localIdentifier = nil;
|
|
531
|
+
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
|
|
532
|
+
PHAssetCreationRequest *request = [PHAssetCreationRequest creationRequestForAsset];
|
|
533
|
+
if (photoData) {
|
|
534
|
+
[request addResourceWithType:PHAssetResourceTypePhoto data:photoData options:nil];
|
|
535
|
+
} else {
|
|
536
|
+
[request addResourceWithType:PHAssetResourceTypeVideo fileURL:videoURL options:nil];
|
|
537
|
+
}
|
|
538
|
+
localIdentifier = request.placeholderForCreatedAsset.localIdentifier;
|
|
539
|
+
} completionHandler:^(BOOL success, NSError * _Nullable error) {
|
|
540
|
+
// Fetching back requires read access; with add-only access this is simply nil.
|
|
541
|
+
PHAsset *asset = success ? [ImagePickerManager fetchAssetWithIdentifier:localIdentifier] : nil;
|
|
542
|
+
completion(asset, error);
|
|
543
|
+
}];
|
|
549
544
|
}
|
|
550
545
|
|
|
551
|
-
- (void)
|
|
552
|
-
|
|
553
|
-
if(
|
|
554
|
-
|
|
555
|
-
|
|
546
|
+
- (void)applySavedAsset:(PHAsset *)asset assetType:(PHAssetResourceType)type toResponse:(NSMutableDictionary *)response
|
|
547
|
+
{
|
|
548
|
+
if (!asset) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
NSString *originalFilename = [self originalFilenameForAsset:asset assetType:type];
|
|
552
|
+
response[@"fileName"] = originalFilename ?: [NSNull null];
|
|
553
|
+
if (asset.creationDate) {
|
|
554
|
+
response[@"timestamp"] = [[ImagePickerManager ISO8601DateFormatter] stringFromDate:asset.creationDate];
|
|
556
555
|
}
|
|
556
|
+
response[@"origURL"] = [@"ph://" stringByAppendingString:asset.localIdentifier];
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
559
|
+
#pragma mark - Writing images
|
|
560
|
+
|
|
561
|
+
- (NSMutableDictionary *)writeImage:(UIImage *)image options:(NSDictionary *)options error:(NSString **)error
|
|
562
|
+
{
|
|
563
|
+
BOOL png = [[ImagePickerManager stringValue:options[@"imageFileType"]] isEqualToString:@"png"];
|
|
564
|
+
NSString *path = [self destinationPathWithExtension:(png ? @"png" : @"jpg") options:options error:error];
|
|
565
|
+
if (!path) {
|
|
566
|
+
return nil;
|
|
564
567
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
568
|
+
|
|
569
|
+
UIImage *editedImage = [self fixOrientation:image]; // Rotate the image for upload to web
|
|
570
|
+
|
|
571
|
+
// If needed, downscale image
|
|
572
|
+
CGFloat maxWidth = editedImage.size.width;
|
|
573
|
+
CGFloat maxHeight = editedImage.size.height;
|
|
574
|
+
if ([options[@"maxWidth"] isKindOfClass:[NSNumber class]]) {
|
|
575
|
+
maxWidth = [options[@"maxWidth"] floatValue];
|
|
576
|
+
}
|
|
577
|
+
if ([options[@"maxHeight"] isKindOfClass:[NSNumber class]]) {
|
|
578
|
+
maxHeight = [options[@"maxHeight"] floatValue];
|
|
579
|
+
}
|
|
580
|
+
editedImage = [self downscaleImageIfNecessary:editedImage maxWidth:maxWidth maxHeight:maxHeight];
|
|
581
|
+
|
|
582
|
+
NSData *data;
|
|
583
|
+
NSString *mimeType;
|
|
584
|
+
if (png) {
|
|
585
|
+
data = UIImagePNGRepresentation(editedImage);
|
|
586
|
+
mimeType = @"image/png";
|
|
577
587
|
} else {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
588
|
+
CGFloat quality = [options[@"quality"] isKindOfClass:[NSNumber class]] ? [options[@"quality"] floatValue] : 1.0;
|
|
589
|
+
data = UIImageJPEGRepresentation(editedImage, quality);
|
|
590
|
+
mimeType = @"image/jpeg";
|
|
591
|
+
}
|
|
592
|
+
if (!data || ![data writeToFile:path atomically:YES]) {
|
|
593
|
+
if (error) { *error = @"Could not save photo"; }
|
|
594
|
+
return nil;
|
|
595
|
+
}
|
|
596
|
+
[self applySkipBackupIfNeeded:path options:options];
|
|
597
|
+
|
|
598
|
+
NSMutableDictionary *response = [NSMutableDictionary dictionary];
|
|
599
|
+
response[@"type"] = mimeType;
|
|
600
|
+
if (![options[@"noData"] boolValue]) {
|
|
601
|
+
response[@"data"] = [data base64EncodedStringWithOptions:0];
|
|
581
602
|
}
|
|
603
|
+
response[@"isVertical"] = @(editedImage.size.width < editedImage.size.height);
|
|
604
|
+
response[@"width"] = @(editedImage.size.width);
|
|
605
|
+
response[@"height"] = @(editedImage.size.height);
|
|
606
|
+
[self applyFileURL:[NSURL fileURLWithPath:path] toResponse:response];
|
|
607
|
+
return response;
|
|
582
608
|
}
|
|
583
609
|
|
|
584
|
-
- (
|
|
610
|
+
- (NSMutableDictionary *)writeGIFData:(NSData *)data image:(UIImage *)image options:(NSDictionary *)options error:(NSString **)error
|
|
611
|
+
{
|
|
612
|
+
NSString *path = [self destinationPathWithExtension:@"gif" options:options error:error];
|
|
613
|
+
if (!path) {
|
|
614
|
+
return nil;
|
|
615
|
+
}
|
|
616
|
+
if (![data writeToFile:path atomically:YES]) {
|
|
617
|
+
if (error) { *error = @"Could not save photo"; }
|
|
618
|
+
return nil;
|
|
619
|
+
}
|
|
620
|
+
[self applySkipBackupIfNeeded:path options:options];
|
|
621
|
+
|
|
622
|
+
NSMutableDictionary *response = [NSMutableDictionary dictionary];
|
|
623
|
+
response[@"type"] = @"image/gif";
|
|
624
|
+
if (![options[@"noData"] boolValue]) {
|
|
625
|
+
response[@"data"] = [data base64EncodedStringWithOptions:0];
|
|
626
|
+
}
|
|
627
|
+
response[@"isVertical"] = @(image.size.width < image.size.height);
|
|
628
|
+
response[@"width"] = @(image.size.width);
|
|
629
|
+
response[@"height"] = @(image.size.height);
|
|
630
|
+
[self applyFileURL:[NSURL fileURLWithPath:path] toResponse:response];
|
|
631
|
+
return response;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
- (void)applyFileURL:(NSURL *)fileURL toResponse:(NSMutableDictionary *)response
|
|
635
|
+
{
|
|
636
|
+
response[@"uri"] = fileURL.absoluteString;
|
|
637
|
+
NSNumber *fileSizeValue = nil;
|
|
638
|
+
[fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:nil];
|
|
639
|
+
if (fileSizeValue) {
|
|
640
|
+
response[@"fileSize"] = fileSizeValue;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Temporary directory by default; the (persisted) documents directory when storageOptions are given.
|
|
645
|
+
- (NSString *)destinationPathWithExtension:(NSString *)extension options:(NSDictionary *)options error:(NSString **)error
|
|
646
|
+
{
|
|
647
|
+
NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingPathExtension:extension];
|
|
648
|
+
NSString *directory = [NSTemporaryDirectory() stringByStandardizingPath];
|
|
649
|
+
|
|
650
|
+
NSDictionary *storageOptions = [ImagePickerManager storageOptionsFrom:options];
|
|
651
|
+
if (storageOptions) {
|
|
652
|
+
directory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
|
653
|
+
NSString *subpath = [ImagePickerManager stringValue:storageOptions[@"path"]];
|
|
654
|
+
if (subpath.length > 0) {
|
|
655
|
+
directory = [directory stringByAppendingPathComponent:subpath];
|
|
656
|
+
NSError *createError = nil;
|
|
657
|
+
if (![[NSFileManager defaultManager] createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&createError]) {
|
|
658
|
+
NSLog(@"Error creating documents subdirectory: %@", createError);
|
|
659
|
+
if (error) { *error = createError.localizedDescription ?: @"Could not create directory"; }
|
|
660
|
+
return nil;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return [directory stringByAppendingPathComponent:fileName];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
- (void)applySkipBackupIfNeeded:(NSString *)path options:(NSDictionary *)options
|
|
668
|
+
{
|
|
669
|
+
if ([[ImagePickerManager storageOptionsFrom:options][@"skipBackup"] boolValue]) {
|
|
670
|
+
[self addSkipBackupAttributeToItemAtPath:path]; // Don't back up the file to iCloud
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
#pragma mark - Responses
|
|
675
|
+
|
|
676
|
+
// RCTResponseSenderBlock may only be invoked once; drop the callback as soon as it's used.
|
|
677
|
+
- (void)sendResponse:(NSDictionary *)response
|
|
585
678
|
{
|
|
586
679
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
680
|
+
RCTResponseSenderBlock callback = self.callback;
|
|
681
|
+
self.callback = nil;
|
|
682
|
+
if (callback) {
|
|
683
|
+
callback(@[response]);
|
|
684
|
+
}
|
|
590
685
|
});
|
|
591
686
|
}
|
|
592
687
|
|
|
593
|
-
|
|
688
|
+
- (NSString *)stringOption:(NSString *)key
|
|
689
|
+
{
|
|
690
|
+
return [ImagePickerManager stringValue:self.options[key]];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
- (NSString *)fileNameFromSuggestedName:(NSString *)suggestedName extension:(NSString *)extension
|
|
694
|
+
{
|
|
695
|
+
if (suggestedName.length == 0) {
|
|
696
|
+
return nil;
|
|
697
|
+
}
|
|
698
|
+
if (extension.length == 0 || suggestedName.pathExtension.length > 0) {
|
|
699
|
+
return suggestedName;
|
|
700
|
+
}
|
|
701
|
+
return [suggestedName stringByAppendingPathExtension:extension];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
- (NSString * _Nullable)originalFilenameForAsset:(PHAsset * _Nullable)asset assetType:(PHAssetResourceType)type {
|
|
705
|
+
if (!asset) { return nil; }
|
|
706
|
+
|
|
707
|
+
PHAssetResource *originalResource;
|
|
708
|
+
// Get the underlying resources for the PHAsset (PhotoKit)
|
|
709
|
+
NSArray<PHAssetResource *> *pickedAssetResources = [PHAssetResource assetResourcesForAsset:asset];
|
|
710
|
+
|
|
711
|
+
// Find the original resource (underlying image) for the asset, which has the desired filename
|
|
712
|
+
for (PHAssetResource *resource in pickedAssetResources) {
|
|
713
|
+
if (resource.type == type) {
|
|
714
|
+
originalResource = resource;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return originalResource.originalFilename;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
#pragma mark - Permissions
|
|
594
722
|
|
|
595
723
|
- (void)checkCameraPermissions:(void(^)(BOOL granted))callback
|
|
596
724
|
{
|
|
597
725
|
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
|
598
726
|
if (status == AVAuthorizationStatusAuthorized) {
|
|
599
727
|
callback(YES);
|
|
600
|
-
|
|
601
|
-
} else if (status == AVAuthorizationStatusNotDetermined){
|
|
728
|
+
} else if (status == AVAuthorizationStatusNotDetermined) {
|
|
602
729
|
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
|
603
730
|
callback(granted);
|
|
604
|
-
return;
|
|
605
731
|
}];
|
|
606
732
|
} else {
|
|
607
733
|
callback(NO);
|
|
608
734
|
}
|
|
609
735
|
}
|
|
610
736
|
|
|
611
|
-
-
|
|
612
|
-
{
|
|
613
|
-
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
|
|
614
|
-
if (status == PHAuthorizationStatusAuthorized) {
|
|
615
|
-
callback(YES);
|
|
616
|
-
return;
|
|
617
|
-
} else if (status == PHAuthorizationStatusNotDetermined) {
|
|
618
|
-
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
|
|
619
|
-
if (status == PHAuthorizationStatusAuthorized) {
|
|
620
|
-
callback(YES);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
callback(NO);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
}];
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
callback(NO);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
737
|
+
#pragma mark - Image helpers
|
|
633
738
|
|
|
634
|
-
- (UIImage*)downscaleImageIfNecessary:(UIImage*)image maxWidth:(
|
|
739
|
+
- (UIImage*)downscaleImageIfNecessary:(UIImage*)image maxWidth:(CGFloat)maxWidth maxHeight:(CGFloat)maxHeight
|
|
635
740
|
{
|
|
636
|
-
UIImage* newImage = image;
|
|
637
|
-
|
|
638
741
|
// Nothing to do here
|
|
639
742
|
if (image.size.width <= maxWidth && image.size.height <= maxHeight) {
|
|
640
|
-
return
|
|
743
|
+
return image;
|
|
641
744
|
}
|
|
642
745
|
|
|
643
746
|
CGSize scaledSize = CGSizeMake(image.size.width, image.size.height);
|
|
@@ -648,18 +751,21 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
648
751
|
scaledSize = CGSizeMake((maxHeight / scaledSize.height) * scaledSize.width, maxHeight);
|
|
649
752
|
}
|
|
650
753
|
|
|
651
|
-
// If the pixels are floats, it causes a white line
|
|
754
|
+
// If the pixels are floats, it causes a white line
|
|
652
755
|
scaledSize.width = (int)scaledSize.width;
|
|
653
756
|
scaledSize.height = (int)scaledSize.height;
|
|
654
757
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
758
|
+
// fixOrientation produces a scale-1 image, so points == pixels; render at scale 1 to match.
|
|
759
|
+
UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat];
|
|
760
|
+
format.scale = 1;
|
|
761
|
+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:scaledSize format:format];
|
|
762
|
+
UIImage *newImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull context) {
|
|
763
|
+
[image drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
|
|
764
|
+
}];
|
|
658
765
|
if (newImage == nil) {
|
|
659
766
|
NSLog(@"could not scale image");
|
|
767
|
+
return image;
|
|
660
768
|
}
|
|
661
|
-
UIGraphicsEndImageContext();
|
|
662
|
-
|
|
663
769
|
return newImage;
|
|
664
770
|
}
|
|
665
771
|
|
|
@@ -712,6 +818,9 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
712
818
|
}
|
|
713
819
|
|
|
714
820
|
CGContextRef ctx = CGBitmapContextCreate(NULL, srcImg.size.width, srcImg.size.height, CGImageGetBitsPerComponent(srcImg.CGImage), 0, CGImageGetColorSpace(srcImg.CGImage), CGImageGetBitmapInfo(srcImg.CGImage));
|
|
821
|
+
if (!ctx) {
|
|
822
|
+
return srcImg;
|
|
823
|
+
}
|
|
715
824
|
CGContextConcatCTM(ctx, transform);
|
|
716
825
|
switch (srcImg.imageOrientation) {
|
|
717
826
|
case UIImageOrientationLeft:
|
|
@@ -754,6 +863,113 @@ RCT_EXPORT_METHOD(showImagePicker:(NSDictionary *)options callback:(RCTResponseS
|
|
|
754
863
|
|
|
755
864
|
#pragma mark - Class Methods
|
|
756
865
|
|
|
866
|
+
+ (NSString *)stringValue:(id)value
|
|
867
|
+
{
|
|
868
|
+
return [value isKindOfClass:[NSString class]] ? value : nil;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
+ (NSDictionary *)storageOptionsFrom:(NSDictionary *)options
|
|
872
|
+
{
|
|
873
|
+
id storageOptions = options[@"storageOptions"];
|
|
874
|
+
return [storageOptions isKindOfClass:[NSDictionary class]] ? storageOptions : nil;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Only touches PhotoKit when the app already has read access, so this never triggers a permission prompt.
|
|
878
|
+
+ (PHAsset *)fetchAssetWithIdentifier:(NSString *)identifier
|
|
879
|
+
{
|
|
880
|
+
if (identifier.length == 0) {
|
|
881
|
+
return nil;
|
|
882
|
+
}
|
|
883
|
+
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatusForAccessLevel:PHAccessLevelReadWrite];
|
|
884
|
+
if (status != PHAuthorizationStatusAuthorized && status != PHAuthorizationStatusLimited) {
|
|
885
|
+
return nil;
|
|
886
|
+
}
|
|
887
|
+
return [PHAsset fetchAssetsWithLocalIdentifiers:@[identifier] options:nil].firstObject;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
+ (NSDictionary *)imagePropertiesFromData:(NSData *)data
|
|
891
|
+
{
|
|
892
|
+
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
|
|
893
|
+
if (!source) {
|
|
894
|
+
return nil;
|
|
895
|
+
}
|
|
896
|
+
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
|
|
897
|
+
CFRelease(source);
|
|
898
|
+
return properties;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
+ (CLLocation *)locationFromImageProperties:(NSDictionary *)properties
|
|
902
|
+
{
|
|
903
|
+
NSDictionary *gps = properties[(NSString *)kCGImagePropertyGPSDictionary];
|
|
904
|
+
if (![gps isKindOfClass:[NSDictionary class]]) {
|
|
905
|
+
return nil;
|
|
906
|
+
}
|
|
907
|
+
NSNumber *latitude = gps[(NSString *)kCGImagePropertyGPSLatitude];
|
|
908
|
+
NSNumber *longitude = gps[(NSString *)kCGImagePropertyGPSLongitude];
|
|
909
|
+
if (![latitude isKindOfClass:[NSNumber class]] || ![longitude isKindOfClass:[NSNumber class]]) {
|
|
910
|
+
return nil;
|
|
911
|
+
}
|
|
912
|
+
double lat = latitude.doubleValue;
|
|
913
|
+
double lon = longitude.doubleValue;
|
|
914
|
+
if ([gps[(NSString *)kCGImagePropertyGPSLatitudeRef] isEqual:@"S"]) { lat = -lat; }
|
|
915
|
+
if ([gps[(NSString *)kCGImagePropertyGPSLongitudeRef] isEqual:@"W"]) { lon = -lon; }
|
|
916
|
+
if (lat == 0 && lon == 0) {
|
|
917
|
+
return nil;
|
|
918
|
+
}
|
|
919
|
+
return [[CLLocation alloc] initWithLatitude:lat longitude:lon];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
+ (void)applyLocation:(CLLocation *)location toResponse:(NSMutableDictionary *)response
|
|
923
|
+
{
|
|
924
|
+
if (!location) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
response[@"latitude"] = @(location.coordinate.latitude);
|
|
928
|
+
response[@"longitude"] = @(location.coordinate.longitude);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
+ (NSDate *)dateFromImageProperties:(NSDictionary *)properties
|
|
932
|
+
{
|
|
933
|
+
NSDictionary *exif = properties[(NSString *)kCGImagePropertyExifDictionary];
|
|
934
|
+
if (![exif isKindOfClass:[NSDictionary class]]) {
|
|
935
|
+
return nil;
|
|
936
|
+
}
|
|
937
|
+
NSString *dateString = [self stringValue:exif[(NSString *)kCGImagePropertyExifDateTimeOriginal]];
|
|
938
|
+
if (dateString.length == 0) {
|
|
939
|
+
return nil;
|
|
940
|
+
}
|
|
941
|
+
NSString *offset = [self stringValue:exif[(NSString *)kCGImagePropertyExifOffsetTimeOriginal]];
|
|
942
|
+
|
|
943
|
+
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
|
944
|
+
formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
|
945
|
+
if (offset.length > 0) {
|
|
946
|
+
formatter.dateFormat = @"yyyy:MM:dd HH:mm:ssxxx";
|
|
947
|
+
return [formatter dateFromString:[dateString stringByAppendingString:offset]];
|
|
948
|
+
}
|
|
949
|
+
// No offset recorded: EXIF times are local to where the photo was taken; assume the device's zone.
|
|
950
|
+
formatter.dateFormat = @"yyyy:MM:dd HH:mm:ss";
|
|
951
|
+
formatter.timeZone = [NSTimeZone localTimeZone];
|
|
952
|
+
return [formatter dateFromString:dateString];
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
+ (NSData *)JPEGDataForImage:(UIImage *)image metadata:(NSDictionary *)metadata
|
|
956
|
+
{
|
|
957
|
+
if (!image.CGImage) {
|
|
958
|
+
return UIImageJPEGRepresentation(image, 0.9);
|
|
959
|
+
}
|
|
960
|
+
NSMutableData *data = [NSMutableData data];
|
|
961
|
+
CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)data, (__bridge CFStringRef)UTTypeJPEG.identifier, 1, NULL);
|
|
962
|
+
if (!destination) {
|
|
963
|
+
return UIImageJPEGRepresentation(image, 0.9);
|
|
964
|
+
}
|
|
965
|
+
NSMutableDictionary *properties = [metadata mutableCopy] ?: [NSMutableDictionary dictionary];
|
|
966
|
+
properties[(NSString *)kCGImageDestinationLossyCompressionQuality] = @0.9;
|
|
967
|
+
CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef)properties);
|
|
968
|
+
BOOL ok = CGImageDestinationFinalize(destination);
|
|
969
|
+
CFRelease(destination);
|
|
970
|
+
return ok ? data : UIImageJPEGRepresentation(image, 0.9);
|
|
971
|
+
}
|
|
972
|
+
|
|
757
973
|
+ (NSDateFormatter * _Nonnull)ISO8601DateFormatter {
|
|
758
974
|
static NSDateFormatter *ISO8601DateFormatter;
|
|
759
975
|
static dispatch_once_t onceToken;
|