Permalink
Browse files

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
  • Loading branch information...
nicklockwood authored and facebook-github-bot-5 committed Jan 1, 2016
1 parent 24b942f commit 718cd7953f6a30338882c4b56bd11c83fb311d7f
@@ -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 comment has been minimized.

Show comment
Hide comment
@scottmmjackson

scottmmjackson Jan 1, 2016

Image.getSize is not a function on Android.

This causes the entire example to crash when running on Android. See comment below.

@scottmmjackson

scottmmjackson Jan 1, 2016

Image.getSize is not a function on Android.

This causes the entire example to crash when running on Android. See comment below.

+ 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} />;
+ }

This comment has been minimized.

Show comment
Hide comment
@scottmmjackson

scottmmjackson Jan 1, 2016

Should add property platform: 'ios'

@scottmmjackson

scottmmjackson Jan 1, 2016

Should add property platform: 'ios'

This comment has been minimized.

Show comment
Hide comment
@nicklockwood

nicklockwood Jan 1, 2016

Contributor

Oops, my bad. I can't approve my own diffs, but if you wouldn't mind putting up a PR for the fix, I'll land it.

@nicklockwood

nicklockwood Jan 1, 2016

Contributor

Oops, my bad. I can't approve my own diffs, but if you wouldn't mind putting up a PR for the fix, I'll land it.

+ },
];
var fullImage = {uri: 'http://facebook.github.io/react/img/logo_og.png'};
@@ -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;
@@ -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)
@@ -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 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
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 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
}
// Use networking module to load image
- __block RCTImageLoaderCancellationBlock cancelDecode = nil;
RCTURLRequestCompletionBlock processResponse =
^(NSURLResponse *response, NSData *data, NSError *error) {
@@ -283,12 +288,8 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
}
}
- // 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 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
userInfo:nil
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
forRequest:request];
-
// Process image data
processResponse(response, data, nil);
@@ -337,9 +337,6 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
cancelLoad = ^{
[task cancel];
- if (cancelDecode) {
- cancelDecode();
- }
};
});
@@ -352,6 +349,45 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
};
}
+- (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 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
}
}
+- (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
@@ -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
@@ -218,15 +218,14 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
}
// get original image size
- CGSize sourceSize;
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
if (!imageProperties) {
CFRelease(sourceRef);
return nil;
}
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 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
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;
@@ -12,6 +12,7 @@
#import <UIKit/UIKit.h>
#import "RCTConvert.h"
+#import "RCTImageLoader.h"
#import "RCTImageSource.h"
#import "RCTImageView.h"
@@ -42,4 +43,18 @@ - (UIView *)view
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

5 comments on commit 718cd79

@ssssssssssss

This comment has been minimized.

Show comment
Hide comment

Cool!

@PhilippKrone

This comment has been minimized.

Show comment
Hide comment
@PhilippKrone

PhilippKrone Jan 1, 2016

Contributor

@nicklockwood Is there a similar implementation for android (planned)?

Contributor

PhilippKrone replied Jan 1, 2016

@nicklockwood Is there a similar implementation for android (planned)?

@nicklockwood

This comment has been minimized.

Show comment
Hide comment
@nicklockwood

nicklockwood Jan 1, 2016

Contributor

@PhilippKrone absolutely. Most of my Java-savvy colleagues are still on vacation, but we'll try to get an Android port of this out asap.

Contributor

nicklockwood replied Jan 1, 2016

@PhilippKrone absolutely. Most of my Java-savvy colleagues are still on vacation, but we'll try to get an Android port of this out asap.

@seb0zz

This comment has been minimized.

Show comment
Hide comment
@seb0zz

seb0zz Jan 6, 2016

Would be so great to have this on android. @PhilippKrone are your colleagues already working on it or could you need some contribution support?

Would be so great to have this on android. @PhilippKrone are your colleagues already working on it or could you need some contribution support?

@menq

This comment has been minimized.

Show comment
Hide comment
@menq

menq Jan 8, 2016

this is what i want !

menq replied Jan 8, 2016

this is what i want !

Please sign in to comment.