Added getImageSize method

Summary:
public

This diff adds a `getSize()` method to `Image` to retrieve the width and height of an image prior to displaying it. This is useful when working with images from uncontrolled sources, and has been a much-requested feature.

In order to retrieve the image dimensions, the image may first need to be loaded or downloaded, after which it will be cached. This means that in principle you could use this method to preload images, however it is not optimized for that purpose, and may in future be implemented in a way that does not fully load/download the image data.

A fully supported way to preload images will be provided in a future diff.

The API (separate success and failure callbacks) is far from ideal, but until we agree on a unified standard, this was the most conventional way I could think of to implement it. If it returned a promise or something similar, it would be unique among all such APIS in the framework.

Please note that this has been a long time coming, in part due to much bikeshedding about what the API should look like, so while it's not unlikely that the API may change in future, I think having *some* way to do this is better than waiting until we can define the "perfect" way.

Reviewed By: vjeux

Differential Revision: D2797365

fb-gh-sync-id: 11eb1b8547773b1f8be0bc55ddf6dfedebf7fc0a
This commit is contained in:
Nick Lockwood 2015-12-31 18:50:26 -08:00 committed by facebook-github-bot-5
parent 24b942faeb
commit 718cd7953f
7 changed files with 191 additions and 27 deletions

View File

@ -68,8 +68,6 @@ var NetworkImageCallbackExample = React.createClass({
});
var NetworkImageExample = React.createClass({
watchID: (null: ?number),
getInitialState: function() {
return {
error: false,
@ -97,6 +95,38 @@ var NetworkImageExample = React.createClass({
}
});
var ImageSizeExample = React.createClass({
getInitialState: function() {
return {
width: 0,
height: 0,
};
},
componentDidMount: function() {
Image.getSize(this.props.source.uri, (width, height) => {
this.setState({width, height});
});
},
render: function() {
return (
<View style={{flexDirection: 'row'}}>
<Image
style={{
width: 60,
height: 60,
backgroundColor: 'transparent',
marginRight: 10,
}}
source={this.props.source} />
<Text>
Actual dimensions:{'\n'}
Width: {this.state.width}, Height: {this.state.height}
</Text>
</View>
);
},
});
exports.displayName = (undefined: ?string);
exports.framework = 'React';
exports.title = '<Image>';
@ -408,6 +438,12 @@ exports.examples = [
},
platform: 'ios',
},
{
title: 'Image Size',
render: function() {
return <ImageSizeExample source={fullImage} />;
}
},
];
var fullImage = {uri: 'http://facebook.github.io/react/img/logo_og.png'};

View File

@ -15,7 +15,6 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType');
var ImageResizeMode = require('ImageResizeMode');
var ImageStylePropTypes = require('ImageStylePropTypes');
var NativeMethodsMixin = require('NativeMethodsMixin');
var NativeModules = require('NativeModules');
var PropTypes = require('ReactPropTypes');
var React = require('React');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
@ -29,6 +28,11 @@ var requireNativeComponent = require('requireNativeComponent');
var resolveAssetSource = require('resolveAssetSource');
var warning = require('warning');
var {
ImageViewManager,
NetworkImageViewManager,
} = require('NativeModules');
/**
* A React component for displaying different types of images,
* including network images, static resources, temporary local images, and
@ -197,7 +201,7 @@ var Image = React.createClass({
/>
);
}
}
},
});
var styles = StyleSheet.create({
@ -207,7 +211,30 @@ var styles = StyleSheet.create({
});
var RCTImageView = requireNativeComponent('RCTImageView', Image);
var RCTNetworkImageView = NativeModules.NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView;
var RCTNetworkImageView = NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView;
var RCTVirtualImage = requireNativeComponent('RCTVirtualImage', Image);
/**
* Retrieve the width and height (in pixels) of an image prior to displaying it.
* This method can fail if the image cannot be found, or fails to download.
*
* In order to retrieve the image dimensions, the image may first need to be
* loaded or downloaded, after which it will be cached. This means that in
* principle you could use this method to preload images, however it is not
* optimized for that purpose, and may in future be implemented in a way that
* does not fully load/download the image data. A proper, supported way to
* preload images will be provided as a separate API.
*
* @platform ios
*/
Image.getSize = function(
uri: string,
success: (width: number, height: number) => void,
failure: (error: any) => void,
) {
ImageViewManager.getSize(uri, success, failure || function() {
console.warn('Failed to get size for image: ' + uri);
});
};
module.exports = Image;

View File

@ -55,6 +55,13 @@ typedef void (^RCTImageLoaderCancellationBlock)(void);
resizeMode:(UIViewContentMode)resizeMode
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;
/**
* Get image size, in pixels. This method will do the least work possible to get
* the information, and won't decode the image if it doesn't have to.
*/
- (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag
block:(void(^)(NSError *error, CGSize size))completionBlock;
@end
@interface RCTBridge (RCTImageLoader)

View File

@ -11,6 +11,7 @@
#import <libkern/OSAtomic.h>
#import <UIKit/UIKit.h>
#import <ImageIO/ImageIO.h>
#import "RCTConvert.h"
#import "RCTDefines.h"
@ -183,29 +184,34 @@ RCT_EXPORT_MODULE()
completionBlock:callback];
}
- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
/**
* This returns either an image, or raw image data, depending on the loading
* path taken. This is useful if you want to skip decoding, e.g. when preloading
* the image, or retrieving metadata.
*/
- (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
completionBlock:(void (^)(NSError *error, id imageOrData))completionBlock
{
__block volatile uint32_t cancelled = 0;
__block void(^cancelLoad)(void) = nil;
__weak RCTImageLoader *weakSelf = self;
RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) {
void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) {
if ([NSThread isMainThread]) {
// Most loaders do not return on the main thread, so caller is probably not
// expecting it, and may do expensive post-processing in the callback
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (!cancelled) {
completionBlock(error, image);
completionBlock(error, imageOrData);
}
});
} else if (!cancelled) {
completionBlock(error, image);
completionBlock(error, imageOrData);
}
};
@ -259,7 +265,6 @@ RCT_EXPORT_MODULE()
}
// Use networking module to load image
__block RCTImageLoaderCancellationBlock cancelDecode = nil;
RCTURLRequestCompletionBlock processResponse =
^(NSURLResponse *response, NSData *data, NSError *error) {
@ -283,12 +288,8 @@ RCT_EXPORT_MODULE()
}
}
// Decode image
cancelDecode = [strongSelf decodeImageData:data
size:size
scale:scale
resizeMode:resizeMode
completionBlock:completionHandler];
// Call handler
completionHandler(nil, data);
};
// Add missing png extension
@ -325,7 +326,6 @@ RCT_EXPORT_MODULE()
userInfo:nil
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
forRequest:request];
// Process image data
processResponse(response, data, nil);
@ -337,9 +337,6 @@ RCT_EXPORT_MODULE()
cancelLoad = ^{
[task cancel];
if (cancelDecode) {
cancelDecode();
}
};
});
@ -352,6 +349,45 @@ RCT_EXPORT_MODULE()
};
}
- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
{
__block volatile uint32_t cancelled = 0;
__block void(^cancelLoad)(void) = nil;
__weak RCTImageLoader *weakSelf = self;
void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) {
if (!cancelled) {
if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) {
completionBlock(error, imageOrData);
} else {
cancelLoad = [weakSelf decodeImageData:imageOrData
size:size
scale:scale
resizeMode:resizeMode
completionBlock:completionBlock] ?: ^{};
}
}
};
cancelLoad = [self loadImageOrDataWithTag:imageTag
size:size
scale:scale
resizeMode:resizeMode
progressBlock:progressHandler
completionBlock:completionHandler] ?: ^{};
return ^{
if (cancelLoad) {
cancelLoad();
}
OSAtomicOr32Barrier(1, &cancelled);
};
}
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
size:(CGSize)size
scale:(CGFloat)scale
@ -394,6 +430,33 @@ RCT_EXPORT_MODULE()
}
}
- (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag
block:(void(^)(NSError *error, CGSize size))completionBlock
{
return [self loadImageOrDataWithTag:imageTag
size:CGSizeZero
scale:1
resizeMode:UIViewContentModeScaleToFill
progressBlock:nil
completionBlock:^(NSError *error, id imageOrData) {
CGSize size;
if ([imageOrData isKindOfClass:[NSData class]]) {
NSDictionary *meta = RCTGetImageMetadata(imageOrData);
size = (CGSize){
[meta[(id)kCGImagePropertyPixelWidth] doubleValue],
[meta[(id)kCGImagePropertyPixelHeight] doubleValue],
};
} else {
UIImage *image = imageOrData;
size = (CGSize){
image.size.width * image.scale,
image.size.height * image.scale,
};
}
completionBlock(error, size);
}];
}
#pragma mark - RCTURLRequestHandler
- (BOOL)canHandleRequest:(NSURLRequest *)request

View File

@ -58,6 +58,12 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
CGFloat destScale,
UIViewContentMode resizeMode);
/**
* This function takes the source data for an image and decodes just the
* metadata, without decompressing the image itself.
*/
RCT_EXTERN NSDictionary<NSString *, id> *RCTGetImageMetadata(NSData *data);
/**
* Convert an image back into data. Images with an alpha channel will be
* converted to lossless PNG data. Images without alpha will be converted to

View File

@ -218,7 +218,6 @@ UIImage *RCTDecodeImageWithData(NSData *data,
}
// get original image size
CGSize sourceSize;
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
if (!imageProperties) {
CFRelease(sourceRef);
@ -226,7 +225,7 @@ UIImage *RCTDecodeImageWithData(NSData *data,
}
NSNumber *width = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth);
NSNumber *height = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight);
sourceSize = (CGSize){width.doubleValue, height.doubleValue};
CGSize sourceSize = {width.doubleValue, height.doubleValue};
CFRelease(imageProperties);
if (CGSizeEqualToSize(destSize, CGSizeZero)) {
@ -266,6 +265,17 @@ UIImage *RCTDecodeImageWithData(NSData *data,
return image;
}
NSDictionary<NSString *, id> *RCTGetImageMetadata(NSData *data)
{
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!sourceRef) {
return nil;
}
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
CFRelease(sourceRef);
return (__bridge_transfer id)imageProperties;
}
NSData *RCTGetImageData(CGImageRef image, float quality)
{
NSDictionary *properties;

View File

@ -12,6 +12,7 @@
#import <UIKit/UIKit.h>
#import "RCTConvert.h"
#import "RCTImageLoader.h"
#import "RCTImageSource.h"
#import "RCTImageView.h"
@ -42,4 +43,18 @@ RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView)
view.renderingMode = json ? UIImageRenderingModeAlwaysTemplate : defaultView.renderingMode;
}
RCT_EXPORT_METHOD(getSize:(NSURL *)imageURL
successBlock:(RCTResponseSenderBlock)successBlock
errorBlock:(RCTResponseErrorBlock)errorBlock)
{
[self.bridge.imageLoader getImageSize:imageURL.absoluteString
block:^(NSError *error, CGSize size) {
if (error) {
errorBlock(error);
} else {
successBlock(@[@(size.width), @(size.height)]);
}
}];
}
@end