From 2b657003b75b7a4bf454d4f69c09af7bed3db2d5 Mon Sep 17 00:00:00 2001 From: Matthieu Achard Date: Tue, 17 Nov 2015 09:53:47 -0800 Subject: [PATCH] RTCImageStoreManager uses NSData instead of UIImage Summary: Hi, I'm currently building an app that changes metadata, does some resizes, maybe watermarking ...etc. I want to use RCTImageStoreManager to store the original image in memory and allow me to command different modifications from javascript as it gives me more flexibility. As RCTImageEditingManager does for example. But currently the RTCImageStoreManager uses UIImage to store the image, the problem is that UIImage losses metadata. So i suggest we change it to NSData. Additionally I added a method to remove an image from the store. A related PR can be found here https://github.com/lwansbrough/react-native-camera/pull/100. Closes https://github.com/facebook/react-native/pull/3290 Reviewed By: javache Differential Revision: D2647271 Pulled By: nicklockwood fb-gh-sync-id: e66353ae3005423beee72ec22189dcb117fc719f --- Libraries/Image/RCTImageStoreManager.h | 27 +++- Libraries/Image/RCTImageStoreManager.m | 210 ++++++++++++++++++------- Libraries/Image/RCTImageUtils.h | 9 ++ Libraries/Image/RCTImageUtils.m | 38 ++++- Libraries/Network/RCTNetworkTask.m | 5 +- 5 files changed, 217 insertions(+), 72 deletions(-) diff --git a/Libraries/Image/RCTImageStoreManager.h b/Libraries/Image/RCTImageStoreManager.h index 9862d1bb2..0d789f1e3 100644 --- a/Libraries/Image/RCTImageStoreManager.h +++ b/Libraries/Image/RCTImageStoreManager.h @@ -3,23 +3,34 @@ #import #import "RCTBridge.h" -#import "RCTImageLoader.h" #import "RCTURLRequestHandler.h" -@interface RCTImageStoreManager : NSObject +@interface RCTImageStoreManager : NSObject /** - * Set and get cached images. These must be called from the main thread. + * Set and get cached image data asynchronously. It is safe to call these from any + * thread. The callbacks will be called on an unspecified thread. */ -- (NSString *)storeImage:(UIImage *)image; -- (UIImage *)imageForTag:(NSString *)imageTag; +- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block; +- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block; +- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block; /** - * Set and get cached images asynchronously. It is safe to call these from any - * thread. The callbacks will be called on the main thread. + * Convenience method to store an image directly (image is converted to data + * internally, so any metadata such as scale or orientation will be lost). */ - (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block; -- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block; + +@end + +@interface RCTImageStoreManager (Deprecated) + +/** + * These methods are deprecated - use the data-based alternatives instead. + */ +- (NSString *)storeImage:(UIImage *)image __deprecated; +- (UIImage *)imageForTag:(NSString *)imageTag __deprecated; +- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block __deprecated; @end diff --git a/Libraries/Image/RCTImageStoreManager.m b/Libraries/Image/RCTImageStoreManager.m index 46074cc37..f34a13e8d 100644 --- a/Libraries/Image/RCTImageStoreManager.m +++ b/Libraries/Image/RCTImageStoreManager.m @@ -9,12 +9,21 @@ #import "RCTImageStoreManager.h" +#import +#import +#import + #import "RCTAssert.h" +#import "RCTImageUtils.h" +#import "RCTLog.h" #import "RCTUtils.h" +static NSString *const RCTImageStoreURLScheme = @"rct-image-store"; + @implementation RCTImageStoreManager { - NSMutableDictionary *_store; + NSMutableDictionary *_store; + NSUInteger *_id; } @synthesize methodQueue = _methodQueue; @@ -24,61 +33,76 @@ RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { - - // TODO: need a way to clear this store _store = [NSMutableDictionary new]; + _id = 0; } return self; } -- (NSString *)storeImage:(UIImage *)image +- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block { - RCTAssertMainThread(); - NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", _store.count]; - _store[tag] = image; - return tag; + dispatch_async(_methodQueue, ^{ + [self removeImageForTag:imageTag]; + if (block) { + block(); + } + }); } -- (UIImage *)imageForTag:(NSString *)imageTag +- (NSString *)_storeImageData:(NSData *)imageData { - RCTAssertMainThread(); - return _store[imageTag]; + RCTAssertThread(_methodQueue, @"Must be called on RCTImageStoreManager thread"); + NSString *imageTag = [NSString stringWithFormat:@"%@://%tu", RCTImageStoreURLScheme, _id++]; + _store[imageTag] = imageData; + return imageTag; +} + +- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block +{ + RCTAssertParam(block); + dispatch_async(_methodQueue, ^{ + block([self _storeImageData:imageData]); + }); +} + +- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block +{ + RCTAssertParam(block); + dispatch_async(_methodQueue, ^{ + block(_store[imageTag]); + }); } - (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block { - dispatch_async(dispatch_get_main_queue(), ^{ - NSString *imageTag = [self storeImage:image]; - if (block) { + RCTAssertParam(block); + dispatch_async(_methodQueue, ^{ + NSString *imageTag = [self _storeImageData:RCTGetImageData(image.CGImage, 0.75)]; + dispatch_async(dispatch_get_main_queue(), ^{ block(imageTag); - } + }); }); } -- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block +RCT_EXPORT_METHOD(removeImageForTag:(NSString *)imageTag) { - RCTAssert(block != nil, @"block must not be nil"); - dispatch_async(dispatch_get_main_queue(), ^{ - block([self imageForTag:imageTag]); - }); + [_store removeObjectForKey:imageTag]; } -// TODO (#5906496): Name could be more explicit - something like getBase64EncodedJPEGDataForTag:? +// TODO (#5906496): Name could be more explicit - something like getBase64EncodedDataForTag:? RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseErrorBlock)errorCallback) { - [self getImageForTag:imageTag withBlock:^(UIImage *image) { - if (!image) { - errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag])); - return; - } - dispatch_async(_methodQueue, ^{ - NSData *imageData = UIImageJPEGRepresentation(image, 1.0); - NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; - successCallback(@[[base64 stringByReplacingOccurrencesOfString:@"\n" withString:@""]]); - }); - }]; + NSData *imageData = _store[imageTag]; + if (!imageData) { + errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag])); + return; + } + // Dispatching to a background thread to perform base64 encoding + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + successCallback(@[[imageData base64EncodedStringWithOptions:0]]); + }); } RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String @@ -86,38 +110,114 @@ RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String errorCallback:(RCTResponseErrorBlock)errorCallback) { - NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0]; - if (imageData) { - UIImage *image = [[UIImage alloc] initWithData:imageData]; - [self storeImage:image withBlock:^(NSString *imageTag) { - successCallback(@[imageTag]); - }]; - } else { - errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String")); + // Dispatching to a background thread to perform base64 decoding + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0]; + if (imageData) { + dispatch_async(_methodQueue, ^{ + successCallback(@[[self _storeImageData:imageData]]); + }); + } else { + errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String")); + } + }); +} + +#pragma mark - RCTURLRequestHandler + +- (BOOL)canHandleRequest:(NSURLRequest *)request +{ + return [request.URL.scheme caseInsensitiveCompare:RCTImageStoreURLScheme] == NSOrderedSame; +} + +- (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate +{ + __block volatile uint32_t cancelled = 0; + void (^cancellationBlock)(void) = ^{ + OSAtomicOr32Barrier(1, &cancelled); + }; + + // Dispatch async to give caller time to cancel the request + dispatch_async(_methodQueue, ^{ + if (cancelled) { + return; + } + + NSString *imageTag = request.URL.absoluteString; + NSData *imageData = _store[imageTag]; + if (!imageData) { + NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + return; + } + + CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); + if (!sourceRef) { + NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Unable to decode data for imageTag: %@", imageTag]); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + return; + } + CFStringRef UTI = CGImageSourceGetType(sourceRef); + CFRelease(sourceRef); + + NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType); + NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL + MIMEType:MIMEType + expectedContentLength:imageData.length + textEncodingName:nil]; + + [delegate URLRequest:cancellationBlock didReceiveResponse:response]; + [delegate URLRequest:cancellationBlock didReceiveData:imageData]; + [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; + + }); + + return cancellationBlock; +} + +- (void)cancelRequest:(id)requestToken +{ + if (requestToken) { + ((void (^)(void))requestToken)(); } } -#pragma mark - RCTImageLoader +@end -- (BOOL)canLoadImageURL:(NSURL *)requestURL +@implementation RCTImageStoreManager (Deprecated) + +- (NSString *)storeImage:(UIImage *)image { - return [requestURL.scheme.lowercaseString isEqualToString:@"rct-image-store"]; + RCTAssertMainThread(); + RCTLogWarn(@"RCTImageStoreManager.storeImage() is deprecated and has poor performance. Use an alternative method instead."); + __block NSString *imageTag; + dispatch_sync(_methodQueue, ^{ + imageTag = [self _storeImageData:RCTGetImageData(image.CGImage, 0.75)]; + }); + return imageTag; } -- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +- (UIImage *)imageForTag:(NSString *)imageTag { - NSString *imageTag = imageURL.absoluteString; - [self getImageForTag:imageTag withBlock:^(UIImage *image) { - if (image) { - completionHandler(nil, image); - } else { - NSString *errorMessage = [NSString stringWithFormat:@"Unable to load image from image store: %@", imageTag]; - NSError *error = RCTErrorWithMessage(errorMessage); - completionHandler(error, nil); - } - }]; + RCTAssertMainThread(); + RCTLogWarn(@"RCTImageStoreManager.imageForTag() is deprecated and has poor performance. Use an alternative method instead."); + __block NSData *imageData; + dispatch_sync(_methodQueue, ^{ + imageData = _store[imageTag]; + }); + return [UIImage imageWithData:imageData]; +} - return nil; +- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block +{ + RCTAssertParam(block); + dispatch_async(_methodQueue, ^{ + NSData *imageData = _store[imageTag]; + UIImage *image = [UIImage imageWithData:imageData]; + dispatch_async(dispatch_get_main_queue(), ^{ + block(image); + }); + }); } @end diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h index e71288f5d..901a876ab 100644 --- a/Libraries/Image/RCTImageUtils.h +++ b/Libraries/Image/RCTImageUtils.h @@ -57,3 +57,12 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data, CGSize destSize, CGFloat destScale, UIViewContentMode resizeMode); + +/** + * 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 + * JPEG. The `quality` argument controls the compression ratio of the JPEG + * conversion, with 1.0 being maximum quality. It has no effect for images + * using PNG compression. + */ +RCT_EXTERN NSData *RCTGetImageData(CGImageRef image, float quality); diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index 2240a3219..c05bf526b 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -10,9 +10,11 @@ #import "RCTImageUtils.h" #import +#import #import #import "RCTLog.h" +#import "RCTUtils.h" static CGFloat RCTCeilValue(CGFloat value, CGFloat scale) { @@ -197,7 +199,7 @@ BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, } } -RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale) +CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale) { return (CGSize){ ceil(pointSize.width * scale), @@ -205,10 +207,10 @@ RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale) }; } -RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data, - CGSize destSize, - CGFloat destScale, - UIViewContentMode resizeMode) +UIImage *RCTDecodeImageWithData(NSData *data, + CGSize destSize, + CGFloat destScale, + UIViewContentMode resizeMode) { CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); if (!sourceRef) { @@ -251,10 +253,9 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data, return nil; } - //adjust scale + // adjust scale size_t actualWidth = CGImageGetWidth(imageRef); CGFloat scale = actualWidth / targetSize.width; - // return image UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale @@ -262,3 +263,26 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data, CGImageRelease(imageRef); return image; } + +NSData *RCTGetImageData(CGImageRef image, float quality) +{ + NSDictionary *properties; + CGImageDestinationRef destination; + CFMutableDataRef imageData = CFDataCreateMutable(NULL, 0); + if (RCTImageHasAlpha(image)) { + // get png data + destination = CGImageDestinationCreateWithData(imageData, kUTTypePNG, 1, NULL); + } else { + // get jpeg data + destination = CGImageDestinationCreateWithData(imageData, kUTTypeJPEG, 1, NULL); + properties = @{(NSString *)kCGImageDestinationLossyCompressionQuality: @(quality)}; + } + CGImageDestinationAddImage(destination, image, (__bridge CFDictionaryRef)properties); + if (!CGImageDestinationFinalize(destination)) + { + CFRelease(imageData); + imageData = NULL; + } + CFRelease(destination); + return (__bridge_transfer NSData *)imageData; +} diff --git a/Libraries/Network/RCTNetworkTask.m b/Libraries/Network/RCTNetworkTask.m index 1c6cfca55..13aba182b 100644 --- a/Libraries/Network/RCTNetworkTask.m +++ b/Libraries/Network/RCTNetworkTask.m @@ -61,8 +61,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)cancel { - if ([_handler respondsToSelector:@selector(cancelRequest:)]) { - [_handler cancelRequest:_requestToken]; + __strong id strongToken = _requestToken; + if (strongToken && [_handler respondsToSelector:@selector(cancelRequest:)]) { + [_handler cancelRequest:strongToken]; } [self invalidate]; }