From 9c73e2ff7aeac8c65f25472135e4c0410f797eea Mon Sep 17 00:00:00 2001 From: Adam Roth Date: Tue, 21 Jul 2015 05:39:57 -0700 Subject: [PATCH] [Image] Improved loading of Assets Library and Photos Framework images. Summary: Update to https://github.com/facebook/react-native/pull/1969 -- Recent improvements allow RCTImageLoader to select a more appropriate sized image based on the layout dimensions. Sizes: - asset.thumbnail - asset.aspectRatioThumbnail - asset.defaultRepresentation.fullScreenImage - asset.defaultRepresentation.fullResolutionImage Prior, only the fullResolutionImage was used. This was memory intensive and resulted in crashes when loading several large images at once. The updated implementation works well, but can be made more efficient: Consider loading 10 8MP (3264x2448) images in 150x150 pixel containers. The target size (150x150) is larger than asset.thumbnail (approx 100x100), therefore the fullScreenImage representation is used instead (approx 1334x1000). This commit will scale the asset to the minimum size required while taking into account original aspect ratio and device scale. Memory usage is considerably lower and many more images can be loaded in sequence without having to worry Closes https://github.com/facebook/react-native/pull/2008 Github Author: Adam Roth --- .../UIExplorer/AssetScaledImageExample.js | 98 +++++++++++++++++ Examples/UIExplorer/CameraRollExample.ios.js | 39 ++++--- Examples/UIExplorer/createExamplePage.js | 1 + Libraries/Image/RCTImageLoader.m | 102 ++++++++++++++---- 4 files changed, 206 insertions(+), 34 deletions(-) create mode 100644 Examples/UIExplorer/AssetScaledImageExample.js 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 {