mirror of
https://github.com/status-im/react-native-image-crop-picker.git
synced 2025-02-23 02:48:12 +00:00
extract exif data from image
This commit is contained in:
parent
aa54cdc3e0
commit
d3da5de487
@ -49,7 +49,7 @@ ImagePicker.openPicker({
|
||||
});
|
||||
```
|
||||
|
||||
**Android: The prop 'cropping' has been known to cause videos not to be display in the gallery on Android. Please do not set cropping to true when selecting videos.**
|
||||
**Android: The prop 'cropping' has been known to cause videos not to be display in the gallery on Android. Please do not set cropping to true when selecting videos.**
|
||||
|
||||
|
||||
### Select from camera
|
||||
@ -94,6 +94,7 @@ ImagePicker.clean().then(() => {
|
||||
| height | number | Height of result image when used with `cropping` option |
|
||||
| multiple | bool (default false) | Enable or disable multiple image selection |
|
||||
| includeBase64 | bool (default false) | Enable or disable returning base64 data with image |
|
||||
| includeExif | bool (default false) | Include image exif data in the response |
|
||||
| cropperActiveWidgetColor (android only) | string (default `"#424242"`) | When cropping image, determines ActiveWidget color. |
|
||||
| cropperStatusBarColor (android only) | string (default `#424242`) | When cropping image, determines the color of StatusBar. |
|
||||
| cropperToolbarColor (android only) | string (default `#424242`) | When cropping image, determines the color of Toolbar. |
|
||||
@ -126,6 +127,7 @@ ImagePicker.clean().then(() => {
|
||||
| mime | string | Selected image MIME type (image/jpeg, image/png) |
|
||||
| size | number | Selected image size in bytes |
|
||||
| data | base64 | Optional base64 selected file representation |
|
||||
| exif | object | Extracted exif data from image. Response format is platform specific |
|
||||
|
||||
# Install
|
||||
|
||||
|
@ -0,0 +1,71 @@
|
||||
package com.reactnative.ivpusic.imagepicker;
|
||||
|
||||
import android.media.ExifInterface;
|
||||
import android.os.Build;
|
||||
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static android.media.ExifInterface.*;
|
||||
|
||||
public class ExifExtractor {
|
||||
|
||||
public static WritableMap extract(String path) throws IOException {
|
||||
WritableMap exifData = new WritableNativeMap();
|
||||
|
||||
List<String> attributes = getBasicAttributes();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
attributes.addAll(getLevel23Attributes());
|
||||
}
|
||||
|
||||
ExifInterface exif = new ExifInterface(path);
|
||||
|
||||
for (String attribute : attributes) {
|
||||
String value = exif.getAttribute(attribute);
|
||||
exifData.putString(attribute, value);
|
||||
}
|
||||
|
||||
return exifData;
|
||||
}
|
||||
|
||||
private static List<String> getBasicAttributes() {
|
||||
return new ArrayList<>(Arrays.asList(
|
||||
TAG_APERTURE,
|
||||
TAG_DATETIME,
|
||||
TAG_EXPOSURE_TIME,
|
||||
TAG_FLASH,
|
||||
TAG_FOCAL_LENGTH,
|
||||
TAG_GPS_ALTITUDE,
|
||||
TAG_GPS_ALTITUDE_REF,
|
||||
TAG_GPS_DATESTAMP,
|
||||
TAG_GPS_LATITUDE,
|
||||
TAG_GPS_LATITUDE_REF,
|
||||
TAG_GPS_LONGITUDE,
|
||||
TAG_GPS_LONGITUDE_REF,
|
||||
TAG_GPS_PROCESSING_METHOD,
|
||||
TAG_GPS_TIMESTAMP,
|
||||
TAG_IMAGE_LENGTH,
|
||||
TAG_IMAGE_WIDTH,
|
||||
TAG_ISO,
|
||||
TAG_MAKE,
|
||||
TAG_MODEL,
|
||||
TAG_ORIENTATION,
|
||||
TAG_WHITE_BALANCE
|
||||
));
|
||||
}
|
||||
|
||||
private static List<String> getLevel23Attributes() {
|
||||
return new ArrayList<>(Arrays.asList(
|
||||
TAG_DATETIME_DIGITIZED,
|
||||
TAG_SUBSEC_TIME,
|
||||
TAG_SUBSEC_TIME_DIG,
|
||||
TAG_SUBSEC_TIME_ORIG
|
||||
));
|
||||
}
|
||||
}
|
@ -67,6 +67,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
|
||||
private String mediaType = "any";
|
||||
private boolean multiple = false;
|
||||
private boolean includeBase64 = false;
|
||||
private boolean includeExif = false;
|
||||
private boolean cropping = false;
|
||||
private boolean cropperCircleOverlay = false;
|
||||
private boolean showCropGuidelines = true;
|
||||
@ -112,6 +113,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
|
||||
mediaType = options.hasKey("mediaType") ? options.getString("mediaType") : mediaType;
|
||||
multiple = options.hasKey("multiple") && options.getBoolean("multiple");
|
||||
includeBase64 = options.hasKey("includeBase64") && options.getBoolean("includeBase64");
|
||||
includeExif = options.hasKey("includeExif") && options.getBoolean("includeExif");
|
||||
width = options.hasKey("width") ? options.getInt("width") : width;
|
||||
height = options.hasKey("height") ? options.getInt("height") : height;
|
||||
cropping = options.hasKey("cropping") ? options.getBoolean("cropping") : cropping;
|
||||
@ -539,6 +541,15 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
|
||||
image.putString("data", getBase64StringFromFile(compressedImagePath));
|
||||
}
|
||||
|
||||
if (includeExif) {
|
||||
try {
|
||||
WritableMap exif = ExifExtractor.extract(path);
|
||||
image.putMap("exif", exif);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ export default class App extends Component {
|
||||
cropping: cropping,
|
||||
width: 500,
|
||||
height: 500,
|
||||
includeExif: true,
|
||||
}).then(image => {
|
||||
console.log('received image', image);
|
||||
this.setState({
|
||||
@ -54,7 +55,8 @@ export default class App extends Component {
|
||||
width: 300,
|
||||
height: 300,
|
||||
cropping: cropit,
|
||||
includeBase64: true
|
||||
includeBase64: true,
|
||||
includeExif: true,
|
||||
}).then(image => {
|
||||
console.log('received base64 image');
|
||||
this.setState({
|
||||
@ -114,6 +116,7 @@ export default class App extends Component {
|
||||
compressImageMaxHeight: 480,
|
||||
compressImageQuality: 0.5,
|
||||
compressVideoPreset: 'MediumQuality',
|
||||
includeExif: true,
|
||||
}).then(image => {
|
||||
console.log('received image', image);
|
||||
this.setState({
|
||||
@ -129,7 +132,8 @@ export default class App extends Component {
|
||||
pickMultiple() {
|
||||
ImagePicker.openPicker({
|
||||
multiple: true,
|
||||
waitAnimationEnd: false
|
||||
waitAnimationEnd: false,
|
||||
includeExif: true,
|
||||
}).then(images => {
|
||||
this.setState({
|
||||
image: null,
|
||||
|
@ -472,6 +472,14 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3468219D1F7BAED90093F7F5 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5B590311A8214ACA88447B83 /* libRCTVideo.a */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
34A9DDB71D7F43220012B1F5 /* ImageCropPickerSDK */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -485,7 +493,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
34F74E351E0733E3001D9901 /* libRCTAnimation.a */,
|
||||
34F74E371E0733E3001D9901 /* libRCTAnimation-tvOS.a */,
|
||||
34F74E371E0733E3001D9901 /* libRCTAnimation.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -536,6 +544,7 @@
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
00E356EF1AD99517003FC87E /* exampleTests */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
3468219D1F7BAED90093F7F5 /* Recovered References */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
@ -836,7 +845,7 @@
|
||||
remoteRef = 34F74E341E0733E3001D9901 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
34F74E371E0733E3001D9901 /* libRCTAnimation-tvOS.a */ = {
|
||||
34F74E371E0733E3001D9901 /* libRCTAnimation.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRCTAnimation.a;
|
||||
|
@ -54,6 +54,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
@ -83,6 +84,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
9
example/jsconfig.json
Normal file
9
example/jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
ir{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
5522
example/package-lock.json
generated
Normal file
5522
example/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "16.0.0-alpha.12",
|
||||
"react-native": "0.47.0",
|
||||
"react-native": "0.48.4",
|
||||
"react-native-image-crop-picker": "../",
|
||||
"react-native-video": "git://github.com/react-native-community/react-native-video.git"
|
||||
}
|
||||
|
4994
example/yarn.lock
4994
example/yarn.lock
File diff suppressed because it is too large
Load Diff
2
index.d.ts
vendored
2
index.d.ts
vendored
@ -6,6 +6,7 @@ declare module "react-native-image-crop-picker" {
|
||||
multiple?: boolean;
|
||||
path?: string;
|
||||
includeBase64?: boolean;
|
||||
includeExif?: boolean;
|
||||
cropperTintColor?: string;
|
||||
cropperCircleOverlay?: boolean;
|
||||
maxFiles?: number;
|
||||
@ -31,6 +32,7 @@ declare module "react-native-image-crop-picker" {
|
||||
width: number;
|
||||
height: number;
|
||||
mime: string;
|
||||
exif: null | object;
|
||||
}
|
||||
|
||||
export function openPicker(options: Options): Promise<Image | Image[]>;
|
||||
|
@ -31,10 +31,18 @@
|
||||
#import <math.h>
|
||||
|
||||
@interface ImageCropPicker : NSObject<
|
||||
RCTBridgeModule,
|
||||
QBImagePickerControllerDelegate,
|
||||
RSKImageCropViewControllerDelegate,
|
||||
RSKImageCropViewControllerDataSource>
|
||||
UIImagePickerControllerDelegate,
|
||||
UINavigationControllerDelegate,
|
||||
RCTBridgeModule,
|
||||
QBImagePickerControllerDelegate,
|
||||
RSKImageCropViewControllerDelegate,
|
||||
RSKImageCropViewControllerDataSource>
|
||||
|
||||
typedef enum selectionMode {
|
||||
CAMERA,
|
||||
CROPPING,
|
||||
PICKER
|
||||
} SelectionMode;
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary *croppingFile;
|
||||
@property (nonatomic, strong) NSDictionary *defaultOptions;
|
||||
@ -42,7 +50,7 @@
|
||||
@property (nonatomic, retain) NSMutableDictionary *options;
|
||||
@property (nonatomic, strong) RCTPromiseResolveBlock resolve;
|
||||
@property (nonatomic, strong) RCTPromiseRejectBlock reject;
|
||||
@property BOOL cropOnly;
|
||||
@property SelectionMode currentSelectionMode;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -51,6 +51,7 @@ RCT_EXPORT_MODULE();
|
||||
@"cropping": @NO,
|
||||
@"cropperCircleOverlay": @NO,
|
||||
@"includeBase64": @NO,
|
||||
@"includeExif": @NO,
|
||||
@"compressVideo": @YES,
|
||||
@"minFiles": @1,
|
||||
@"maxFiles": @5,
|
||||
@ -66,7 +67,7 @@ RCT_EXPORT_MODULE();
|
||||
};
|
||||
self.compression = [[Compression alloc] init];
|
||||
}
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@ -74,11 +75,11 @@ RCT_EXPORT_MODULE();
|
||||
if ([[self.options objectForKey:@"waitAnimationEnd"] boolValue]) {
|
||||
return completion;
|
||||
}
|
||||
|
||||
|
||||
if (completion != nil) {
|
||||
completion();
|
||||
}
|
||||
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@ -101,7 +102,7 @@ RCT_EXPORT_MODULE();
|
||||
- (void) setConfiguration:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject {
|
||||
|
||||
|
||||
self.resolve = resolve;
|
||||
self.reject = reject;
|
||||
self.options = [NSMutableDictionary dictionaryWithDictionary:self.defaultOptions];
|
||||
@ -115,17 +116,17 @@ RCT_EXPORT_MODULE();
|
||||
while (root.presentedViewController != nil) {
|
||||
root = root.presentedViewController;
|
||||
}
|
||||
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(openCamera:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
|
||||
|
||||
[self setConfiguration:options resolver:resolve rejecter:reject];
|
||||
self.cropOnly = NO;
|
||||
|
||||
self.currentSelectionMode = CAMERA;
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
self.reject(ERROR_PICKER_CANNOT_RUN_CAMERA_ON_SIMULATOR_KEY, ERROR_PICKER_CANNOT_RUN_CAMERA_ON_SIMULATOR_MSG, nil);
|
||||
return;
|
||||
@ -135,7 +136,7 @@ RCT_EXPORT_METHOD(openCamera:(NSDictionary *)options
|
||||
self.reject(ERROR_PICKER_NO_CAMERA_PERMISSION_KEY, ERROR_PICKER_NO_CAMERA_PERMISSION_MSG, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
||||
picker.delegate = self;
|
||||
picker.allowsEditing = NO;
|
||||
@ -143,7 +144,7 @@ RCT_EXPORT_METHOD(openCamera:(NSDictionary *)options
|
||||
if ([[self.options objectForKey:@"useFrontCamera"] boolValue]) {
|
||||
picker.cameraDevice = UIImagePickerControllerCameraDeviceFront;
|
||||
}
|
||||
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[self getRootVC] presentViewController:picker animated:YES completion:nil];
|
||||
});
|
||||
@ -151,11 +152,20 @@ RCT_EXPORT_METHOD(openCamera:(NSDictionary *)options
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[self viewDidLoad];
|
||||
}
|
||||
|
||||
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
|
||||
UIImage *chosenImage = [info objectForKey:UIImagePickerControllerOriginalImage];
|
||||
UIImage *chosenImageT = [chosenImage fixOrientation];
|
||||
|
||||
[self processSingleImagePick:chosenImageT withViewController:picker withSourceURL:self.croppingFile[@"sourceURL"] withLocalIdentifier:self.croppingFile[@"localIdentifier"] withFilename:self.croppingFile[@"filename"]];
|
||||
NSDictionary *exif;
|
||||
if([[self.options objectForKey:@"includeExif"] boolValue]) {
|
||||
exif = [info objectForKey:UIImagePickerControllerMediaMetadata];
|
||||
}
|
||||
|
||||
[self processSingleImagePick:chosenImageT withExif:exif withViewController:picker withSourceURL:self.croppingFile[@"sourceURL"] withLocalIdentifier:self.croppingFile[@"localIdentifier"] withFilename:self.croppingFile[@"filename"]];
|
||||
}
|
||||
|
||||
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
|
||||
@ -167,38 +177,38 @@ RCT_EXPORT_METHOD(openCamera:(NSDictionary *)options
|
||||
- (NSString*) getTmpDirectory {
|
||||
NSString *TMP_DIRECTORY = @"react-native-image-crop-picker/";
|
||||
NSString *tmpFullPath = [NSTemporaryDirectory() stringByAppendingString:TMP_DIRECTORY];
|
||||
|
||||
|
||||
BOOL isDir;
|
||||
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:tmpFullPath isDirectory:&isDir];
|
||||
if (!exists) {
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath: tmpFullPath
|
||||
withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
}
|
||||
|
||||
|
||||
return tmpFullPath;
|
||||
}
|
||||
|
||||
- (BOOL)cleanTmpDirectory {
|
||||
NSString* tmpDirectoryPath = [self getTmpDirectory];
|
||||
NSArray* tmpDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectoryPath error:NULL];
|
||||
|
||||
|
||||
for (NSString *file in tmpDirectory) {
|
||||
BOOL deleted = [[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@%@", tmpDirectoryPath, file] error:NULL];
|
||||
|
||||
|
||||
if (!deleted) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(cleanSingle:(NSString *) path
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
|
||||
|
||||
BOOL deleted = [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
|
||||
|
||||
|
||||
if (!deleted) {
|
||||
reject(ERROR_CLEANUP_ERROR_KEY, ERROR_CLEANUP_ERROR_MSG, nil);
|
||||
} else {
|
||||
@ -218,16 +228,16 @@ RCT_REMAP_METHOD(clean, resolver:(RCTPromiseResolveBlock)resolve
|
||||
RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
|
||||
|
||||
[self setConfiguration:options resolver:resolve rejecter:reject];
|
||||
self.cropOnly = NO;
|
||||
|
||||
self.currentSelectionMode = PICKER;
|
||||
|
||||
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
|
||||
if (status != PHAuthorizationStatusAuthorized) {
|
||||
self.reject(ERROR_PICKER_UNAUTHORIZED_KEY, ERROR_PICKER_UNAUTHORIZED_MSG, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// init picker
|
||||
QBImagePickerController *imagePickerController =
|
||||
@ -237,15 +247,15 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options
|
||||
imagePickerController.minimumNumberOfSelection = abs([[self.options objectForKey:@"minFiles"] intValue]);
|
||||
imagePickerController.maximumNumberOfSelection = abs([[self.options objectForKey:@"maxFiles"] intValue]);
|
||||
imagePickerController.showsNumberOfSelectedAssets = [[self.options objectForKey:@"showsSelectedCount"] boolValue];
|
||||
|
||||
|
||||
if ([self.options objectForKey:@"smartAlbums"] != nil) {
|
||||
NSDictionary *smartAlbums = @{
|
||||
@"UserLibrary" : @(PHAssetCollectionSubtypeSmartAlbumUserLibrary),
|
||||
@"PhotoStream" : @(PHAssetCollectionSubtypeAlbumMyPhotoStream),
|
||||
@"Panoramas" : @(PHAssetCollectionSubtypeSmartAlbumPanoramas),
|
||||
@"Videos" : @(PHAssetCollectionSubtypeSmartAlbumVideos),
|
||||
@"Bursts" : @(PHAssetCollectionSubtypeSmartAlbumBursts),
|
||||
};
|
||||
@"UserLibrary" : @(PHAssetCollectionSubtypeSmartAlbumUserLibrary),
|
||||
@"PhotoStream" : @(PHAssetCollectionSubtypeAlbumMyPhotoStream),
|
||||
@"Panoramas" : @(PHAssetCollectionSubtypeSmartAlbumPanoramas),
|
||||
@"Videos" : @(PHAssetCollectionSubtypeSmartAlbumVideos),
|
||||
@"Bursts" : @(PHAssetCollectionSubtypeSmartAlbumBursts),
|
||||
};
|
||||
NSMutableArray *albumsToShow = [NSMutableArray arrayWithCapacity:5];
|
||||
for (NSString* album in [self.options objectForKey:@"smartAlbums"]) {
|
||||
if ([smartAlbums objectForKey:album] != nil) {
|
||||
@ -254,12 +264,12 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options
|
||||
}
|
||||
imagePickerController.assetCollectionSubtypes = albumsToShow;
|
||||
}
|
||||
|
||||
|
||||
if ([[self.options objectForKey:@"cropping"] boolValue]) {
|
||||
imagePickerController.mediaType = QBImagePickerMediaTypeImage;
|
||||
} else {
|
||||
NSString *mediaType = [self.options objectForKey:@"mediaType"];
|
||||
|
||||
|
||||
if ([mediaType isEqualToString:@"any"]) {
|
||||
imagePickerController.mediaType = QBImagePickerMediaTypeAny;
|
||||
} else if ([mediaType isEqualToString:@"photo"]) {
|
||||
@ -267,9 +277,9 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options
|
||||
} else {
|
||||
imagePickerController.mediaType = QBImagePickerMediaTypeVideo;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
[[self getRootVC] presentViewController:imagePickerController animated:YES completion:nil];
|
||||
});
|
||||
}];
|
||||
@ -278,12 +288,12 @@ RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options
|
||||
RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
|
||||
|
||||
[self setConfiguration:options resolver:resolve rejecter:reject];
|
||||
self.cropOnly = YES;
|
||||
|
||||
self.currentSelectionMode = CROPPING;
|
||||
|
||||
NSString *path = [options objectForKey:@"path"];
|
||||
|
||||
|
||||
[self.bridge.imageLoader loadImageWithURLRequest:[RCTConvert NSURLRequest:path] callback:^(NSError *error, UIImage *image) {
|
||||
if (error) {
|
||||
self.reject(ERROR_CROPPER_IMAGE_NOT_FOUND_KEY, ERROR_CROPPER_IMAGE_NOT_FOUND_MSG, nil);
|
||||
@ -305,7 +315,7 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
imageCropVC.delegate = self;
|
||||
[imageCropVC setModalPresentationStyle:UIModalPresentationCustom];
|
||||
[imageCropVC setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
|
||||
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[self getRootVC] presentViewController:imageCropVC animated:YES completion:nil];
|
||||
});
|
||||
@ -314,18 +324,18 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
- (void)showActivityIndicator:(void (^)(UIActivityIndicatorView*, UIView*))handler {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
UIView *mainView = [[self getRootVC] view];
|
||||
|
||||
|
||||
// create overlay
|
||||
UIView *loadingView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
loadingView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.5];
|
||||
loadingView.clipsToBounds = YES;
|
||||
|
||||
|
||||
// create loading spinner
|
||||
UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
activityView.frame = CGRectMake(65, 40, activityView.bounds.size.width, activityView.bounds.size.height);
|
||||
activityView.center = loadingView.center;
|
||||
[loadingView addSubview:activityView];
|
||||
|
||||
|
||||
// create message
|
||||
UILabel *loadingLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 115, 130, 22)];
|
||||
loadingLabel.backgroundColor = [UIColor clearColor];
|
||||
@ -334,15 +344,15 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
CGPoint loadingLabelLocation = loadingView.center;
|
||||
loadingLabelLocation.y += [activityView bounds].size.height;
|
||||
loadingLabel.center = loadingLabelLocation;
|
||||
loadingLabel.textAlignment = UITextAlignmentCenter;
|
||||
loadingLabel.textAlignment = NSTextAlignmentCenter;
|
||||
loadingLabel.text = [self.options objectForKey:@"loadingLabelText"];
|
||||
[loadingLabel setFont:[UIFont boldSystemFontOfSize:18]];
|
||||
[loadingView addSubview:loadingLabel];
|
||||
|
||||
|
||||
// show all
|
||||
[mainView addSubview:loadingView];
|
||||
[activityView startAnimating];
|
||||
|
||||
|
||||
handler(activityView, loadingView);
|
||||
});
|
||||
}
|
||||
@ -352,39 +362,40 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
PHImageManager *manager = [PHImageManager defaultManager];
|
||||
PHVideoRequestOptions *options = [[PHVideoRequestOptions alloc] init];
|
||||
options.version = PHVideoRequestOptionsVersionOriginal;
|
||||
|
||||
|
||||
[manager
|
||||
requestAVAssetForVideo:forAsset
|
||||
options:options
|
||||
resultHandler:^(AVAsset * asset, AVAudioMix * audioMix,
|
||||
NSDictionary *info) {
|
||||
NSURL *sourceURL = [(AVURLAsset *)asset URL];
|
||||
|
||||
|
||||
// create temp file
|
||||
NSString *tmpDirFullPath = [self getTmpDirectory];
|
||||
NSString *filePath = [tmpDirFullPath stringByAppendingString:[[NSUUID UUID] UUIDString]];
|
||||
filePath = [filePath stringByAppendingString:@".mp4"];
|
||||
NSURL *outputURL = [NSURL fileURLWithPath:filePath];
|
||||
|
||||
|
||||
[self.compression compressVideo:sourceURL outputURL:outputURL withOptions:self.options handler:^(AVAssetExportSession *exportSession) {
|
||||
if (exportSession.status == AVAssetExportSessionStatusCompleted) {
|
||||
AVAsset *compressedAsset = [AVAsset assetWithURL:outputURL];
|
||||
AVAssetTrack *track = [[compressedAsset tracksWithMediaType:AVMediaTypeVideo] firstObject];
|
||||
|
||||
|
||||
NSNumber *fileSizeValue = nil;
|
||||
[outputURL getResourceValue:&fileSizeValue
|
||||
forKey:NSURLFileSizeKey
|
||||
error:nil];
|
||||
|
||||
|
||||
completion([self createAttachmentResponse:[outputURL absoluteString]
|
||||
withExif:nil
|
||||
withSourceURL:[sourceURL absoluteString]
|
||||
withLocalIdentifier: forAsset.localIdentifier
|
||||
withFilename:[forAsset valueForKey:@"filename"]
|
||||
withLocalIdentifier: forAsset.localIdentifier
|
||||
withFilename:[forAsset valueForKey:@"filename"]
|
||||
withWidth:[NSNumber numberWithFloat:track.naturalSize.width]
|
||||
withHeight:[NSNumber numberWithFloat:track.naturalSize.height]
|
||||
withMime:@"video/mp4"
|
||||
withSize:fileSizeValue
|
||||
withData:[NSNull null]]);
|
||||
withData:nil]);
|
||||
} else {
|
||||
completion(nil);
|
||||
}
|
||||
@ -392,43 +403,44 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSDictionary*) createAttachmentResponse:(NSString*)filePath withSourceURL:(NSString*)sourceURL withLocalIdentifier:(NSString*)localIdentifier withFilename:(NSString*)filename withWidth:(NSNumber*)width withHeight:(NSNumber*)height withMime:(NSString*)mime withSize:(NSNumber*)size withData:(NSString*)data {
|
||||
- (NSDictionary*) createAttachmentResponse:(NSString*)filePath withExif:(NSDictionary*) exif withSourceURL:(NSString*)sourceURL withLocalIdentifier:(NSString*)localIdentifier withFilename:(NSString*)filename withWidth:(NSNumber*)width withHeight:(NSNumber*)height withMime:(NSString*)mime withSize:(NSNumber*)size withData:(NSString*)data {
|
||||
return @{
|
||||
@"path": filePath,
|
||||
@"sourceURL": (sourceURL) ? sourceURL : @"",
|
||||
@"localIdentifier": (localIdentifier) ? localIdentifier : @"",
|
||||
@"filename": (filename) ? filename : @"",
|
||||
@"sourceURL": (sourceURL) ? sourceURL : [NSNull null],
|
||||
@"localIdentifier": (localIdentifier) ? localIdentifier : [NSNull null],
|
||||
@"filename": (filename) ? filename : [NSNull null],
|
||||
@"width": width,
|
||||
@"height": height,
|
||||
@"mime": mime,
|
||||
@"size": size,
|
||||
@"data": data,
|
||||
@"data": (data) ? data : [NSNull null],
|
||||
@"exif": (exif) ? exif : [NSNull null],
|
||||
};
|
||||
}
|
||||
|
||||
- (void)qb_imagePickerController:
|
||||
(QBImagePickerController *)imagePickerController
|
||||
didFinishPickingAssets:(NSArray *)assets {
|
||||
|
||||
|
||||
PHImageManager *manager = [PHImageManager defaultManager];
|
||||
PHImageRequestOptions* options = [[PHImageRequestOptions alloc] init];
|
||||
options.synchronous = NO;
|
||||
options.networkAccessAllowed = YES;
|
||||
|
||||
|
||||
if ([[[self options] objectForKey:@"multiple"] boolValue]) {
|
||||
NSMutableArray *selections = [[NSMutableArray alloc] init];
|
||||
|
||||
|
||||
[self showActivityIndicator:^(UIActivityIndicatorView *indicatorView, UIView *overlayView) {
|
||||
NSLock *lock = [[NSLock alloc] init];
|
||||
__block int processed = 0;
|
||||
|
||||
|
||||
for (PHAsset *phAsset in assets) {
|
||||
|
||||
|
||||
if (phAsset.mediaType == PHAssetMediaTypeVideo) {
|
||||
[self getVideoAsset:phAsset completion:^(NSDictionary* video) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[lock lock];
|
||||
|
||||
|
||||
if (video == nil) {
|
||||
[indicatorView stopAnimating];
|
||||
[overlayView removeFromSuperview];
|
||||
@ -437,11 +449,11 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
}]];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
[selections addObject:video];
|
||||
processed++;
|
||||
[lock unlock];
|
||||
|
||||
|
||||
if (processed == [assets count]) {
|
||||
[indicatorView stopAnimating];
|
||||
[overlayView removeFromSuperview];
|
||||
@ -478,22 +490,28 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary* exif = nil;
|
||||
if([[self.options objectForKey:@"includeExif"] boolValue]) {
|
||||
exif = [[CIImage imageWithData:imageData] properties];
|
||||
}
|
||||
|
||||
[selections addObject:[self createAttachmentResponse:filePath
|
||||
withSourceURL:[sourceURL absoluteString]
|
||||
withExif: exif
|
||||
withSourceURL:[sourceURL absoluteString]
|
||||
withLocalIdentifier: phAsset.localIdentifier
|
||||
withFilename: [phAsset valueForKey:@"filename"]
|
||||
withWidth:imageResult.width
|
||||
withHeight:imageResult.height
|
||||
withMime:imageResult.mime
|
||||
withSize:[NSNumber numberWithUnsignedInteger:imageResult.data.length]
|
||||
withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [imageResult.data base64EncodedStringWithOptions:0] : [NSNull null]
|
||||
withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [imageResult.data base64EncodedStringWithOptions:0]: nil
|
||||
]];
|
||||
}
|
||||
processed++;
|
||||
[lock unlock];
|
||||
|
||||
|
||||
if (processed == [assets count]) {
|
||||
|
||||
|
||||
[indicatorView stopAnimating];
|
||||
[overlayView removeFromSuperview];
|
||||
[imagePickerController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{
|
||||
@ -508,7 +526,7 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
}];
|
||||
} else {
|
||||
PHAsset *phAsset = [assets objectAtIndex:0];
|
||||
|
||||
|
||||
[self showActivityIndicator:^(UIActivityIndicatorView *indicatorView, UIView *overlayView) {
|
||||
if (phAsset.mediaType == PHAssetMediaTypeVideo) {
|
||||
[self getVideoAsset:phAsset completion:^(NSDictionary* video) {
|
||||
@ -532,10 +550,21 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
UIImageOrientation orientation,
|
||||
NSDictionary *info) {
|
||||
NSURL *sourceURL = [info objectForKey:@"PHImageFileURLKey"];
|
||||
NSDictionary* exif;
|
||||
if([[self.options objectForKey:@"includeExif"] boolValue]) {
|
||||
exif = [[CIImage imageWithData:imageData] properties];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[indicatorView stopAnimating];
|
||||
[overlayView removeFromSuperview];
|
||||
[self processSingleImagePick:[UIImage imageWithData:imageData] withViewController:imagePickerController withSourceURL:[sourceURL absoluteString] withLocalIdentifier:phAsset.localIdentifier withFilename:[phAsset valueForKey:@"filename"]];
|
||||
|
||||
[self processSingleImagePick:[UIImage imageWithData:imageData]
|
||||
withExif: exif
|
||||
withViewController:imagePickerController
|
||||
withSourceURL:[sourceURL absoluteString]
|
||||
withLocalIdentifier:phAsset.localIdentifier
|
||||
withFilename:[phAsset valueForKey:@"filename"]];
|
||||
});
|
||||
}];
|
||||
}
|
||||
@ -552,8 +581,8 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
// when user selected single image, with camera or from photo gallery,
|
||||
// this method will take care of attaching image metadata, and sending image to cropping controller
|
||||
// or to user directly
|
||||
- (void) processSingleImagePick:(UIImage*)image withViewController:(UIViewController*)viewController withSourceURL:(NSString*)sourceURL withLocalIdentifier:(NSString*)localIdentifier withFilename:(NSString*)filename {
|
||||
|
||||
- (void) processSingleImagePick:(UIImage*)image withExif:(NSDictionary*) exif withViewController:(UIViewController*)viewController withSourceURL:(NSString*)sourceURL withLocalIdentifier:(NSString*)localIdentifier withFilename:(NSString*)filename {
|
||||
|
||||
if (image == nil) {
|
||||
[viewController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{
|
||||
self.reject(ERROR_PICKER_NO_DATA_KEY, ERROR_PICKER_NO_DATA_MSG, nil);
|
||||
@ -580,11 +609,12 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
}]];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Wait for viewController to dismiss before resolving, or we lose the ability to display
|
||||
// Alert.alert in the .then() handler.
|
||||
[viewController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{
|
||||
self.resolve([self createAttachmentResponse:filePath
|
||||
withExif:exif
|
||||
withSourceURL:sourceURL
|
||||
withLocalIdentifier:localIdentifier
|
||||
withFilename:filename
|
||||
@ -592,7 +622,7 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
withHeight:imageResult.height
|
||||
withMime:imageResult.mime
|
||||
withSize:[NSNumber numberWithUnsignedInteger:imageResult.data.length]
|
||||
withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [imageResult.data base64EncodedStringWithOptions:0] : [NSNull null]]);
|
||||
withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [imageResult.data base64EncodedStringWithOptions:0] : nil]);
|
||||
}]];
|
||||
}
|
||||
}
|
||||
@ -605,14 +635,14 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
CGSize maskSize = CGSizeMake(
|
||||
[[self.options objectForKey:@"width"] intValue],
|
||||
[[self.options objectForKey:@"height"] intValue]);
|
||||
|
||||
|
||||
CGFloat viewWidth = CGRectGetWidth(controller.view.frame);
|
||||
CGFloat viewHeight = CGRectGetHeight(controller.view.frame);
|
||||
|
||||
|
||||
CGRect maskRect = CGRectMake((viewWidth - maskSize.width) * 0.5f,
|
||||
(viewHeight - maskSize.height) * 0.5f,
|
||||
maskSize.width, maskSize.height);
|
||||
|
||||
|
||||
return maskRect;
|
||||
}
|
||||
|
||||
@ -622,13 +652,13 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
CGRect rect = controller.maskRect;
|
||||
CGFloat viewWidth = CGRectGetWidth(controller.view.frame);
|
||||
CGFloat viewHeight = CGRectGetHeight(controller.view.frame);
|
||||
|
||||
|
||||
double scaleFactor = fmin(viewWidth / rect.size.width, viewHeight / rect.size.height);
|
||||
rect.size.width *= scaleFactor;
|
||||
rect.size.height *= scaleFactor;
|
||||
rect.origin.x = (viewWidth - rect.size.width) / 2;
|
||||
rect.origin.y = (viewHeight - rect.size.height) / 2;
|
||||
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
@ -653,26 +683,30 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
// Crop image has been canceled.
|
||||
- (void)imageCropViewControllerDidCancelCrop:
|
||||
(RSKImageCropViewController *)controller {
|
||||
[self dismissCropper:controller dismissAll: NO completion:[self waitAnimationEnd:^{
|
||||
if (self.cropOnly) {
|
||||
[self dismissCropper:controller selectionDone:NO completion:[self waitAnimationEnd:^{
|
||||
if (self.currentSelectionMode == CROPPING) {
|
||||
self.reject(ERROR_PICKER_CANCEL_KEY, ERROR_PICKER_CANCEL_MSG, nil);
|
||||
}
|
||||
}]];
|
||||
}
|
||||
|
||||
- (void) dismissCropper:(RSKImageCropViewController*) controller dismissAll: (BOOL) dissmissAll completion:(void (^)())completion {
|
||||
//We've presented the cropper on top of the image picker as to not have a double modal animation.
|
||||
//Thus, we need to dismiss the image picker view controller to dismiss the whole stack.
|
||||
if (!self.cropOnly) {
|
||||
if (dissmissAll) {
|
||||
UIViewController *topViewController = controller.presentingViewController.presentingViewController;
|
||||
[topViewController dismissViewControllerAnimated:YES completion:completion];
|
||||
} else {
|
||||
UIViewController *topViewController = controller.presentingViewController;
|
||||
[topViewController dismissViewControllerAnimated:YES completion:completion];
|
||||
}
|
||||
} else {
|
||||
[controller dismissViewControllerAnimated:YES completion:completion];
|
||||
- (void) dismissCropper:(RSKImageCropViewController*)controller selectionDone:(BOOL)selectionDone completion:(void (^)())completion {
|
||||
switch (self.currentSelectionMode) {
|
||||
case CROPPING:
|
||||
[controller dismissViewControllerAnimated:YES completion:completion];
|
||||
break;
|
||||
case PICKER:
|
||||
if (selectionDone) {
|
||||
[controller.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:completion];
|
||||
} else {
|
||||
// if user opened picker, tried to crop image, and cancelled cropping
|
||||
// return him to the image selection instead of returning him to the app
|
||||
[controller.presentingViewController dismissViewControllerAnimated:YES completion:completion];
|
||||
}
|
||||
break;
|
||||
case CAMERA:
|
||||
[controller.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:completion];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -680,31 +714,37 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
- (void)imageCropViewController:(RSKImageCropViewController *)controller
|
||||
didCropImage:(UIImage *)croppedImage
|
||||
usingCropRect:(CGRect)cropRect {
|
||||
|
||||
|
||||
// we have correct rect, but not correct dimensions
|
||||
// so resize image
|
||||
CGSize resizedImageSize = CGSizeMake([[[self options] objectForKey:@"width"] intValue], [[[self options] objectForKey:@"height"] intValue]);
|
||||
UIImage *resizedImage = [croppedImage resizedImageToFitInSize:resizedImageSize scaleIfSmaller:YES];
|
||||
ImageResult *imageResult = [self.compression compressImage:resizedImage withOptions:self.options];
|
||||
|
||||
|
||||
NSString *filePath = [self persistFile:imageResult.data];
|
||||
if (filePath == nil) {
|
||||
[self dismissCropper:controller dismissAll: YES completion:[self waitAnimationEnd:^{
|
||||
[self dismissCropper:controller selectionDone:YES completion:[self waitAnimationEnd:^{
|
||||
self.reject(ERROR_CANNOT_SAVE_IMAGE_KEY, ERROR_CANNOT_SAVE_IMAGE_MSG, nil);
|
||||
}]];
|
||||
return;
|
||||
}
|
||||
|
||||
[self dismissCropper:controller dismissAll: YES completion:[self waitAnimationEnd:^{
|
||||
|
||||
NSDictionary* exif = nil;
|
||||
if([[self.options objectForKey:@"includeExif"] boolValue]) {
|
||||
exif = [[CIImage imageWithData:imageResult.data] properties];
|
||||
}
|
||||
|
||||
[self dismissCropper:controller selectionDone:YES completion:[self waitAnimationEnd:^{
|
||||
self.resolve([self createAttachmentResponse:filePath
|
||||
withExif: exif
|
||||
withSourceURL: self.croppingFile[@"sourceURL"]
|
||||
withLocalIdentifier: self.croppingFile[@"localIdentifier"]
|
||||
withFilename: self.croppingFile[@"filename"]
|
||||
withLocalIdentifier: self.croppingFile[@"localIdentifier"]
|
||||
withFilename: self.croppingFile[@"filename"]
|
||||
withWidth:imageResult.width
|
||||
withHeight:imageResult.height
|
||||
withMime:imageResult.mime
|
||||
withSize:[NSNumber numberWithUnsignedInteger:imageResult.data.length]
|
||||
withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [imageResult.data base64EncodedStringWithOptions:0] : [NSNull null]]);
|
||||
withData:[[self.options objectForKey:@"includeBase64"] boolValue] ? [imageResult.data base64EncodedStringWithOptions:0] : nil]);
|
||||
}]];
|
||||
}
|
||||
|
||||
@ -715,13 +755,13 @@ RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options
|
||||
NSString *tmpDirFullPath = [self getTmpDirectory];
|
||||
NSString *filePath = [tmpDirFullPath stringByAppendingString:[[NSUUID UUID] UUIDString]];
|
||||
filePath = [filePath stringByAppendingString:@".jpg"];
|
||||
|
||||
|
||||
// save cropped file
|
||||
BOOL status = [data writeToFile:filePath atomically:YES];
|
||||
if (!status) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-image-crop-picker",
|
||||
"version": "0.16.1",
|
||||
"version": "0.17.0",
|
||||
"description": "Select single or multiple images, with cropping option",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@ -40,4 +40,4 @@
|
||||
"url": "https://opencollective.com/react-native-image-crop-picker",
|
||||
"logo": "https://opencollective.com/opencollective/logo.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user