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);
}