diff --git a/Examples/UIExplorer/AssetThumbnailExample.ios.js b/Examples/UIExplorer/AssetThumbnailExample.ios.js new file mode 100644 index 00000000000000..f561978f98ca80 --- /dev/null +++ b/Examples/UIExplorer/AssetThumbnailExample.ios.js @@ -0,0 +1,132 @@ +/** + * 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 { + Image, + StyleSheet, + Text, + View, + ScrollView +} = React; + +var AssetThumbnailExampleView = React.createClass({ + + getInitialState() { + return { + asset: this.props.asset + }; + }, + + render() { + var asset = this.state.asset; + return ( + + + + + + + + + + + + + + + + + ); + }, + + +}); + +var styles = StyleSheet.create({ + row: { + padding: 10, + flex: 1, + flexDirection: 'row', + }, + + details: { + margin: 5 + }, + + textColumn: { + flex: 1, + flexDirection: 'column' + }, + + image1: { + borderWidth: 1, + borderColor: 'black', + width: 240, + height: 320, + margin: 5, + }, + + image2: { + borderWidth: 1, + borderColor: 'black', + width: 320, + height: 240 + }, + + image3: { + borderWidth: 1, + borderColor: 'black', + width: 100, + height: 100 + }, + + image4: { + borderWidth: 1, + borderColor: 'black', + width: 200, + height: 200 + }, + + image5: { + borderWidth: 1, + borderColor: 'black', + width: 355, + height: 100 + }, + + image6: { + borderWidth: 1, + borderColor: 'black', + width: 355, + height: 355 + }, + +}); + +exports.title = ''; +exports.description = 'Example component that displays the thumbnail capabilities of the tag'; +module.exports = AssetThumbnailExampleView; \ No newline at end of file diff --git a/Examples/UIExplorer/CameraRollExample.ios.js b/Examples/UIExplorer/CameraRollExample.ios.js index 73678407213224..41cb692c927495 100644 --- a/Examples/UIExplorer/CameraRollExample.ios.js +++ b/Examples/UIExplorer/CameraRollExample.ios.js @@ -22,11 +22,13 @@ var { SliderIOS, StyleSheet, SwitchIOS, + TouchableOpacity, Text, View, } = React; var CameraRollView = require('./CameraRollView.ios'); +var AssetThumbnailExampleView = require('./AssetThumbnailExample.ios'); var CAMERA_ROLL_VIEW = 'camera_roll_view'; @@ -61,6 +63,15 @@ var CameraRollExample = React.createClass({ ); }, + + loadAsset(asset){ + this.props.navigator.push({ + title: "Thumbnails", + component: AssetThumbnailExampleView, + backButtonTitle: 'Back', + passProps: { asset: asset }, + }); + }, _renderImage(asset) { var imageSize = this.state.bigImages ? 150 : 75; @@ -68,18 +79,20 @@ var CameraRollExample = React.createClass({ var location = asset.node.location.longitude ? JSON.stringify(asset.node.location) : 'Unknown location'; return ( - - - - {asset.node.image.uri} - {location} - {asset.node.group_name} - {new Date(asset.node.timestamp).toString()} + + + + + {asset.node.image.uri} + {location} + {asset.node.group_name} + {new Date(asset.node.timestamp).toString()} + - + ); }, diff --git a/Examples/UIExplorer/createExamplePage.js b/Examples/UIExplorer/createExamplePage.js index 3d5a1ac88c4c98..352f84a4c4adec 100644 --- a/Examples/UIExplorer/createExamplePage.js +++ b/Examples/UIExplorer/createExamplePage.js @@ -55,6 +55,7 @@ var createExamplePage = function(title: ?string, exampleModule: ExampleModule) var result = example.render(null); if (result) { renderedComponent = result; + result.props.navigator = this.props.navigator; } (React: Object).render = originalRender; (React: Object).renderComponent = originalRenderComponent; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 7a629ce9a60c48..20ceed1c030108 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -49,6 +49,12 @@ var warning = require('warning'); * style={styles.logo} * source={{uri: 'http://facebook.github.io/react/img/logo_og.png'}} * /> + * * * ); * }, @@ -61,9 +67,41 @@ var Image = React.createClass({ * `uri` is a string representing the resource identifier for the image, which * could be an http address, a local file path, or the name of a static image * resource (which should be wrapped in the `require('image!name')` function). + * + * `options` supports the following optional properties: + * + * Photos Framework and Asset Library: + * ----------------------------------- + * assetUseMaximumSize (boolean): Boolean value indicating whether to use the full + * resolution asset. Defaults to false. + * + * If false, the image will be automatically sized based + * on the target dimensions specified in the style property of the + * tag (width, height). + * + * If true, the full resolution asset will be used. + * This can result in substantial memory usage and potential crashes, + * especially when rendering many images in sequence. Consider that + * an 8MP photo taken with an iPhone6 will require 32MB of memory to + * display in full resolution (3264x2448). + * + * + * Photos Framework only (assets with uri matching ph://...): + * ---------------------------------------------------------- + * contentMode (string): Content mode used when requesting images using the Photos Framework. + * `fit` (PHImageContentModeAspectFit) default + * `fill` (PHImageContentModeAspectFill) + * + * renderMode (string): Render mode used when reqeusting images using the Photos Framework. + * `fast` (PHImageRequestOptionsResizeModeFast) default + * `exact` (PHImageRequestOptionsResizeModeFast) + * `none` (PHImageRequestOptionsResizeModeNone) + * + * */ source: PropTypes.shape({ uri: PropTypes.string, + assetOptions: PropTypes.object, }), /** * A static image to display while downloading the final image off the @@ -154,7 +192,19 @@ var Image = React.createClass({ tintColor: style.tintColor, }); if (isStored) { - nativeProps.imageTag = source.uri; + var options = { + // iOS specific asset options + assetResizeMode: 'fast', + assetContentMode: 'fill', + assetTargetSize: { width: style.width, height: style.height }, + assetUseMaximumSize: false + }; + + Object.assign( options, this.props.source.options ); + + nativeProps.imageTag = { uri: source.uri, + options: options }; + } else { nativeProps.src = source.uri; } diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m index d7b42f88560aba..5fd7148de93193 100644 --- a/Libraries/Image/RCTCameraRollManager.m +++ b/Libraries/Image/RCTCameraRollManager.m @@ -25,7 +25,7 @@ @implementation RCTCameraRollManager successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseSenderBlock)errorCallback) { - [RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) { + [RCTImageLoader loadImageWithTag:imageTag options:@{} callback:^(NSError *loadError, UIImage *loadedImage) { if (loadError) { errorCallback(@[[loadError localizedDescription]]); return; diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index 186a53cd1046b0..a7ecb0208c837b 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -21,6 +21,7 @@ * Will always call callback on main thread. */ + (void)loadImageWithTag:(NSString *)tag + options:(NSDictionary *)options callback:(void (^)(NSError *error, id /* UIImage or CAAnimation */ image))callback; @end diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 04fa17f5d4b3d1..aeef3e2bc223e5 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -56,11 +56,44 @@ + (ALAssetsLibrary *)assetsLibrary return assetsLibrary; } ++(CGImageRef )scaledImageRefForAssetRepresentation:(ALAssetRepresentation *)assetRepresentation maxPixelSize:(float)maxPixelSize +{ + NSData *data = nil; + CGImageRef imageRef = nil; + + uint8_t *buffer = (uint8_t *)malloc(sizeof(uint8_t)*[assetRepresentation size]); + if (buffer != NULL) { + NSError *error = nil; + NSUInteger bytesRead = [assetRepresentation getBytes:buffer fromOffset:0 length:[assetRepresentation size] error:&error]; + data = [NSData dataWithBytes:buffer length:bytesRead]; + + free(buffer); + } + + if ([data length]){ + CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil); + + NSMutableDictionary *options = [NSMutableDictionary dictionary]; + + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceShouldAllowFloat]; + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailWithTransform]; + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailFromImageAlways]; + [options setObject:(id)[NSNumber numberWithFloat:maxPixelSize] forKey:(id)kCGImageSourceThumbnailMaxPixelSize]; + imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options); + + if (sourceRef){ + CFRelease(sourceRef); + } + } + + return imageRef; +} + /** * Can be called from any thread. * Will always call callback on main thread. */ -+ (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, id image))callback ++ (void)loadImageWithTag:(NSString *)imageTag options:(NSDictionary *)options callback:(void (^)(NSError *error, id image))callback { if ([imageTag hasPrefix:@"assets-library"]) { [[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) { @@ -75,7 +108,38 @@ + (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, @autoreleasepool { ALAssetRepresentation *representation = [asset defaultRepresentation]; ALAssetOrientation orientation = [representation orientation]; - UIImage *image = [UIImage imageWithCGImage:[representation fullResolutionImage] scale:1.0f orientation:(UIImageOrientation)orientation]; + + CGImageRef ref = nil; + UIImage *image = nil; + + if ([options[@"assetUseMaximumSize"] boolValue]) { // Full resolution + ref = [[asset defaultRepresentation] fullResolutionImage]; + image = [UIImage imageWithCGImage:ref scale:[representation scale] orientation:(UIImageOrientation)orientation]; + + } else { + CGFloat retinaScale = [UIScreen mainScreen].scale; + + CGFloat targetWidth = [options[@"assetTargetSize"][@"width"] floatValue]; + CGFloat targetHeight = [options[@"assetTargetSize"][@"height"] floatValue]; + + CGFloat fullWidth = [representation dimensions].width; + CGFloat fullHeight = [representation dimensions].height; + CGFloat fullAspectRatio = fullWidth / fullHeight; + + CGFloat maxPixelSize; + + if (fullWidth > fullHeight) { + maxPixelSize = ceil((fullAspectRatio * targetHeight) * retinaScale); + } else { + maxPixelSize = ceil((targetWidth / fullAspectRatio) * retinaScale); + } + + ref = [self scaledImageRefForAssetRepresentation:representation maxPixelSize:maxPixelSize]; + image = [UIImage imageWithCGImage:ref]; + } + + //RCTLogInfo(@"[%@] Full size: (%f, %f) Container: (%f, %f), Scale: %f, UIImage: (%f, %f), Memory=%.2fkb", [options[@"assetUseMaximumSize"] boolValue] ? @"Maximum" : @"Scaled", [representation dimensions].width, [representation dimensions].height, [options[@"assetTargetSize"][@"width"] floatValue], [options[@"assetTargetSize"][@"height"] floatValue], [UIScreen mainScreen].scale, image.size.width, image.size.height, (CGImageGetHeight(image.CGImage) * CGImageGetBytesPerRow(image.CGImage)) / 1024.0 ); + RCTDispatchCallbackOnMainQueue(callback, nil, image); } }); @@ -102,9 +166,42 @@ + (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, RCTDispatchCallbackOnMainQueue(callback, error, nil); return; } - + PHAsset *asset = [results firstObject]; - [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:nil resultHandler:^(UIImage *result, NSDictionary *info) { + PHImageRequestOptions *imageOptions = [[PHImageRequestOptions alloc] init]; + + // Resize Mode (default: PHImageRequestOptionsResizeModeFast) + if ([options[@"resizeMode"] isEqualToString:@"fast"]) { + imageOptions.resizeMode = PHImageRequestOptionsResizeModeFast; + } else if ([options[@"resizeMode"] isEqualToString:@"exact"]) { + imageOptions.resizeMode = PHImageRequestOptionsResizeModeExact; + } else if ([options[@"resizeMode"] isEqualToString:@"none"]) { + imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone; + } else { + imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone; + } + + // Content Mode (default: PHImageContentModeAspectFill) + PHImageContentMode contentMode; + if ([options[@"contentMode"] isEqualToString:@"fill"]) { + contentMode = PHImageContentModeAspectFill; + } else if ([options[@"contentMode"] isEqualToString:@"fit"]) { + contentMode = PHImageContentModeAspectFit; + } else { + contentMode = PHImageContentModeAspectFit; + } + + float retinaScale = [UIScreen mainScreen].scale; + CGSize targetSize; + + if ([options[@"assetUseMaximumSize"] boolValue]) { + targetSize = PHImageManagerMaximumSize; + } else { + targetSize = CGSizeMake([options[@"targetSize"][@"width"] floatValue] * retinaScale, + [options[@"targetSize"][@"height"] floatValue] * retinaScale); + } + + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:imageOptions resultHandler:^(UIImage *result, NSDictionary *info) { if (result) { RCTDispatchCallbackOnMainQueue(callback, nil, result); } else { @@ -114,6 +211,7 @@ + (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, return; } }]; + } else if ([imageTag hasPrefix:@"http"]) { NSURL *url = [NSURL URLWithString:imageTag]; if (!url) { diff --git a/Libraries/Image/RCTImageRequestHandler.m b/Libraries/Image/RCTImageRequestHandler.m index e5eb3bfd4f1d2c..450c722a1e89d0 100644 --- a/Libraries/Image/RCTImageRequestHandler.m +++ b/Libraries/Image/RCTImageRequestHandler.m @@ -30,7 +30,7 @@ - (id)sendRequest:(NSURLRequest *)request { NSNumber *requestToken = @(++_currentToken); NSString *URLString = [request.URL absoluteString]; - [RCTImageLoader loadImageWithTag:URLString callback:^(NSError *error, UIImage *image) { + [RCTImageLoader loadImageWithTag:URLString options:@{} callback:^(NSError *error, UIImage *image) { if (error) { [delegate URLRequest:requestToken didCompleteWithError:error]; return; diff --git a/Libraries/Image/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m index bdc6f0596a673d..864cde226c5556 100644 --- a/Libraries/Image/RCTStaticImageManager.m +++ b/Libraries/Image/RCTStaticImageManager.m @@ -51,10 +51,10 @@ - (UIView *)view view.tintColor = defaultView.tintColor; } } -RCT_CUSTOM_VIEW_PROPERTY(imageTag, NSString, RCTStaticImage) +RCT_CUSTOM_VIEW_PROPERTY(imageTag, NSDictionary, RCTStaticImage) { if (json) { - [RCTImageLoader loadImageWithTag:[RCTConvert NSString:json] callback:^(NSError *error, id image) { + [RCTImageLoader loadImageWithTag:[RCTConvert NSString:json[@"uri"]] options:[RCTConvert NSDictionary:json[@"options"]] callback:^(NSError *error, id image) { if (error) { RCTLogWarn(@"%@", error.localizedDescription); }