diff --git a/Examples/UIExplorer/AssetScaledImageExample.js b/Examples/UIExplorer/AssetScaledImageExample.js new file mode 100644 index 00000000000..dbfe7afb7b6 --- /dev/null +++ b/Examples/UIExplorer/AssetScaledImageExample.js @@ -0,0 +1,98 @@ +/** + * 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, + View, + ScrollView +} = React; + +var AssetScaledImageExample = React.createClass({ + + getInitialState() { + return { + asset: this.props.asset + }; + }, + + render() { + var image = this.state.asset.node.image; + return ( + + + + + + + + + + + + + + + ); + }, +}); + +var styles = StyleSheet.create({ + row: { + padding: 5, + flex: 1, + flexDirection: 'row', + alignSelf: 'center', + }, + textColumn: { + flex: 1, + flexDirection: 'column', + }, + imageWide: { + borderWidth: 1, + borderColor: 'black', + width: 320, + height: 240, + margin: 5, + }, + imageThumb: { + borderWidth: 1, + borderColor: 'black', + width: 100, + height: 100, + margin: 5, + }, + imageT1: { + borderWidth: 1, + borderColor: 'black', + width: 212, + height: 320, + margin: 5, + }, + imageT2: { + borderWidth: 1, + borderColor: 'black', + width: 100, + height: 320, + margin: 5, + }, +}); + +exports.title = ''; +exports.description = 'Example component that displays the automatic scaling capabilities of the tag'; +module.exports = AssetScaledImageExample; diff --git a/Examples/UIExplorer/CameraRollExample.ios.js b/Examples/UIExplorer/CameraRollExample.ios.js index 73678407213..d783d9d8e24 100644 --- a/Examples/UIExplorer/CameraRollExample.ios.js +++ b/Examples/UIExplorer/CameraRollExample.ios.js @@ -24,9 +24,11 @@ var { SwitchIOS, Text, View, + TouchableOpacity } = React; var CameraRollView = require('./CameraRollView.ios'); +var AssetScaledImageExampleView = require('./AssetScaledImageExample'); var CAMERA_ROLL_VIEW = 'camera_roll_view'; @@ -54,7 +56,7 @@ var CameraRollExample = React.createClass({ {'Group Type: ' + this.state.groupTypes} @@ -62,24 +64,35 @@ var CameraRollExample = React.createClass({ ); }, + loadAsset(asset){ + this.props.navigator.push({ + title: 'Camera Roll Image', + component: AssetScaledImageExampleView, + backButtonTitle: 'Back', + passProps: { asset: asset }, + }); + }, + _renderImage(asset) { var imageSize = this.state.bigImages ? 150 : 75; var imageStyle = [styles.image, {width: imageSize, height: imageSize}]; 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()} + - + ); }, @@ -115,7 +128,7 @@ var styles = StyleSheet.create({ }, }); -exports.title = ''; +exports.title = 'Camera Roll'; exports.description = 'Example component that uses CameraRoll to list user\'s photos'; exports.examples = [ { diff --git a/Examples/UIExplorer/createExamplePage.js b/Examples/UIExplorer/createExamplePage.js index 3d5a1ac88c4..352f84a4c4a 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/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 4300f3b9458..f9bfd1bf79a 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -72,6 +72,64 @@ static dispatch_queue_t RCTImageLoaderQueue(void) completionBlock:callback]; } +// +// Why use a custom scaling method: +// http://www.mindsea.com/2012/12/downscaling-huge-alassets-without-fear-of-sigkill/ +// Greater efficiency, reduced memory overhead. ++ (UIImage *)scaledImageForAssetRepresentation:(ALAssetRepresentation *)representation + size:(CGSize)size + scale:(CGFloat)scale + orientation:(UIImageOrientation)orientation +{ + UIImage *image = nil; + NSData *data = nil; + + uint8_t *buffer = (uint8_t *)malloc(sizeof(uint8_t)*(NSUInteger)[representation size]); + if (buffer != NULL) { + NSError *error = nil; + NSUInteger bytesRead = [representation getBytes:buffer fromOffset:0 length:(NSUInteger)[representation 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]; + + CGSize source = representation.dimensions; + CGFloat mW = size.width / source.width; + CGFloat mH = size.height / source.height; + + if (mH > mW) { + size.width = size.height / source.height * source.width; + } else if (mW > mH) { + size.height = size.width / source.width * source.height; + } + + CGFloat maxPixelSize = MAX(size.width, size.height) * scale; + + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceShouldAllowFloat]; + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailWithTransform]; + [options setObject:(id)kCFBooleanTrue forKey:(id)kCGImageSourceCreateThumbnailFromImageAlways]; + [options setObject:(id)@(maxPixelSize) forKey:(id)kCGImageSourceThumbnailMaxPixelSize]; + + CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options); + + if (imageRef) { + image = [UIImage imageWithCGImage:imageRef scale:[representation scale] orientation:orientation]; + CGImageRelease(imageRef); + } + + if (sourceRef) { + CFRelease(sourceRef); + } + } + + return image; +} + + (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag size:(CGSize)size scale:(CGFloat)scale @@ -94,28 +152,17 @@ static dispatch_queue_t RCTImageLoaderQueue(void) BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); ALAssetOrientation orientation = ALAssetOrientationUp; - CGImageRef imageRef = NULL; + ALAssetRepresentation *representation = [asset defaultRepresentation]; - if (!useMaximumSize) { - imageRef = asset.thumbnail; - } - if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) { - if (!useMaximumSize) { - imageRef = asset.aspectRatioThumbnail; - } - if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) { - ALAssetRepresentation *representation = [asset defaultRepresentation]; - orientation = [representation orientation]; - if (!useMaximumSize) { - imageRef = [representation fullScreenImage]; - } - if (RCTUpscalingRequired((CGSize){CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}, 1, size, scale, resizeMode)) { - imageRef = [representation fullResolutionImage]; - } - } + UIImage *image; + + if (useMaximumSize) { + image = [UIImage imageWithCGImage:representation.fullResolutionImage scale:scale orientation:(UIImageOrientation)orientation]; + + } else { + image = [self scaledImageForAssetRepresentation:representation size:size scale:scale orientation:(UIImageOrientation)orientation]; } - UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:(UIImageOrientation)orientation]; RCTDispatchCallbackOnMainQueue(completion, nil, image); } }); @@ -145,12 +192,25 @@ static dispatch_queue_t RCTImageLoaderQueue(void) } PHAsset *asset = [results firstObject]; - CGSize targetSize = CGSizeEqualToSize(size, CGSizeZero) ? PHImageManagerMaximumSize : size; + + PHImageRequestOptions *imageOptions = [[PHImageRequestOptions alloc] init]; + + BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); + CGSize targetSize; + + if ( useMaximumSize ){ + targetSize = PHImageManagerMaximumSize; + imageOptions.resizeMode = PHImageRequestOptionsResizeModeNone; + } else { + targetSize = size; + imageOptions.resizeMode = PHImageRequestOptionsResizeModeFast; + } + PHImageContentMode contentMode = PHImageContentModeAspectFill; if (resizeMode == UIViewContentModeScaleAspectFit) { contentMode = PHImageContentModeAspectFit; } - [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:nil resultHandler:^(UIImage *result, NSDictionary *info) { + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:targetSize contentMode:contentMode options:imageOptions resultHandler:^(UIImage *result, NSDictionary *info) { if (result) { RCTDispatchCallbackOnMainQueue(completion, nil, result); } else {