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:
parent
24b942faeb
commit
718cd7953f
|
@ -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'};
|
||||
|
|
|
@ -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 @@ 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
|
||||
|
|
|
@ -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,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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue