diff --git a/Examples/UIExplorer/ImageEditingExample.js b/Examples/UIExplorer/ImageEditingExample.js new file mode 100644 index 000000000..affd36796 --- /dev/null +++ b/Examples/UIExplorer/ImageEditingExample.js @@ -0,0 +1,305 @@ + +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * 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'; + +var React = require('react-native'); +var { + CameraRoll, + Image, + NativeModules, + ScrollView, + StyleSheet, + Text, + TouchableHighlight, + View, +} = React; +var ImageEditingManager = NativeModules.ImageEditingManager; +var RCTScrollViewConsts = NativeModules.UIManager.RCTScrollView.Constants; + +var PAGE_SIZE = 20; + +type ImageOffset = { + x: number; + y: number; +}; + +type ImageSize = { + width: number; + height: number; +}; + +type TransformData = { + offset: ImageOffset; + size: ImageSize; +} + +class SquareImageCropper extends React.Component { + _isMounted: boolean; + _transformData: TransformData; + + constructor(props) { + super(props); + this._isMounted = true; + this.state = { + randomPhoto: null, + measuredSize: null, + croppedImageURI: null, + cropError: null, + }; + this._fetchRandomPhoto(); + } + + _fetchRandomPhoto() { + CameraRoll.getPhotos( + {first: PAGE_SIZE}, + (data) => { + if (!this._isMounted) { + return; + } + var edges = data.edges; + var edge = edges[Math.floor(Math.random() * edges.length)]; + var randomPhoto = edge && edge.node && edge.node.image; + if (randomPhoto) { + this.setState({randomPhoto}); + } + }, + (error) => undefined + ); + } + + componentWillUnmount() { + this._isMounted = false; + } + + render() { + if (!this.state.measuredSize) { + return ( + { + var measuredWidth = event.nativeEvent.layout.width; + if (!measuredWidth) { + return; + } + this.setState({ + measuredSize: {width: measuredWidth, height: measuredWidth}, + }); + }} + /> + ); + } + + if (!this.state.croppedImageURI) { + return this._renderImageCropper(); + } + return this._renderCroppedImage(); + } + + _renderImageCropper() { + if (!this.state.randomPhoto) { + return ( + + ); + } + var error = null; + if (this.state.cropError) { + error = ( + {this.state.cropError.message} + ); + } + return ( + + Drag the image within the square to crop: + this._transformData = data} + /> + + + + Crop + + + + {error} + + ); + } + + _renderCroppedImage() { + return ( + + Here is the cropped image: + + + + + Try again + + + + + ); + } + + _crop() { + ImageEditingManager.cropImage( + this.state.randomPhoto.uri, + this._transformData, + (croppedImageURI) => this.setState({croppedImageURI}), + (cropError) => this.setState({cropError}) + ); + } + + _reset() { + this.setState({ + randomPhoto: null, + croppedImageURI: null, + cropError: null, + }); + this._fetchRandomPhoto(); + } + +} + +class ImageCropper extends React.Component { + _scaledImageSize: ImageSize; + _contentOffset: ImageOffset; + + componentWillMount() { + // Scale an image to the minimum size that is large enough to completely + // fill the crop box. + var widthRatio = this.props.image.width / this.props.size.width; + var heightRatio = this.props.image.height / this.props.size.height; + if (widthRatio < heightRatio) { + this._scaledImageSize = { + width: this.props.size.width, + height: this.props.image.height / widthRatio, + }; + } else { + this._scaledImageSize = { + width: this.props.image.width / heightRatio, + height: this.props.size.height, + }; + } + this._contentOffset = { + x: (this._scaledImageSize.width - this.props.size.width) / 2, + y: (this._scaledImageSize.height - this.props.size.height) / 2, + }; + this._updateTransformData( + this._contentOffset, + this._scaledImageSize, + this.props.size + ); + } + + _onScroll(event) { + this._updateTransformData( + event.nativeEvent.contentOffset, + event.nativeEvent.contentSize, + event.nativeEvent.layoutMeasurement + ); + } + + _updateTransformData(offset, scaledImageSize, croppedImageSize) { + var offsetRatioX = offset.x / scaledImageSize.width; + var offsetRatioY = offset.y / scaledImageSize.height; + var sizeRatioX = croppedImageSize.width / scaledImageSize.width; + var sizeRatioY = croppedImageSize.height / scaledImageSize.height; + + this.props.onTransformDataChange && this.props.onTransformDataChange({ + offset: { + x: this.props.image.width * offsetRatioX, + y: this.props.image.height * offsetRatioY, + }, + size: { + width: this.props.image.width * sizeRatioX, + height: this.props.image.height * sizeRatioY, + }, + }); + } + + render() { + var decelerationRate = + RCTScrollViewConsts && RCTScrollViewConsts.DecelerationRate ? + RCTScrollViewConsts.DecelerationRate.Fast : + 0; + + return ( + + + + ); + } + +} + +exports.framework = 'React'; +exports.title = 'ImageEditingManager'; +exports.description = 'Cropping and scaling with ImageEditingManager'; +exports.examples = [{ + title: 'Image Cropping', + render() { + return ; + } +}]; + +var styles = StyleSheet.create({ + container: { + flex: 1, + alignSelf: 'stretch', + }, + imageCropper: { + alignSelf: 'center', + marginTop: 12, + }, + cropButtonTouchable: { + alignSelf: 'center', + marginTop: 12, + }, + cropButton: { + padding: 12, + backgroundColor: 'blue', + borderRadius: 4, + }, + cropButtonLabel: { + color: 'white', + fontSize: 16, + fontWeight: '500', + }, +}); diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index d76a19936..25f69684d 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -75,6 +75,7 @@ var APIS = [ require('./TimerExample'), require('./VibrationIOSExample'), require('./XHRExample'), + require('./ImageEditingExample'), ]; // Register suitable examples for snapshot tests diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js deleted file mode 100644 index cbb24a126..000000000 --- a/Examples/UIExplorer/UIExplorerList.js +++ /dev/null @@ -1,344 +0,0 @@ -/** - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * 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'; - -var React = require('react-native'); -var { - AppRegistry, - ListView, - PixelRatio, - Platform, - Settings, - StyleSheet, - Text, - TextInput, - TouchableHighlight, - View, -} = React; - -var { TestModule } = React.addons; - -import type { ExampleModule } from 'ExampleTypes'; -import type { NavigationContext } from 'NavigationContext'; - -var createExamplePage = require('./createExamplePage'); - -var COMMON_COMPONENTS = [ - require('./ImageExample'), - require('./LayoutEventsExample'), - require('./ListViewExample'), - require('./ListViewGridLayoutExample'), - require('./ListViewPagingExample'), - require('./MapViewExample'), - require('./Navigator/NavigatorExample'), - require('./ScrollViewExample'), - require('./TextInputExample'), - require('./TouchableExample'), - require('./ViewExample'), - require('./WebViewExample'), -]; - -var COMMON_APIS = [ - require('./AnimationExample/AnExApp'), - require('./GeolocationExample'), - require('./LayoutExample'), - require('./PanResponderExample'), - require('./PointerEventsExample'), -]; - -if (Platform.OS === 'ios') { - var COMPONENTS = COMMON_COMPONENTS.concat([ - require('./ActivityIndicatorIOSExample'), - require('./DatePickerIOSExample'), - require('./NavigatorIOSColorsExample'), - require('./NavigatorIOSExample'), - require('./PickerIOSExample'), - require('./ProgressViewIOSExample'), - require('./SegmentedControlIOSExample'), - require('./SliderIOSExample'), - require('./SwitchIOSExample'), - require('./TabBarIOSExample'), - require('./TextExample.ios'), - ]); - - var APIS = COMMON_APIS.concat([ - require('./AccessibilityIOSExample'), - require('./ActionSheetIOSExample'), - require('./AdSupportIOSExample'), - require('./AlertIOSExample'), - require('./AppStateIOSExample'), - require('./AsyncStorageExample'), - require('./TransformExample'), - require('./BorderExample'), - require('./CameraRollExample.ios'), - require('./NetInfoExample'), - require('./PushNotificationIOSExample'), - require('./StatusBarIOSExample'), - require('./TimerExample'), - require('./VibrationIOSExample'), - require('./XHRExample'), - ]); - -} else if (Platform.OS === 'android') { - var COMPONENTS = COMMON_COMPONENTS.concat([ - ]); - - var APIS = COMMON_APIS.concat([ - ]); -} - -var ds = new ListView.DataSource({ - rowHasChanged: (r1, r2) => r1 !== r2, - sectionHeaderHasChanged: (h1, h2) => h1 !== h2, -}); - -function makeRenderable(example: any): ReactClass { - return example.examples ? - createExamplePage(null, example) : - example; -} - -// Register suitable examples for snapshot tests -COMPONENTS.concat(APIS).forEach((Example) => { - if (Example.displayName) { - var Snapshotter = React.createClass({ - componentDidMount: function() { - // View is still blank after first RAF :\ - global.requestAnimationFrame(() => - global.requestAnimationFrame(() => TestModule.verifySnapshot( - TestModule.markTestPassed - ) - )); - }, - render: function() { - var Renderable = makeRenderable(Example); - return ; - }, - }); - AppRegistry.registerComponent(Example.displayName, () => Snapshotter); - } -}); - -type Props = { - navigator: { - navigationContext: NavigationContext, - push: (route: {title: string, component: ReactClass}) => void, - }, - onExternalExampleRequested: Function, - onSelectExample: Function, - isInDrawer: bool, -}; - -class UIExplorerList extends React.Component { - props: Props; - - constructor(props: Props) { - super(props); - this.state = { - dataSource: ds.cloneWithRowsAndSections({ - components: COMPONENTS, - apis: APIS, - }), - searchText: Platform.OS === 'ios' ? Settings.get('searchText') : '', - }; - } - - componentWillMount() { - this.props.navigator.navigationContext.addListener('didfocus', function(event) { - if (event.data.route.title === 'UIExplorer') { - Settings.set({visibleExample: null}); - } - }); - } - - componentDidMount() { - this._search(this.state.searchText); - } - - render() { - if (Platform.OS === 'ios' || - (Platform.OS === 'android' && !this.props.isInDrawer)) { - var platformTextInputStyle = - Platform.OS === 'ios' ? styles.searchTextInputIOS : - Platform.OS === 'android' ? styles.searchTextInputAndroid : {}; - var textInput = ( - - - ); - } - - var homePage; - if (Platform.OS === 'android' && this.props.isInDrawer) { - homePage = this._renderRow({ - title: 'UIExplorer', - description: 'List of examples', - }, -1); - } - - return ( - - {textInput} - {homePage} - - - ); - } - - _renderSectionHeader(data: any, section: string) { - return ( - - - {section.toUpperCase()} - - - ); - } - - _renderRow(example: any, i: number) { - return ( - - this._onPressRow(example)}> - - - {example.title} - - - {example.description} - - - - - - ); - } - - _search(text: mixed) { - var regex = new RegExp(text, 'i'); - var filter = (component) => regex.test(component.title); - - this.setState({ - dataSource: ds.cloneWithRowsAndSections({ - components: COMPONENTS.filter(filter), - apis: APIS.filter(filter), - }), - searchText: text, - }); - Settings.set({searchText: text}); - } - - _openExample(example: any) { - if (example.external) { - this.props.onExternalExampleRequested(example); - return; - } - - var Component = makeRenderable(example); - if (Platform.OS === 'ios') { - this.props.navigator.push({ - title: Component.title, - component: Component, - }); - } else if (Platform.OS === 'android') { - this.props.onSelectExample({ - title: Component.title, - component: Component, - }); - } - } - - _onPressRow(example: any) { - Settings.set({visibleExample: example.title}); - this._openExample(example); - } -} - -var styles = StyleSheet.create({ - listContainer: { - flex: 1, - }, - list: { - backgroundColor: '#eeeeee', - }, - sectionHeader: { - padding: 5, - }, - group: { - backgroundColor: 'white', - }, - sectionHeaderTitle: { - fontWeight: '500', - fontSize: 11, - }, - row: { - backgroundColor: 'white', - justifyContent: 'center', - paddingHorizontal: 15, - paddingVertical: 8, - }, - separator: { - height: 1 / PixelRatio.get(), - backgroundColor: '#bbbbbb', - marginLeft: 15, - }, - rowTitleText: { - fontSize: 17, - fontWeight: '500', - }, - rowDetailText: { - fontSize: 15, - color: '#888888', - lineHeight: 20, - }, - searchRow: { - backgroundColor: '#eeeeee', - paddingTop: 75, - paddingLeft: 10, - paddingRight: 10, - paddingBottom: 10, - }, - searchTextInput: { - backgroundColor: 'white', - borderColor: '#cccccc', - borderRadius: 3, - borderWidth: 1, - paddingLeft: 8, - }, - searchTextInputIOS: { - height: 30, - }, - searchTextInputAndroid: { - padding: 2, - }, -}); - -module.exports = UIExplorerList; diff --git a/Examples/UIExplorer/XHRExample.js b/Examples/UIExplorer/XHRExample.js index d142222c9..250061f1f 100644 --- a/Examples/UIExplorer/XHRExample.js +++ b/Examples/UIExplorer/XHRExample.js @@ -144,7 +144,6 @@ class FormUploader extends React.Component { CameraRoll.getPhotos( {first: PAGE_SIZE}, (data) => { - console.log('isMounted', this._isMounted); if (!this._isMounted) { return; } diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index e0c8d0468..cbc8c58ee 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; }; + 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; }; 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; }; /* End PBXBuildFile section */ @@ -50,6 +51,8 @@ 143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = ""; }; 35123E691B59C99D00EBAD80 /* RCTImageStoreManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageStoreManager.h; sourceTree = ""; }; 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageStoreManager.m; sourceTree = ""; }; + 354631661B69857700AA0B86 /* RCTImageEditingManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageEditingManager.h; sourceTree = ""; }; + 354631671B69857700AA0B86 /* RCTImageEditingManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageEditingManager.m; sourceTree = ""; }; 58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; }; 58B511891A9E6BD600147676 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = ""; }; 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = ""; }; @@ -75,6 +78,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */, 58B511891A9E6BD600147676 /* RCTImageDownloader.h */, 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */, + 354631661B69857700AA0B86 /* RCTImageEditingManager.h */, + 354631671B69857700AA0B86 /* RCTImageEditingManager.m */, 143879361AAD32A300F088A5 /* RCTImageLoader.h */, 143879371AAD32A300F088A5 /* RCTImageLoader.m */, 137620331B31C53500677FF0 /* RCTImagePickerManager.h */, @@ -167,6 +172,7 @@ 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */, 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */, 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, + 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */, ); diff --git a/Libraries/Image/RCTImageEditingManager.h b/Libraries/Image/RCTImageEditingManager.h new file mode 100644 index 000000000..1f45ff95f --- /dev/null +++ b/Libraries/Image/RCTImageEditingManager.h @@ -0,0 +1,14 @@ +/** + * 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. + */ + +#import "RCTBridgeModule.h" + +@interface RCTImageEditingManager : NSObject + +@end diff --git a/Libraries/Image/RCTImageEditingManager.m b/Libraries/Image/RCTImageEditingManager.m new file mode 100644 index 000000000..cd81bbfbe --- /dev/null +++ b/Libraries/Image/RCTImageEditingManager.m @@ -0,0 +1,133 @@ +/** + * 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. + */ + +#import "RCTImageEditingManager.h" + +#import + +#import "RCTConvert.h" +#import "RCTLog.h" +#import "RCTUtils.h" + +#import "RCTImageStoreManager.h" +#import "RCTImageLoader.h" + +@implementation RCTImageEditingManager + +RCT_EXPORT_MODULE() + +@synthesize bridge = _bridge; + +/** + * Crops an image and adds the result to the image store. + * + * @param imageTag A URL, a string identifying an asset etc. + * @param cropData Dictionary with `offset`, `size` and `displaySize`. + * `offset` and `size` are relative to the full-resolution image size. + * `displaySize` is an optimization - if specified, the image will + * be scaled down to `displaySize` rather than `size`. + * All units are in px (not points). + */ +RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag + cropData:(NSDictionary *)cropData + successCallback:(RCTResponseSenderBlock)successCallback + errorCallback:(RCTResponseErrorBlock)errorCallback) +{ + NSDictionary *offset = cropData[@"offset"]; + NSDictionary *size = cropData[@"size"]; + NSDictionary *displaySize = cropData[@"displaySize"]; + NSString *resizeMode = cropData[@"resizeMode"] ?: @"contain"; + + if (!offset[@"x"] || !offset[@"y"] || !size[@"width"] || !size[@"height"]) { + NSString *errorMessage = [NSString stringWithFormat:@"Invalid cropData: %@", cropData]; + RCTLogError(@"%@", errorMessage); + errorCallback(RCTErrorWithMessage(errorMessage)); + return; + } + + [_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}; + UIGraphicsBeginImageContextWithOptions(rect.size, !RCTImageHasAlpha(image.CGImage), image.scale); + [image drawInRect:rectToDrawIn]; + UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + if (displaySize && displaySize[@"width"] && displaySize[@"height"]) { + CGSize targetSize = [RCTConvert CGSize:displaySize]; + croppedImage = [self scaleImage:croppedImage targetSize:targetSize resizeMode:resizeMode]; + } + + [_bridge.imageStoreManager storeImage:croppedImage withBlock:^(NSString *croppedImageTag) { + if (!croppedImageTag) { + NSString *errorMessage = @"Error storing cropped image in RCTImageStoreManager"; + RCTLogWarn(@"%@", errorMessage); + errorCallback(RCTErrorWithMessage(errorMessage)); + return; + } + successCallback(@[croppedImageTag]); + }]; + }]; +} + +- (UIImage *)scaleImage:(UIImage *)image targetSize:(CGSize)targetSize resizeMode:(NSString *)resizeMode +{ + if (CGSizeEqualToSize(image.size, targetSize)) { + return image; + } + + CGFloat imageRatio = image.size.width / image.size.height; + CGFloat targetRatio = targetSize.width / targetSize.height; + + CGFloat newWidth = targetSize.width; + CGFloat newHeight = targetSize.height; + + // contain vs cover + // http://blog.vjeux.com/2013/image/css-container-and-cover.html + if ([resizeMode isEqualToString:@"contain"]) { + if (imageRatio <= targetRatio) { + newWidth = targetSize.height * imageRatio; + newHeight = targetSize.height; + } else { + newWidth = targetSize.width; + newHeight = targetSize.width / imageRatio; + } + } else if ([resizeMode isEqualToString:@"cover"]) { + if (imageRatio <= targetRatio) { + newWidth = targetSize.width; + newHeight = targetSize.width / imageRatio; + } else { + newWidth = targetSize.height * imageRatio; + newHeight = targetSize.height; + } + } // else assume we're stretching the image + + // prevent upscaling + newWidth = MIN(newWidth, image.size.width); + newHeight = MIN(newHeight, image.size.height); + + // perform the scaling @1x because targetSize is in actual pixel width/height + UIGraphicsBeginImageContextWithOptions(targetSize, NO, 1.0f); + [image drawInRect:CGRectMake(0.f, 0.f, newWidth, newHeight)]; + UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return scaledImage; +} + +@end diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 1ab2bae4f..338475029 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -61,7 +61,7 @@ RCT_EXPORT_MODULE() { return [self loadImageWithTag:imageTag size:CGSizeZero - scale:0 + scale:1 resizeMode:UIViewContentModeScaleToFill progressBlock:nil completionBlock:callback];