From 83c2e0303b108371b9fa2d41180bce8da9e1dd5c Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 22 Dec 2015 13:36:37 -0800 Subject: [PATCH] Added JS wrappers for ImageStore and ImageEditor Summary: public Added JS wrappers for ImageStore(Manager) and ImageEditor(Manager) so they can be required in the normal way instead of accessed directly via NativeModules. Reviewed By: dmmiller Differential Revision: D2773822 fb-gh-sync-id: 6eeafd3f80a87b1b91a04a2aebad6e2fd31b0e98 --- Examples/UIExplorer/ImageEditingExample.js | 22 +++--- Examples/UIExplorer/TransformExample.js | 1 + Libraries/Image/ImageEditor.js | 72 ++++++++++++++++++ Libraries/Image/ImageStore.js | 83 +++++++++++++++++++++ Libraries/Image/RCTImageEditingManager.m | 23 ++---- Libraries/Image/RCTImageStoreManager.m | 6 ++ Libraries/react-native/react-native.js | 2 + Libraries/react-native/react-native.js.flow | 2 + 8 files changed, 186 insertions(+), 25 deletions(-) create mode 100644 Libraries/Image/ImageEditor.js create mode 100644 Libraries/Image/ImageStore.js diff --git a/Examples/UIExplorer/ImageEditingExample.js b/Examples/UIExplorer/ImageEditingExample.js index 56ab1c4ba..29b2dfd84 100644 --- a/Examples/UIExplorer/ImageEditingExample.js +++ b/Examples/UIExplorer/ImageEditingExample.js @@ -20,6 +20,7 @@ var React = require('react-native'); var { CameraRoll, Image, + ImageEditor, NativeModules, ScrollView, StyleSheet, @@ -28,7 +29,7 @@ var { UIManager, View, } = React; -var ImageEditingManager = NativeModules.ImageEditingManager; + var RCTScrollViewConsts = UIManager.RCTScrollView.Constants; var PAGE_SIZE = 20; @@ -43,14 +44,16 @@ type ImageSize = { height: number; }; -type TransformData = { +type ImageCropData = { offset: ImageOffset; size: ImageSize; -} + displaySize?: ?ImageSize; + resizeMode?: ?any; +}; class SquareImageCropper extends React.Component { _isMounted: boolean; - _transformData: TransformData; + _transformData: ImageCropData; constructor(props) { super(props); @@ -167,7 +170,7 @@ class SquareImageCropper extends React.Component { } _crop() { - ImageEditingManager.cropImage( + ImageEditor.cropImage( this.state.randomPhoto.uri, this._transformData, (croppedImageURI) => this.setState({croppedImageURI}), @@ -231,7 +234,7 @@ class ImageCropper extends React.Component { var sizeRatioX = croppedImageSize.width / scaledImageSize.width; var sizeRatioY = croppedImageSize.height / scaledImageSize.height; - this.props.onTransformDataChange && this.props.onTransformDataChange({ + var cropData: ImageCropData = { offset: { x: this.props.image.width * offsetRatioX, y: this.props.image.height * offsetRatioY, @@ -240,7 +243,8 @@ class ImageCropper extends React.Component { width: this.props.image.width * sizeRatioX, height: this.props.image.height * sizeRatioY, }, - }); + }; + this.props.onTransformDataChange && this.props.onTransformDataChange(cropData); } render() { @@ -271,8 +275,8 @@ class ImageCropper extends React.Component { } exports.framework = 'React'; -exports.title = 'ImageEditingManager'; -exports.description = 'Cropping and scaling with ImageEditingManager'; +exports.title = 'ImageEditor'; +exports.description = 'Cropping and scaling with ImageEditor'; exports.examples = [{ title: 'Image Cropping', render() { diff --git a/Examples/UIExplorer/TransformExample.js b/Examples/UIExplorer/TransformExample.js index 7da9eb5ac..01a2813d4 100644 --- a/Examples/UIExplorer/TransformExample.js +++ b/Examples/UIExplorer/TransformExample.js @@ -10,6 +10,7 @@ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * @flow */ 'use strict'; diff --git a/Libraries/Image/ImageEditor.js b/Libraries/Image/ImageEditor.js new file mode 100644 index 000000000..4ea15349d --- /dev/null +++ b/Libraries/Image/ImageEditor.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ImageEditor + * @flow + */ +'use strict'; + +const RCTImageEditingManager = require('NativeModules').ImageEditingManager; + +type ImageCropData = { + /** + * The top-left corner of the cropped image, specified in the original + * image's coordinate space. + */ + offset: { + x: number; + y: number; + }; + /** + * The size (dimensions) of the cropped image, specified in the original + * image's coordinate space. + */ + size: { + width: number; + height: number; + }; + /** + * (Optional) size to scale the cropped image to. + */ + displaySize?: ?{ + width: number; + height: number; + }; + /** + * (Optional) the resizing mode to use when scaling the image. If the + * `displaySize` param is not specified, this has no effect. + */ + resizeMode?: ?$Enum<{ + contain: string; + cover: string; + stretch: string; + }>; +}; + +class ImageEditor { + /** + * Crop the image specified by the URI param. If URI points to a remote + * image, it will be downloaded automatically. If the image cannot be + * loaded/downloaded, the failure callback will be called. + * + * If the cropping process is successful, the resultant cropped image + * will be stored in the ImageStore, and the URI returned in the success + * callback will point to the image in the store. Remember to delete the + * cropped image from the ImageStore when you are done with it. + */ + static cropImage( + uri: string, + cropData: ImageCropData, + success: (uri: string) => void, + failure: (error: Object) => void + ) { + RCTImageEditingManager.cropImage(uri, cropData, success, failure); + } +} + +module.exports = ImageEditor; diff --git a/Libraries/Image/ImageStore.js b/Libraries/Image/ImageStore.js new file mode 100644 index 000000000..e4006a73d --- /dev/null +++ b/Libraries/Image/ImageStore.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ImageStore + * @flow + */ +'use strict'; + +const RCTImageStoreManager = require('NativeModules').ImageStoreManager; + +class ImageStore { + /** + * Check if the ImageStore contains image data for the specified URI. + * @platform ios + */ + static hasImageForTag(uri: string, callback: (hasImage: bool) => void) { + if (RCTImageStoreManager.hasImageForTag) { + RCTImageStoreManager.hasImageForTag(uri, callback); + } else { + console.warn('hasImageForTag() not implemented'); + } + } + + /** + * Delete an image from the ImageStore. Images are stored in memory and + * must be manually removed when you are finished with them, otherwise they + * will continue to use up RAM until the app is terminated. It is safe to + * call `removeImageForTag()` without first calling `hasImageForTag()`, it + * will simply fail silently. + * @platform ios + */ + static removeImageForTag(uri: string) { + if (RCTImageStoreManager.removeImageForTag) { + RCTImageStoreManager.removeImageForTag(uri); + } else { + console.warn('removeImageForTag() not implemented'); + } + } + + /** + * Stores a base64-encoded image in the ImageStore, and returns a URI that + * can be used to access or display the image later. Images are stored in + * memory only, and must be manually deleted when you are finished with + * them by calling `removeImageForTag()`. + * + * Note that it is very inefficient to transfer large quantities of binary + * data between JS and native code, so you should avoid calling this more + * than necessary. + */ + static addImageFromBase64( + base64ImageData: string, + success: (uri: string) => void, + failure: (error: any) => void + ) { + RCTImageStoreManager.addImageFromBase64(base64ImageData, success, failure); + } + + /** + * Retrieves the base64-encoded data for an image in the ImageStore. If the + * specified URI does not match an image in the store, the failure callback + * will be called. + * + * Note that it is very inefficient to transfer large quantities of binary + * data between JS and native code, so you should avoid calling this more + * than necessary. To display an image in the ImageStore, you can just pass + * the URI to an `` component; there is no need to retrieve the + * base64 data. + */ + static getBase64ForTag( + uri: string, + success: (base64ImageData: string) => void, + failure: (error: any) => void + ) { + RCTImageStoreManager.getBase64ForTag(uri, success, failure); + } +} + +module.exports = ImageStore; \ No newline at end of file diff --git a/Libraries/Image/RCTImageEditingManager.m b/Libraries/Image/RCTImageEditingManager.m index cd81bbfbe..76fbc2703 100644 --- a/Libraries/Image/RCTImageEditingManager.m +++ b/Libraries/Image/RCTImageEditingManager.m @@ -39,27 +39,18 @@ RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseErrorBlock)errorCallback) { - NSDictionary *offset = cropData[@"offset"]; - NSDictionary *size = cropData[@"size"]; - NSDictionary *displaySize = cropData[@"displaySize"]; - NSString *resizeMode = cropData[@"resizeMode"] ?: @"contain"; + CGRect rect = { + [RCTConvert CGPoint:cropData[@"offset"]], + [RCTConvert CGSize:cropData[@"size"]] + }; - if (!offset[@"x"] || !offset[@"y"] || !size[@"width"] || !size[@"height"]) { - NSString *errorMessage = [NSString stringWithFormat:@"Invalid cropData: %@", cropData]; - RCTLogError(@"%@", errorMessage); - errorCallback(RCTErrorWithMessage(errorMessage)); - return; - } + NSString *resizeMode = cropData[@"resizeMode"] ?: @"contain"; [_bridge.imageLoader loadImageWithTag:imageTag callback:^(NSError *error, UIImage *image) { if (error) { errorCallback(error); return; } - CGRect rect = (CGRect){ - [RCTConvert CGPoint:offset], - [RCTConvert CGSize:size] - }; // Crop image CGRect rectToDrawIn = {{-rect.origin.x, -rect.origin.y}, image.size}; @@ -68,8 +59,8 @@ RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - if (displaySize && displaySize[@"width"] && displaySize[@"height"]) { - CGSize targetSize = [RCTConvert CGSize:displaySize]; + if (cropData[@"displaySize"]) { + CGSize targetSize = [RCTConvert CGSize:cropData[@"displaySize"]]; croppedImage = [self scaleImage:croppedImage targetSize:targetSize resizeMode:resizeMode]; } diff --git a/Libraries/Image/RCTImageStoreManager.m b/Libraries/Image/RCTImageStoreManager.m index 3bda2df35..1827bd285 100644 --- a/Libraries/Image/RCTImageStoreManager.m +++ b/Libraries/Image/RCTImageStoreManager.m @@ -86,6 +86,12 @@ RCT_EXPORT_METHOD(removeImageForTag:(NSString *)imageTag) [_store removeObjectForKey:imageTag]; } +RCT_EXPORT_METHOD(hasImageForTag:(NSString *)imageTag + callback:(RCTResponseSenderBlock)callback) +{ + callback(@[@(_store[imageTag] != nil)]); +} + // TODO (#5906496): Name could be more explicit - something like getBase64EncodedDataForTag:? RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag successCallback:(RCTResponseSenderBlock)successCallback diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 68876a712..f2313318b 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -18,6 +18,8 @@ var ReactNative = { get DatePickerIOS() { return require('DatePickerIOS'); }, get DrawerLayoutAndroid() { return require('DrawerLayoutAndroid'); }, get Image() { return require('Image'); }, + get ImageEditor() { return require('ImageEditor'); }, + get ImageStore() { return require('ImageStore'); }, get ListView() { return require('ListView'); }, get MapView() { return require('MapView'); }, get Modal() { return require('Modal'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index c3d841e5a..bc7d05513 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -30,6 +30,8 @@ var ReactNative = Object.assign(Object.create(require('React')), { DatePickerIOS: require('DatePickerIOS'), DrawerLayoutAndroid: require('DrawerLayoutAndroid'), Image: require('Image'), + ImageEditor: require('ImageEditor'), + ImageStore: require('ImageStore'), ListView: require('ListView'), MapView: require('MapView'), Modal: require('Modal'),