Skip to content

Commit 718cd79

Browse files
nicklockwoodfacebook-github-bot-5
authored andcommitted
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
1 parent 24b942f commit 718cd79

File tree

7 files changed

+191
-27
lines changed

7 files changed

+191
-27
lines changed

Examples/UIExplorer/ImageExample.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ var NetworkImageCallbackExample = React.createClass({
6868
});
6969

7070
var NetworkImageExample = React.createClass({
71-
watchID: (null: ?number),
72-
7371
getInitialState: function() {
7472
return {
7573
error: false,
@@ -97,6 +95,38 @@ var NetworkImageExample = React.createClass({
9795
}
9896
});
9997

98+
var ImageSizeExample = React.createClass({
99+
getInitialState: function() {
100+
return {
101+
width: 0,
102+
height: 0,
103+
};
104+
},
105+
componentDidMount: function() {
106+
Image.getSize(this.props.source.uri, (width, height) => {
107+
this.setState({width, height});
108+
});
109+
},
110+
render: function() {
111+
return (
112+
<View style={{flexDirection: 'row'}}>
113+
<Image
114+
style={{
115+
width: 60,
116+
height: 60,
117+
backgroundColor: 'transparent',
118+
marginRight: 10,
119+
}}
120+
source={this.props.source} />
121+
<Text>
122+
Actual dimensions:{'\n'}
123+
Width: {this.state.width}, Height: {this.state.height}
124+
</Text>
125+
</View>
126+
);
127+
},
128+
});
129+
100130
exports.displayName = (undefined: ?string);
101131
exports.framework = 'React';
102132
exports.title = '<Image>';
@@ -408,6 +438,12 @@ exports.examples = [
408438
},
409439
platform: 'ios',
410440
},
441+
{
442+
title: 'Image Size',
443+
render: function() {
444+
return <ImageSizeExample source={fullImage} />;
445+
}
446+
},
411447
];
412448

413449
var fullImage = {uri: 'http://facebook.github.io/react/img/logo_og.png'};

Libraries/Image/Image.ios.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType');
1515
var ImageResizeMode = require('ImageResizeMode');
1616
var ImageStylePropTypes = require('ImageStylePropTypes');
1717
var NativeMethodsMixin = require('NativeMethodsMixin');
18-
var NativeModules = require('NativeModules');
1918
var PropTypes = require('ReactPropTypes');
2019
var React = require('React');
2120
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
@@ -29,6 +28,11 @@ var requireNativeComponent = require('requireNativeComponent');
2928
var resolveAssetSource = require('resolveAssetSource');
3029
var warning = require('warning');
3130

31+
var {
32+
ImageViewManager,
33+
NetworkImageViewManager,
34+
} = require('NativeModules');
35+
3236
/**
3337
* A React component for displaying different types of images,
3438
* including network images, static resources, temporary local images, and
@@ -197,7 +201,7 @@ var Image = React.createClass({
197201
/>
198202
);
199203
}
200-
}
204+
},
201205
});
202206

203207
var styles = StyleSheet.create({
@@ -207,7 +211,30 @@ var styles = StyleSheet.create({
207211
});
208212

209213
var RCTImageView = requireNativeComponent('RCTImageView', Image);
210-
var RCTNetworkImageView = NativeModules.NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView;
214+
var RCTNetworkImageView = NetworkImageViewManager ? requireNativeComponent('RCTNetworkImageView', Image) : RCTImageView;
211215
var RCTVirtualImage = requireNativeComponent('RCTVirtualImage', Image);
212216

217+
/**
218+
* Retrieve the width and height (in pixels) of an image prior to displaying it.
219+
* This method can fail if the image cannot be found, or fails to download.
220+
*
221+
* In order to retrieve the image dimensions, the image may first need to be
222+
* loaded or downloaded, after which it will be cached. This means that in
223+
* principle you could use this method to preload images, however it is not
224+
* optimized for that purpose, and may in future be implemented in a way that
225+
* does not fully load/download the image data. A proper, supported way to
226+
* preload images will be provided as a separate API.
227+
*
228+
* @platform ios
229+
*/
230+
Image.getSize = function(
231+
uri: string,
232+
success: (width: number, height: number) => void,
233+
failure: (error: any) => void,
234+
) {
235+
ImageViewManager.getSize(uri, success, failure || function() {
236+
console.warn('Failed to get size for image: ' + uri);
237+
});
238+
};
239+
213240
module.exports = Image;

Libraries/Image/RCTImageLoader.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ typedef void (^RCTImageLoaderCancellationBlock)(void);
5555
resizeMode:(UIViewContentMode)resizeMode
5656
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;
5757

58+
/**
59+
* Get image size, in pixels. This method will do the least work possible to get
60+
* the information, and won't decode the image if it doesn't have to.
61+
*/
62+
- (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag
63+
block:(void(^)(NSError *error, CGSize size))completionBlock;
64+
5865
@end
5966

6067
@interface RCTBridge (RCTImageLoader)

Libraries/Image/RCTImageLoader.m

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#import <libkern/OSAtomic.h>
1313
#import <UIKit/UIKit.h>
14+
#import <ImageIO/ImageIO.h>
1415

1516
#import "RCTConvert.h"
1617
#import "RCTDefines.h"
@@ -183,29 +184,34 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
183184
completionBlock:callback];
184185
}
185186

186-
- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
187-
size:(CGSize)size
188-
scale:(CGFloat)scale
189-
resizeMode:(UIViewContentMode)resizeMode
190-
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
191-
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
187+
/**
188+
* This returns either an image, or raw image data, depending on the loading
189+
* path taken. This is useful if you want to skip decoding, e.g. when preloading
190+
* the image, or retrieving metadata.
191+
*/
192+
- (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag
193+
size:(CGSize)size
194+
scale:(CGFloat)scale
195+
resizeMode:(UIViewContentMode)resizeMode
196+
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
197+
completionBlock:(void (^)(NSError *error, id imageOrData))completionBlock
192198
{
193199
__block volatile uint32_t cancelled = 0;
194200
__block void(^cancelLoad)(void) = nil;
195201
__weak RCTImageLoader *weakSelf = self;
196202

197-
RCTImageLoaderCompletionBlock completionHandler = ^(NSError *error, UIImage *image) {
203+
void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) {
198204
if ([NSThread isMainThread]) {
199205

200206
// Most loaders do not return on the main thread, so caller is probably not
201207
// expecting it, and may do expensive post-processing in the callback
202208
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
203209
if (!cancelled) {
204-
completionBlock(error, image);
210+
completionBlock(error, imageOrData);
205211
}
206212
});
207213
} else if (!cancelled) {
208-
completionBlock(error, image);
214+
completionBlock(error, imageOrData);
209215
}
210216
};
211217

@@ -259,7 +265,6 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
259265
}
260266

261267
// Use networking module to load image
262-
__block RCTImageLoaderCancellationBlock cancelDecode = nil;
263268
RCTURLRequestCompletionBlock processResponse =
264269
^(NSURLResponse *response, NSData *data, NSError *error) {
265270

@@ -283,12 +288,8 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
283288
}
284289
}
285290

286-
// Decode image
287-
cancelDecode = [strongSelf decodeImageData:data
288-
size:size
289-
scale:scale
290-
resizeMode:resizeMode
291-
completionBlock:completionHandler];
291+
// Call handler
292+
completionHandler(nil, data);
292293
};
293294

294295
// Add missing png extension
@@ -325,7 +326,6 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
325326
userInfo:nil
326327
storagePolicy:isHTTPRequest ? NSURLCacheStorageAllowed: NSURLCacheStorageAllowedInMemoryOnly]
327328
forRequest:request];
328-
329329
// Process image data
330330
processResponse(response, data, nil);
331331

@@ -337,9 +337,6 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
337337

338338
cancelLoad = ^{
339339
[task cancel];
340-
if (cancelDecode) {
341-
cancelDecode();
342-
}
343340
};
344341

345342
});
@@ -352,6 +349,45 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
352349
};
353350
}
354351

352+
- (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
353+
size:(CGSize)size
354+
scale:(CGFloat)scale
355+
resizeMode:(UIViewContentMode)resizeMode
356+
progressBlock:(RCTImageLoaderProgressBlock)progressHandler
357+
completionBlock:(RCTImageLoaderCompletionBlock)completionBlock
358+
{
359+
__block volatile uint32_t cancelled = 0;
360+
__block void(^cancelLoad)(void) = nil;
361+
__weak RCTImageLoader *weakSelf = self;
362+
363+
void (^completionHandler)(NSError *error, id imageOrData) = ^(NSError *error, id imageOrData) {
364+
if (!cancelled) {
365+
if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) {
366+
completionBlock(error, imageOrData);
367+
} else {
368+
cancelLoad = [weakSelf decodeImageData:imageOrData
369+
size:size
370+
scale:scale
371+
resizeMode:resizeMode
372+
completionBlock:completionBlock] ?: ^{};
373+
}
374+
}
375+
};
376+
377+
cancelLoad = [self loadImageOrDataWithTag:imageTag
378+
size:size
379+
scale:scale
380+
resizeMode:resizeMode
381+
progressBlock:progressHandler
382+
completionBlock:completionHandler] ?: ^{};
383+
return ^{
384+
if (cancelLoad) {
385+
cancelLoad();
386+
}
387+
OSAtomicOr32Barrier(1, &cancelled);
388+
};
389+
}
390+
355391
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
356392
size:(CGSize)size
357393
scale:(CGFloat)scale
@@ -394,6 +430,33 @@ - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data
394430
}
395431
}
396432

433+
- (RCTImageLoaderCancellationBlock)getImageSize:(NSString *)imageTag
434+
block:(void(^)(NSError *error, CGSize size))completionBlock
435+
{
436+
return [self loadImageOrDataWithTag:imageTag
437+
size:CGSizeZero
438+
scale:1
439+
resizeMode:UIViewContentModeScaleToFill
440+
progressBlock:nil
441+
completionBlock:^(NSError *error, id imageOrData) {
442+
CGSize size;
443+
if ([imageOrData isKindOfClass:[NSData class]]) {
444+
NSDictionary *meta = RCTGetImageMetadata(imageOrData);
445+
size = (CGSize){
446+
[meta[(id)kCGImagePropertyPixelWidth] doubleValue],
447+
[meta[(id)kCGImagePropertyPixelHeight] doubleValue],
448+
};
449+
} else {
450+
UIImage *image = imageOrData;
451+
size = (CGSize){
452+
image.size.width * image.scale,
453+
image.size.height * image.scale,
454+
};
455+
}
456+
completionBlock(error, size);
457+
}];
458+
}
459+
397460
#pragma mark - RCTURLRequestHandler
398461

399462
- (BOOL)canHandleRequest:(NSURLRequest *)request

Libraries/Image/RCTImageUtils.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
5858
CGFloat destScale,
5959
UIViewContentMode resizeMode);
6060

61+
/**
62+
* This function takes the source data for an image and decodes just the
63+
* metadata, without decompressing the image itself.
64+
*/
65+
RCT_EXTERN NSDictionary<NSString *, id> *RCTGetImageMetadata(NSData *data);
66+
6167
/**
6268
* Convert an image back into data. Images with an alpha channel will be
6369
* converted to lossless PNG data. Images without alpha will be converted to

Libraries/Image/RCTImageUtils.m

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,14 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
218218
}
219219

220220
// get original image size
221-
CGSize sourceSize;
222221
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
223222
if (!imageProperties) {
224223
CFRelease(sourceRef);
225224
return nil;
226225
}
227226
NSNumber *width = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelWidth);
228227
NSNumber *height = CFDictionaryGetValue(imageProperties, kCGImagePropertyPixelHeight);
229-
sourceSize = (CGSize){width.doubleValue, height.doubleValue};
228+
CGSize sourceSize = {width.doubleValue, height.doubleValue};
230229
CFRelease(imageProperties);
231230

232231
if (CGSizeEqualToSize(destSize, CGSizeZero)) {
@@ -266,6 +265,17 @@ CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
266265
return image;
267266
}
268267

268+
NSDictionary<NSString *, id> *RCTGetImageMetadata(NSData *data)
269+
{
270+
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
271+
if (!sourceRef) {
272+
return nil;
273+
}
274+
CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL);
275+
CFRelease(sourceRef);
276+
return (__bridge_transfer id)imageProperties;
277+
}
278+
269279
NSData *RCTGetImageData(CGImageRef image, float quality)
270280
{
271281
NSDictionary *properties;

Libraries/Image/RCTImageViewManager.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#import <UIKit/UIKit.h>
1313

1414
#import "RCTConvert.h"
15+
#import "RCTImageLoader.h"
1516
#import "RCTImageSource.h"
1617
#import "RCTImageView.h"
1718

@@ -42,4 +43,18 @@ - (UIView *)view
4243
view.renderingMode = json ? UIImageRenderingModeAlwaysTemplate : defaultView.renderingMode;
4344
}
4445

46+
RCT_EXPORT_METHOD(getSize:(NSURL *)imageURL
47+
successBlock:(RCTResponseSenderBlock)successBlock
48+
errorBlock:(RCTResponseErrorBlock)errorBlock)
49+
{
50+
[self.bridge.imageLoader getImageSize:imageURL.absoluteString
51+
block:^(NSError *error, CGSize size) {
52+
if (error) {
53+
errorBlock(error);
54+
} else {
55+
successBlock(@[@(size.width), @(size.height)]);
56+
}
57+
}];
58+
}
59+
4560
@end

0 commit comments

Comments
 (0)