diff --git a/Libraries/Image/RCTImageStoreManager.h b/Libraries/Image/RCTImageStoreManager.h index 9862d1bb263..0d789f1e378 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 46074cc3722..f34a13e8d65 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 e71288f5d09..901a876ab02 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 2240a3219db..c05bf526b96 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 1c6cfca551b..13aba182b9c 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]; }