From 718cd7953f6a30338882c4b56bd11c83fb311d7f Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 31 Dec 2015 18:50:26 -0800 Subject: [PATCH] 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 --- Examples/UIExplorer/ImageExample.js | 40 +++++++++- Libraries/Image/Image.ios.js | 33 ++++++++- Libraries/Image/RCTImageLoader.h | 7 ++ Libraries/Image/RCTImageLoader.m | 103 +++++++++++++++++++++----- Libraries/Image/RCTImageUtils.h | 6 ++ Libraries/Image/RCTImageUtils.m | 14 +++- Libraries/Image/RCTImageViewManager.m | 15 ++++ 7 files changed, 191 insertions(+), 27 deletions(-) diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index 50c96ffd2..163396f89 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -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 ( + + + + Actual dimensions:{'\n'} + Width: {this.state.width}, Height: {this.state.height} + + + ); + }, +}); + exports.displayName = (undefined: ?string); exports.framework = 'React'; exports.title = ''; @@ -408,6 +438,12 @@ exports.examples = [ }, platform: 'ios', }, + { + title: 'Image Size', + render: function() { + return ; + } + }, ]; var fullImage = {uri: 'http://facebook.github.io/react/img/logo_og.png'}; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 8eda0cc8c..91293c07f 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -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; diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index 770747452..c9bea5437 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -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) diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index acee9dbec..cd681b113 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -11,6 +11,7 @@ #import #import +#import #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 diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h index 901a876ab..b9a118ec1 100644 --- a/Libraries/Image/RCTImageUtils.h +++ b/Libraries/Image/RCTImageUtils.h @@ -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 *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 diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index a6c2ccd46..a9476ff51 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -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 *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; diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m index d318d1f04..7ef593195 100644 --- a/Libraries/Image/RCTImageViewManager.m +++ b/Libraries/Image/RCTImageViewManager.m @@ -12,6 +12,7 @@ #import #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