在使用react-native-image-crop-picker(版本0.20.0)组件选择图片的时候,我希望将选取的图片保存到应用内部。
但是在ios端,选取的图片被保存在临时目录中,大约3天左右会被自动删除。
在android端,选取的图片没有往App内部保存,还是在原来的位置,组件只是返回了图片的存储位置。
要解决这个问题,修改该组件ios和android端的源码即可。
1.android端
修改node_modules\react-native-image-crop-picker\android\src\main\java\com\reactnative\ivpusic\imagepicker\PickerModule.java文件
修改如下
package com.reactnative.ivpusic.imagepicker; import android.Manifest; import android.app.Activity; import android.content.ClipData; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import android.support.v4.app.ActivityCompat; import android.support.v4.content.FileProvider; import android.util.Base64; import android.webkit.MimeTypeMap; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.PromiseImpl; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.PermissionAwareActivity; import com.facebook.react.modules.core.PermissionListener; import com.yalantis.ucrop.UCrop; import com.yalantis.ucrop.UCropActivity; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.Callable; class PickerModule extends ReactContextBaseJavaModule implements ActivityEventListener { private static final int IMAGE_PICKER_REQUEST = 61110; private static final int CAMERA_PICKER_REQUEST = 61111; private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST"; private static final String E_PICKER_CANCELLED_KEY = "E_PICKER_CANCELLED"; private static final String E_PICKER_CANCELLED_MSG = "User cancelled image selection"; private static final String E_CALLBACK_ERROR = "E_CALLBACK_ERROR"; private static final String E_FAILED_TO_SHOW_PICKER = "E_FAILED_TO_SHOW_PICKER"; private static final String E_FAILED_TO_OPEN_CAMERA = "E_FAILED_TO_OPEN_CAMERA"; private static final String E_NO_IMAGE_DATA_FOUND = "E_NO_IMAGE_DATA_FOUND"; private static final String E_CAMERA_IS_NOT_AVAILABLE = "E_CAMERA_IS_NOT_AVAILABLE"; private static final String E_CANNOT_LAUNCH_CAMERA = "E_CANNOT_LAUNCH_CAMERA"; private static final String E_PERMISSIONS_MISSING = "E_PERMISSION_MISSING"; private static final String E_ERROR_WHILE_CLEANING_FILES = "E_ERROR_WHILE_CLEANING_FILES"; 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 freeStyleCropEnabled = false; private boolean showCropGuidelines = true; private boolean hideBottomControls = false; private boolean enableRotationGesture = false; private ReadableMap options; //Grey 800 private final String DEFAULT_TINT = "#424242"; private String cropperActiveWidgetColor = DEFAULT_TINT; private String cropperStatusBarColor = DEFAULT_TINT; private String cropperToolbarColor = DEFAULT_TINT; private String cropperToolbarTitle = null; //Light Blue 500 private final String DEFAULT_WIDGET_COLOR = "#03A9F4"; private int width = 200; private int height = 200; private Uri mCameraCaptureURI; private String mCurrentPhotoPath; private ResultCollector resultCollector; private Compression compression = new Compression(); private ReactApplicationContext reactContext = null; PickerModule(ReactApplicationContext reactContext) { super(reactContext); reactContext.addActivityEventListener(this); this.reactContext = reactContext; } private String getTmpDir(Activity activity) { String tmpDir = activity.getCacheDir() + "/react-native-image-crop-picker"; Boolean created = new File(tmpDir).mkdir(); return tmpDir; } @Override public String getName() { return "ImageCropPicker"; } private void setConfiguration(final ReadableMap options) { 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; cropperActiveWidgetColor = options.hasKey("cropperActiveWidgetColor") ? options.getString("cropperActiveWidgetColor") : cropperActiveWidgetColor; cropperStatusBarColor = options.hasKey("cropperStatusBarColor") ? options.getString("cropperStatusBarColor") : cropperStatusBarColor; cropperToolbarColor = options.hasKey("cropperToolbarColor") ? options.getString("cropperToolbarColor") : cropperToolbarColor; cropperToolbarTitle = options.hasKey("cropperToolbarTitle") ? options.getString("cropperToolbarTitle") : null; cropperCircleOverlay = options.hasKey("cropperCircleOverlay") ? options.getBoolean("cropperCircleOverlay") : cropperCircleOverlay; freeStyleCropEnabled = options.hasKey("freeStyleCropEnabled") ? options.getBoolean("freeStyleCropEnabled") : freeStyleCropEnabled; showCropGuidelines = options.hasKey("showCropGuidelines") ? options.getBoolean("showCropGuidelines") : showCropGuidelines; hideBottomControls = options.hasKey("hideBottomControls") ? options.getBoolean("hideBottomControls") : hideBottomControls; enableRotationGesture = options.hasKey("enableRotationGesture") ? options.getBoolean("enableRotationGesture") : enableRotationGesture; this.options = options; } private void deleteRecursive(File fileOrDirectory) { if (fileOrDirectory.isDirectory()) { for (File child : fileOrDirectory.listFiles()) { deleteRecursive(child); } } fileOrDirectory.delete(); } @ReactMethod public void clean(final Promise promise) { final Activity activity = getCurrentActivity(); final PickerModule module = this; if (activity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); return; } permissionsCheck(activity, promise, Arrays.asList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable<Void>() { @Override public Void call() throws Exception { try { File file = new File(module.getTmpDir(activity)); if (!file.exists()) throw new Exception("File does not exist"); module.deleteRecursive(file); promise.resolve(null); } catch (Exception ex) { ex.printStackTrace(); promise.reject(E_ERROR_WHILE_CLEANING_FILES, ex.getMessage()); } return null; } }); } @ReactMethod public void cleanSingle(final String pathToDelete, final Promise promise) { if (pathToDelete == null) { promise.reject(E_ERROR_WHILE_CLEANING_FILES, "Cannot cleanup empty path"); return; } final Activity activity = getCurrentActivity(); final PickerModule module = this; if (activity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); return; } permissionsCheck(activity, promise, Arrays.asList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable<Void>() { @Override public Void call() throws Exception { try { String path = pathToDelete; final String filePrefix = "file://"; if (path.startsWith(filePrefix)) { path = path.substring(filePrefix.length()); } File file = new File(path); if (!file.exists()) throw new Exception("File does not exist. Path: " + path); module.deleteRecursive(file); promise.resolve(null); } catch (Exception ex) { ex.printStackTrace(); promise.reject(E_ERROR_WHILE_CLEANING_FILES, ex.getMessage()); } return null; } }); } private void permissionsCheck(final Activity activity, final Promise promise, final List<String> requiredPermissions, final Callable<Void> callback) { List<String> missingPermissions = new ArrayList<>(); for (String permission : requiredPermissions) { int status = ActivityCompat.checkSelfPermission(activity, permission); if (status != PackageManager.PERMISSION_GRANTED) { missingPermissions.add(permission); } } if (!missingPermissions.isEmpty()) { ((PermissionAwareActivity) activity).requestPermissions(missingPermissions.toArray(new String[missingPermissions.size()]), 1, new PermissionListener() { @Override public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode == 1) { for (int grantResult : grantResults) { if (grantResult == PackageManager.PERMISSION_DENIED) { promise.reject(E_PERMISSIONS_MISSING, "Required permission missing"); return true; } } try { callback.call(); } catch (Exception e) { promise.reject(E_CALLBACK_ERROR, "Unknown error", e); } } return true; } }); return; } // all permissions granted try { callback.call(); } catch (Exception e) { promise.reject(E_CALLBACK_ERROR, "Unknown error", e); } } @ReactMethod public void openCamera(final ReadableMap options, final Promise promise) { final Activity activity = getCurrentActivity(); if (activity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); return; } if (!isCameraAvailable(activity)) { promise.reject(E_CAMERA_IS_NOT_AVAILABLE, "Camera not available"); return; } setConfiguration(options); resultCollector = new ResultCollector(promise, multiple); permissionsCheck(activity, promise, Arrays.asList(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable<Void>() { @Override public Void call() throws Exception { initiateCamera(activity); return null; } }); } private void initiateCamera(Activity activity) { try { int requestCode = CAMERA_PICKER_REQUEST; Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File imageFile = createImageFile(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { mCameraCaptureURI = Uri.fromFile(imageFile); } else { mCameraCaptureURI = FileProvider.getUriForFile(activity, activity.getApplicationContext().getPackageName() + ".provider", imageFile); } cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, mCameraCaptureURI); if (cameraIntent.resolveActivity(activity.getPackageManager()) == null) { resultCollector.notifyProblem(E_CANNOT_LAUNCH_CAMERA, "Cannot launch camera"); return; } activity.startActivityForResult(cameraIntent, requestCode); } catch (Exception e) { resultCollector.notifyProblem(E_FAILED_TO_OPEN_CAMERA, e); } } private void initiatePicker(final Activity activity) { try { final Intent galleryIntent = new Intent(Intent.ACTION_PICK); if (cropping || mediaType.equals("photo")) { galleryIntent.setType("image/*"); } else if (mediaType.equals("video")) { galleryIntent.setType("video/*"); } else { galleryIntent.setType("*/*"); String[] mimetypes = {"image/*", "video/*"}; galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); } galleryIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); galleryIntent.setAction(Intent.ACTION_GET_CONTENT); galleryIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); final Intent chooserIntent = Intent.createChooser(galleryIntent, "Pick an image"); activity.startActivityForResult(chooserIntent, IMAGE_PICKER_REQUEST); } catch (Exception e) { resultCollector.notifyProblem(E_FAILED_TO_SHOW_PICKER, e); } } @ReactMethod public void openPicker(final ReadableMap options, final Promise promise) { final Activity activity = getCurrentActivity(); if (activity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); return; } setConfiguration(options); resultCollector = new ResultCollector(promise, multiple); permissionsCheck(activity, promise, Collections.singletonList(Manifest.permission.WRITE_EXTERNAL_STORAGE), new Callable<Void>() { @Override public Void call() throws Exception { initiatePicker(activity); return null; } }); } @ReactMethod public void openCropper(final ReadableMap options, final Promise promise) { final Activity activity = getCurrentActivity(); if (activity == null) { promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist"); return; } setConfiguration(options); resultCollector = new ResultCollector(promise, false); Uri uri = Uri.parse(options.getString("path")); startCropping(activity, uri); } private String getBase64StringFromFile(String absoluteFilePath) { InputStream inputStream; try { inputStream = new FileInputStream(new File(absoluteFilePath)); } catch (FileNotFoundException e) { e.printStackTrace(); return null; } byte[] bytes; byte[] buffer = new byte[8192]; int bytesRead; ByteArrayOutputStream output = new ByteArrayOutputStream(); try { while ((bytesRead = inputStream.read(buffer)) != -1) { output.write(buffer, 0, bytesRead); } } catch (IOException e) { e.printStackTrace(); } bytes = output.toByteArray(); return Base64.encodeToString(bytes, Base64.NO_WRAP); } private static String getMimeType(String url) { String type = null; String extension = MimeTypeMap.getFileExtensionFromUrl(url); if (extension != null) { type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } return type; } private WritableMap getSelection(Activity activity, Uri uri, boolean isCamera) throws Exception { String path = resolveRealPath(activity, uri, isCamera); if (path == null || path.isEmpty()) { throw new Exception("Cannot resolve asset path."); } return getImage(activity, path); } private void getAsyncSelection(final Activity activity, Uri uri, boolean isCamera) throws Exception { //String path = resolveRealPath(activity, uri, isCamera); File file = createFileFromURI(uri); String path = file.getAbsolutePath(); //File imageFile = new File(uri.getPath()); if (path == null || path.isEmpty()) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, "Cannot resolve asset path."); return; } String mime = getMimeType(path); if (mime != null && mime.startsWith("video/")) { getVideo(activity, path, mime); return; } resultCollector.notifySuccess(getImage(activity, path)); } private File createFileFromURI(Uri uri) throws Exception { File imgPath = reactContext.getExternalCacheDir(); File path2 = new File(imgPath.getAbsolutePath().replace("cache","images")); if (!path2.exists() && !path2.isDirectory()) { path2.mkdirs(); } String str = uri.getLastPathSegment(); str = str.substring(str.indexOf(".")); File file = new File(path2.getAbsolutePath(), UUID.randomUUID().toString() + str); InputStream input = reactContext.getContentResolver().openInputStream(uri); OutputStream output = new FileOutputStream(file); try { byte[] buffer = new byte[4 * 1024]; int read; while ((read = input.read(buffer)) != -1) { output.write(buffer, 0, read); } output.flush(); } finally { output.close(); input.close(); } return file; } private Bitmap validateVideo(String path) throws Exception { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(path); Bitmap bmp = retriever.getFrameAtTime(); if (bmp == null) { throw new Exception("Cannot retrieve video data"); } return bmp; } private void getVideo(final Activity activity, final String path, final String mime) throws Exception { validateVideo(path); final String compressedVideoPath = getTmpDir(activity) + "/" + UUID.randomUUID().toString() + ".mp4"; new Thread(new Runnable() { @Override public void run() { compression.compressVideo(activity, options, path, compressedVideoPath, new PromiseImpl(new Callback() { @Override public void invoke(Object... args) { String videoPath = (String) args[0]; try { Bitmap bmp = validateVideo(videoPath); long modificationDate = new File(videoPath).lastModified(); WritableMap video = new WritableNativeMap(); video.putInt("width", bmp.getWidth()); video.putInt("height", bmp.getHeight()); video.putString("mime", mime); video.putInt("size", (int) new File(videoPath).length()); video.putString("path", "file://" + videoPath); video.putString("modificationDate", String.valueOf(modificationDate)); resultCollector.notifySuccess(video); } catch (Exception e) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, e); } } }, new Callback() { @Override public void invoke(Object... args) { WritableNativeMap ex = (WritableNativeMap) args[0]; resultCollector.notifyProblem(ex.getString("code"), ex.getString("message")); } })); } }).run(); } private String resolveRealPath(Activity activity, Uri uri, boolean isCamera) { String path; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { path = RealPathUtil.getRealPathFromURI(activity, uri); } else { if (isCamera) { Uri imageUri = Uri.parse(mCurrentPhotoPath); path = imageUri.getPath(); } else { path = RealPathUtil.getRealPathFromURI(activity, uri); } } return path; } private BitmapFactory.Options validateImage(String path) throws Exception { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inPreferredConfig = Bitmap.Config.RGB_565; options.inDither = true; BitmapFactory.decodeFile(path, options); if (options.outMimeType == null || options.outWidth == 0 || options.outHeight == 0) { throw new Exception("Invalid image selected"); } return options; } private WritableMap getImage(final Activity activity, String path) throws Exception { WritableMap image = new WritableNativeMap(); if (path.startsWith("http://") || path.startsWith("https://")) { throw new Exception("Cannot select remote files"); } validateImage(path); // if compression options are provided image will be compressed. If none options is provided, // then original image will be returned File compressedImage = compression.compressImage(activity, options, path); String compressedImagePath = compressedImage.getPath(); BitmapFactory.Options options = validateImage(compressedImagePath); long modificationDate = new File(path).lastModified(); image.putString("path", "file://" + compressedImagePath); image.putInt("width", options.outWidth); image.putInt("height", options.outHeight); image.putString("mime", options.outMimeType); image.putInt("size", (int) new File(compressedImagePath).length()); image.putString("modificationDate", String.valueOf(modificationDate)); if (includeBase64) { image.putString("data", getBase64StringFromFile(compressedImagePath)); } if (includeExif) { try { WritableMap exif = ExifExtractor.extract(path); image.putMap("exif", exif); } catch (Exception ex) { ex.printStackTrace(); } } return image; } private void configureCropperColors(UCrop.Options options) { int activeWidgetColor = Color.parseColor(cropperActiveWidgetColor); int toolbarColor = Color.parseColor(cropperToolbarColor); int statusBarColor = Color.parseColor(cropperStatusBarColor); options.setToolbarColor(toolbarColor); options.setStatusBarColor(statusBarColor); if (activeWidgetColor == Color.parseColor(DEFAULT_TINT)) { /* Default tint is grey => use a more flashy color that stands out more as the call to action Here we use 'Light Blue 500' from https://material.google.com/style/color.html#color-color-palette */ options.setActiveWidgetColor(Color.parseColor(DEFAULT_WIDGET_COLOR)); } else { //If they pass a custom tint color in, we use this for everything options.setActiveWidgetColor(activeWidgetColor); } } private void startCropping(Activity activity, Uri uri) { UCrop.Options options = new UCrop.Options(); options.setCompressionFormat(Bitmap.CompressFormat.JPEG); options.setCompressionQuality(100); options.setCircleDimmedLayer(cropperCircleOverlay); options.setFreeStyleCropEnabled(freeStyleCropEnabled); options.setShowCropGrid(showCropGuidelines); options.setHideBottomControls(hideBottomControls); if (cropperToolbarTitle != null) { options.setToolbarTitle(cropperToolbarTitle); } if (enableRotationGesture) { // UCropActivity.ALL = enable both rotation & scaling options.setAllowedGestures( UCropActivity.ALL, // When 'scale'-tab active UCropActivity.ALL, // When 'rotate'-tab active UCropActivity.ALL // When 'aspect ratio'-tab active ); } configureCropperColors(options); UCrop.of(uri, Uri.fromFile(new File(this.getTmpDir(activity), UUID.randomUUID().toString() + ".jpg"))) .withMaxResultSize(width, height) .withAspectRatio(width, height) .withOptions(options) .start(activity); } private void imagePickerResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (resultCode == Activity.RESULT_CANCELED) { resultCollector.notifyProblem(E_PICKER_CANCELLED_KEY, E_PICKER_CANCELLED_MSG); } else if (resultCode == Activity.RESULT_OK) { if (multiple) { ClipData clipData = data.getClipData(); try { // only one image selected if (clipData == null) { resultCollector.setWaitCount(1); getAsyncSelection(activity, data.getData(), false); } else { resultCollector.setWaitCount(clipData.getItemCount()); for (int i = 0; i < clipData.getItemCount(); i++) { getAsyncSelection(activity, clipData.getItemAt(i).getUri(), false); } } } catch (Exception ex) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); } } else { Uri uri = data.getData(); if (uri == null) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, "Cannot resolve image url"); return; } if (cropping) { startCropping(activity, uri); } else { try { getAsyncSelection(activity, uri, false); } catch (Exception ex) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); } } } } } private void cameraPickerResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (resultCode == Activity.RESULT_CANCELED) { resultCollector.notifyProblem(E_PICKER_CANCELLED_KEY, E_PICKER_CANCELLED_MSG); } else if (resultCode == Activity.RESULT_OK) { Uri uri = mCameraCaptureURI; if (uri == null) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, "Cannot resolve image url"); return; } if (cropping) { UCrop.Options options = new UCrop.Options(); options.setCompressionFormat(Bitmap.CompressFormat.JPEG); startCropping(activity, uri); } else { try { resultCollector.setWaitCount(1); resultCollector.notifySuccess(getSelection(activity, uri, true)); } catch (Exception ex) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); } } } } private void croppingResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (data != null) { final Uri resultUri = UCrop.getOutput(data); if (resultUri != null) { try { WritableMap result = getSelection(activity, resultUri, false); result.putMap("cropRect", PickerModule.getCroppedRectMap(data)); resultCollector.setWaitCount(1); resultCollector.notifySuccess(result); } catch (Exception ex) { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, ex.getMessage()); } } else { resultCollector.notifyProblem(E_NO_IMAGE_DATA_FOUND, "Cannot find image data"); } } else { resultCollector.notifyProblem(E_PICKER_CANCELLED_KEY, E_PICKER_CANCELLED_MSG); } } @Override public void onActivityResult(Activity activity, final int requestCode, final int resultCode, final Intent data) { if (requestCode == IMAGE_PICKER_REQUEST) { imagePickerResult(activity, requestCode, resultCode, data); } else if (requestCode == CAMERA_PICKER_REQUEST) { cameraPickerResult(activity, requestCode, resultCode, data); } else if (requestCode == UCrop.REQUEST_CROP) { croppingResult(activity, requestCode, resultCode, data); } } @Override public void onNewIntent(Intent intent) { } private boolean isCameraAvailable(Activity activity) { return activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) || activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); } private File createImageFile() throws IOException { String imageFileName = "image-" + UUID.randomUUID().toString(); //File file = new File(reactContext.getExternalCacheDir(), "photo-" + uri.getLastPathSegment()); File path = reactContext.getExternalCacheDir(); String filePath = path.getAbsolutePath().replace("cache","images"); // File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); path = new File(filePath); if (!path.exists() && !path.isDirectory()) { path.mkdirs(); } File image = File.createTempFile(imageFileName, ".jpg", path); //start String filename = new StringBuilder("image-").append(UUID.randomUUID().toString()) .append(".jpg").toString(); //ReadableMapUtils.hasAndNotNullReadableMap(options, "storageOptions") //File path2 = reactContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES); File result = new File(path, filename); try { path.mkdirs(); result.createNewFile(); } catch (IOException e) { e.printStackTrace(); result = null; } // Save a file: path for use with ACTION_VIEW intents //mCurrentPhotoPath = "file:" + image.getAbsolutePath(); mCurrentPhotoPath = "file:" + result.getAbsolutePath(); return result; } private static WritableMap getCroppedRectMap(Intent data) { final int DEFAULT_VALUE = -1; final WritableMap map = new WritableNativeMap(); map.putInt("x", data.getIntExtra(UCrop.EXTRA_OUTPUT_OFFSET_X, DEFAULT_VALUE)); map.putInt("y", data.getIntExtra(UCrop.EXTRA_OUTPUT_OFFSET_Y, DEFAULT_VALUE)); map.putInt("width", data.getIntExtra(UCrop.EXTRA_OUTPUT_IMAGE_WIDTH, DEFAULT_VALUE)); map.putInt("height", data.getIntExtra(UCrop.EXTRA_OUTPUT_IMAGE_HEIGHT, DEFAULT_VALUE)); return map; } }
2.ios端
修改node_modules\react-native-image-crop-picker\ios\src\ImageCropPicker.m文件
修改如下:
// // ImageManager.m // // Created by Ivan Pusic on 5/4/16. // Copyright © 2016 Facebook. All rights reserved. // #import "ImageCropPicker.h" #define ERROR_PICKER_CANNOT_RUN_CAMERA_ON_SIMULATOR_KEY @"E_PICKER_CANNOT_RUN_CAMERA_ON_SIMULATOR" #define ERROR_PICKER_CANNOT_RUN_CAMERA_ON_SIMULATOR_MSG @"Cannot run camera on simulator" #define ERROR_PICKER_NO_CAMERA_PERMISSION_KEY @"E_PICKER_NO_CAMERA_PERMISSION" #define ERROR_PICKER_NO_CAMERA_PERMISSION_MSG @"User did not grant camera permission." #define ERROR_PICKER_UNAUTHORIZED_KEY @"E_PERMISSION_MISSING" #define ERROR_PICKER_UNAUTHORIZED_MSG @"Cannot access images. Please allow access if you want to be able to select images." #define ERROR_PICKER_CANCEL_KEY @"E_PICKER_CANCELLED" #define ERROR_PICKER_CANCEL_MSG @"User cancelled image selection" #define ERROR_PICKER_NO_DATA_KEY @"E_NO_IMAGE_DATA_FOUND" #define ERROR_PICKER_NO_DATA_MSG @"Cannot find image data" #define ERROR_CROPPER_IMAGE_NOT_FOUND_KEY @"E_CROPPER_IMAGE_NOT_FOUND" #define ERROR_CROPPER_IMAGE_NOT_FOUND_MSG @"Can't find the image at the specified path" #define ERROR_CLEANUP_ERROR_KEY @"E_ERROR_WHILE_CLEANING_FILES" #define ERROR_CLEANUP_ERROR_MSG @"Error while cleaning up tmp files" #define ERROR_CANNOT_SAVE_IMAGE_KEY @"E_CANNOT_SAVE_IMAGE" #define ERROR_CANNOT_SAVE_IMAGE_MSG @"Cannot save image. Unable to write to tmp location." #define ERROR_CANNOT_PROCESS_VIDEO_KEY @"E_CANNOT_PROCESS_VIDEO" #define ERROR_CANNOT_PROCESS_VIDEO_MSG @"Cannot process video data" @implementation ImageResult @end @implementation ImageCropPicker RCT_EXPORT_MODULE(); @synthesize bridge = _bridge; - (instancetype)init { if (self = [super init]) { self.defaultOptions = @{ @"multiple": @NO, @"cropping": @NO, @"cropperCircleOverlay": @NO, @"includeBase64": @NO, @"includeExif": @NO, @"compressVideo": @YES, @"minFiles": @1, @"maxFiles": @5, @"width": @200, @"waitAnimationEnd": @YES, @"height": @200, @"useFrontCamera": @NO, @"compressImageQuality": @1, @"compressVideoPreset": @"MediumQuality", @"loadingLabelText": @"Processing assets...", @"mediaType": @"any", @"showsSelectedCount": @YES }; self.compression = [[Compression alloc] init]; } return self; } + (BOOL)requiresMainQueueSetup { return YES; } - (void (^ __nullable)(void))waitAnimationEnd:(void (^ __nullable)(void))completion { if ([[self.options objectForKey:@"waitAnimationEnd"] boolValue]) { return completion; } if (completion != nil) { completion(); } return nil; } - (void)checkCameraPermissions:(void(^)(BOOL granted))callback { AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (status == AVAuthorizationStatusAuthorized) { callback(YES); return; } else if (status == AVAuthorizationStatusNotDetermined){ [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { callback(granted); return; }]; } else { callback(NO); } } - (void) setConfiguration:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { self.resolve = resolve; self.reject = reject; self.options = [NSMutableDictionary dictionaryWithDictionary:self.defaultOptions]; for (NSString *key in options.keyEnumerator) { [self.options setValue:options[key] forKey:key]; } } - (UIViewController*) getRootVC { UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 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.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; #else [self checkCameraPermissions:^(BOOL granted) { if (!granted) { 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; picker.sourceType = UIImagePickerControllerSourceTypeCamera; if ([[self.options objectForKey:@"useFrontCamera"] boolValue]) { picker.cameraDevice = UIImagePickerControllerCameraDeviceFront; } dispatch_async(dispatch_get_main_queue(), ^{ [[self getRootVC] presentViewController:picker animated:YES completion:nil]; }); }]; #endif } - (void)viewDidLoad { [self viewDidLoad]; } - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { UIImage *chosenImage = [info objectForKey:UIImagePickerControllerOriginalImage]; UIImage *chosenImageT = [chosenImage fixOrientation]; 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"] withCreationDate:self.croppingFile[@"creationDate"] withModificationDate:self.croppingFile[@"modificationDate"]]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.reject(ERROR_PICKER_CANCEL_KEY, ERROR_PICKER_CANCEL_MSG, nil); }]]; } - (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; */ NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString * documentPath = [paths lastObject]; NSString *tmpFullPath =[documentPath stringByAppendingString:@"/image/"]; 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 { resolve(nil); } } RCT_REMAP_METHOD(clean, resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (![self cleanTmpDirectory]) { reject(ERROR_CLEANUP_ERROR_KEY, ERROR_CLEANUP_ERROR_MSG, nil); } else { resolve(nil); } } RCT_EXPORT_METHOD(openPicker:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [self setConfiguration:options resolver:resolve rejecter:reject]; 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 = [QBImagePickerController new]; imagePickerController.delegate = self; imagePickerController.allowsMultipleSelection = [[self.options objectForKey:@"multiple"] boolValue]; 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), }; NSMutableArray *albumsToShow = [NSMutableArray arrayWithCapacity:5]; for (NSString* album in [self.options objectForKey:@"smartAlbums"]) { if ([smartAlbums objectForKey:album] != nil) { [albumsToShow addObject:[smartAlbums objectForKey:album]]; } } 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"]) { imagePickerController.mediaType = QBImagePickerMediaTypeImage; } else { imagePickerController.mediaType = QBImagePickerMediaTypeVideo; } } [[self getRootVC] presentViewController:imagePickerController animated:YES completion:nil]; }); }]; } RCT_EXPORT_METHOD(openCropper:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { [self setConfiguration:options resolver:resolve rejecter:reject]; 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); } else { [self startCropping:image]; } }]; } - (void)startCropping:(UIImage *)image { RSKImageCropViewController *imageCropVC = [[RSKImageCropViewController alloc] initWithImage:image]; if ([[[self options] objectForKey:@"cropperCircleOverlay"] boolValue]) { imageCropVC.cropMode = RSKImageCropModeCircle; } else { imageCropVC.cropMode = RSKImageCropModeCustom; } imageCropVC.avoidEmptySpaceAroundImage = YES; imageCropVC.dataSource = self; imageCropVC.delegate = self; [imageCropVC setModalPresentationStyle:UIModalPresentationCustom]; [imageCropVC setModalTransitionStyle:UIModalTransitionStyleCrossDissolve]; dispatch_async(dispatch_get_main_queue(), ^{ [[self getRootVC] presentViewController:imageCropVC animated:YES completion:nil]; }); } - (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]; loadingLabel.textColor = [UIColor whiteColor]; loadingLabel.adjustsFontSizeToFitWidth = YES; CGPoint loadingLabelLocation = loadingView.center; loadingLabelLocation.y += [activityView bounds].size.height; loadingLabel.center = loadingLabelLocation; 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); }); } - (void) getVideoAsset:(PHAsset*)forAsset completion:(void (^)(NSDictionary* image))completion { 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"] withWidth:[NSNumber numberWithFloat:track.naturalSize.width] withHeight:[NSNumber numberWithFloat:track.naturalSize.height] withMime:@"video/mp4" withSize:fileSizeValue withData:nil withCreationDate:forAsset.creationDate withModificationDate:forAsset.modificationDate ]); } else { completion(nil); } }]; }]; } - (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 withCreationDate:(NSDate*)creationDate withModificationDate:(NSDate*)modificationDate { return @{ @"path": filePath, @"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 : [NSNull null], @"exif": (exif) ? exif : [NSNull null], @"creationDate:": (creationDate) ? [NSString stringWithFormat:@"%.0f", [creationDate timeIntervalSince1970]] : [NSNull null], @"modificationDate": (modificationDate) ? [NSString stringWithFormat:@"%.0f", [modificationDate timeIntervalSince1970]] : [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]; [imagePickerController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.reject(ERROR_CANNOT_PROCESS_VIDEO_KEY, ERROR_CANNOT_PROCESS_VIDEO_MSG, nil); }]]; return; } [selections addObject:video]; processed++; [lock unlock]; if (processed == [assets count]) { [indicatorView stopAnimating]; [overlayView removeFromSuperview]; [imagePickerController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.resolve(selections); }]]; return; } }); }]; } else { [manager requestImageDataForAsset:phAsset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { NSURL *sourceURL = [info objectForKey:@"PHImageFileURLKey"]; dispatch_async(dispatch_get_main_queue(), ^{ [lock lock]; @autoreleasepool { UIImage *imgT = [UIImage imageWithData:imageData]; UIImage *imageT = [imgT fixOrientation]; ImageResult *imageResult = [self.compression compressImage:imageT withOptions:self.options]; NSString *filePath = [self persistFile:imageResult.data]; if (filePath == nil) { [indicatorView stopAnimating]; [overlayView removeFromSuperview]; [imagePickerController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.reject(ERROR_CANNOT_SAVE_IMAGE_KEY, ERROR_CANNOT_SAVE_IMAGE_MSG, nil); }]]; return; } NSDictionary* exif = nil; if([[self.options objectForKey:@"includeExif"] boolValue]) { exif = [[CIImage imageWithData:imageData] properties]; } [selections addObject:[self createAttachmentResponse:filePath 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]: nil withCreationDate:phAsset.creationDate withModificationDate:phAsset.modificationDate ]]; } processed++; [lock unlock]; if (processed == [assets count]) { [indicatorView stopAnimating]; [overlayView removeFromSuperview]; [imagePickerController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.resolve(selections); }]]; return; } }); }]; } } }]; } else { PHAsset *phAsset = [assets objectAtIndex:0]; [self showActivityIndicator:^(UIActivityIndicatorView *indicatorView, UIView *overlayView) { if (phAsset.mediaType == PHAssetMediaTypeVideo) { [self getVideoAsset:phAsset completion:^(NSDictionary* video) { dispatch_async(dispatch_get_main_queue(), ^{ [indicatorView stopAnimating]; [overlayView removeFromSuperview]; [imagePickerController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ if (video != nil) { self.resolve(video); } else { self.reject(ERROR_CANNOT_PROCESS_VIDEO_KEY, ERROR_CANNOT_PROCESS_VIDEO_MSG, nil); } }]]; }); }]; } else { [manager requestImageDataForAsset:phAsset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, 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] withExif: exif withViewController:imagePickerController withSourceURL:[sourceURL absoluteString] withLocalIdentifier:phAsset.localIdentifier withFilename:[phAsset valueForKey:@"filename"] withCreationDate:phAsset.creationDate withModificationDate:phAsset.modificationDate]; }); }]; } }]; } } - (void)qb_imagePickerControllerDidCancel:(QBImagePickerController *)imagePickerController { [imagePickerController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.reject(ERROR_PICKER_CANCEL_KEY, ERROR_PICKER_CANCEL_MSG, nil); }]]; } // 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 withExif:(NSDictionary*) exif withViewController:(UIViewController*)viewController withSourceURL:(NSString*)sourceURL withLocalIdentifier:(NSString*)localIdentifier withFilename:(NSString*)filename withCreationDate:(NSDate*)creationDate withModificationDate:(NSDate*)modificationDate { if (image == nil) { [viewController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.reject(ERROR_PICKER_NO_DATA_KEY, ERROR_PICKER_NO_DATA_MSG, nil); }]]; return; } NSLog(@"id: %@ filename: %@", localIdentifier, filename); if ([[[self options] objectForKey:@"cropping"] boolValue]) { self.croppingFile = [[NSMutableDictionary alloc] init]; self.croppingFile[@"sourceURL"] = sourceURL; self.croppingFile[@"localIdentifier"] = localIdentifier; self.croppingFile[@"filename"] = filename; self.croppingFile[@"creationDate"] = creationDate; self.croppingFile[@"modifcationDate"] = modificationDate; NSLog(@"CroppingFile %@", self.croppingFile); [self startCropping:image]; } else { ImageResult *imageResult = [self.compression compressImage:image withOptions:self.options]; NSString *filePath = [self persistFile:imageResult.data]; if (filePath == nil) { [viewController dismissViewControllerAnimated:YES completion:[self waitAnimationEnd:^{ self.reject(ERROR_CANNOT_SAVE_IMAGE_KEY, ERROR_CANNOT_SAVE_IMAGE_MSG, nil); }]]; 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 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] : nil withCreationDate:creationDate withModificationDate:modificationDate]); }]]; } } #pragma mark - CustomCropModeDelegates // Returns a custom rect for the mask. - (CGRect)imageCropViewControllerCustomMaskRect: (RSKImageCropViewController *)controller { 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; } // if provided width or height is bigger than screen w/h, // then we should scale draw area - (CGRect) scaleRect:(RSKImageCropViewController *)controller { 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; } // Returns a custom path for the mask. - (UIBezierPath *)imageCropViewControllerCustomMaskPath: (RSKImageCropViewController *)controller { CGRect rect = [self scaleRect:controller]; UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(0, 0)]; return path; } // Returns a custom rect in which the image can be moved. - (CGRect)imageCropViewControllerCustomMovementRect: (RSKImageCropViewController *)controller { return [self scaleRect:controller]; } #pragma mark - CropFinishDelegate // Crop image has been canceled. - (void)imageCropViewControllerDidCancelCrop: (RSKImageCropViewController *)controller { [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 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; } } // The original image has been cropped. - (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 selectionDone:YES completion:[self waitAnimationEnd:^{ self.reject(ERROR_CANNOT_SAVE_IMAGE_KEY, ERROR_CANNOT_SAVE_IMAGE_MSG, nil); }]]; return; } 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"] 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] : nil withCreationDate:self.croppingFile[@"creationDate"] withModificationDate:self.croppingFile[@"modificationDate"]]); }]]; } // at the moment it is not possible to upload image by reading PHAsset // we are saving image and saving it to the tmp location where we are allowed to access image later - (NSString*) persistFile:(NSData*)data { // create temp file 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; } // The original image has been cropped. Additionally provides a rotation angle // used to produce image. - (void)imageCropViewController:(RSKImageCropViewController *)controller didCropImage:(UIImage *)croppedImage usingCropRect:(CGRect)cropRect rotationAngle:(CGFloat)rotationAngle { [self imageCropViewController:controller didCropImage:croppedImage usingCropRect:cropRect]; } @end