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
This commit is contained in:
Matthieu Achard 2015-11-17 09:53:47 -08:00 committed by facebook-github-bot-2
parent 83c9741dc9
commit 2b657003b7
5 changed files with 217 additions and 72 deletions

View File

@ -3,23 +3,34 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "RCTBridge.h" #import "RCTBridge.h"
#import "RCTImageLoader.h"
#import "RCTURLRequestHandler.h" #import "RCTURLRequestHandler.h"
@interface RCTImageStoreManager : NSObject <RCTImageURLLoader> @interface RCTImageStoreManager : NSObject <RCTURLRequestHandler>
/** /**
* 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; - (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block;
- (UIImage *)imageForTag:(NSString *)imageTag; - (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 * Convenience method to store an image directly (image is converted to data
* thread. The callbacks will be called on the main thread. * internally, so any metadata such as scale or orientation will be lost).
*/ */
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block; - (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 @end

View File

@ -9,12 +9,21 @@
#import "RCTImageStoreManager.h" #import "RCTImageStoreManager.h"
#import <ImageIO/ImageIO.h>
#import <libkern/OSAtomic.h>
#import <MobileCoreServices/UTType.h>
#import "RCTAssert.h" #import "RCTAssert.h"
#import "RCTImageUtils.h"
#import "RCTLog.h"
#import "RCTUtils.h" #import "RCTUtils.h"
static NSString *const RCTImageStoreURLScheme = @"rct-image-store";
@implementation RCTImageStoreManager @implementation RCTImageStoreManager
{ {
NSMutableDictionary<NSString *, UIImage *> *_store; NSMutableDictionary<NSString *, NSData *> *_store;
NSUInteger *_id;
} }
@synthesize methodQueue = _methodQueue; @synthesize methodQueue = _methodQueue;
@ -24,61 +33,76 @@ RCT_EXPORT_MODULE()
- (instancetype)init - (instancetype)init
{ {
if ((self = [super init])) { if ((self = [super init])) {
// TODO: need a way to clear this store
_store = [NSMutableDictionary new]; _store = [NSMutableDictionary new];
_id = 0;
} }
return self; return self;
} }
- (NSString *)storeImage:(UIImage *)image - (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block
{ {
RCTAssertMainThread(); dispatch_async(_methodQueue, ^{
NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", _store.count]; [self removeImageForTag:imageTag];
_store[tag] = image; if (block) {
return tag; block();
}
});
} }
- (UIImage *)imageForTag:(NSString *)imageTag - (NSString *)_storeImageData:(NSData *)imageData
{ {
RCTAssertMainThread(); RCTAssertThread(_methodQueue, @"Must be called on RCTImageStoreManager thread");
return _store[imageTag]; 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 - (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
{ {
dispatch_async(dispatch_get_main_queue(), ^{ RCTAssertParam(block);
NSString *imageTag = [self storeImage:image]; dispatch_async(_methodQueue, ^{
if (block) { NSString *imageTag = [self _storeImageData:RCTGetImageData(image.CGImage, 0.75)];
dispatch_async(dispatch_get_main_queue(), ^{
block(imageTag); 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"); [_store removeObjectForKey:imageTag];
dispatch_async(dispatch_get_main_queue(), ^{
block([self imageForTag: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 RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag
successCallback:(RCTResponseSenderBlock)successCallback successCallback:(RCTResponseSenderBlock)successCallback
errorCallback:(RCTResponseErrorBlock)errorCallback) errorCallback:(RCTResponseErrorBlock)errorCallback)
{ {
[self getImageForTag:imageTag withBlock:^(UIImage *image) { NSData *imageData = _store[imageTag];
if (!image) { if (!imageData) {
errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag])); errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]));
return; return;
} }
dispatch_async(_methodQueue, ^{ // Dispatching to a background thread to perform base64 encoding
NSData *imageData = UIImageJPEGRepresentation(image, 1.0); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; successCallback(@[[imageData base64EncodedStringWithOptions:0]]);
successCallback(@[[base64 stringByReplacingOccurrencesOfString:@"\n" withString:@""]]); });
});
}];
} }
RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
@ -86,38 +110,114 @@ RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
errorCallback:(RCTResponseErrorBlock)errorCallback) errorCallback:(RCTResponseErrorBlock)errorCallback)
{ {
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0]; // Dispatching to a background thread to perform base64 decoding
if (imageData) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [[UIImage alloc] initWithData:imageData]; NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
[self storeImage:image withBlock:^(NSString *imageTag) { if (imageData) {
successCallback(@[imageTag]); dispatch_async(_methodQueue, ^{
}]; successCallback(@[[self _storeImageData:imageData]]);
} else { });
errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String")); } 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<RCTURLRequestDelegate>)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; RCTAssertMainThread();
[self getImageForTag:imageTag withBlock:^(UIImage *image) { RCTLogWarn(@"RCTImageStoreManager.imageForTag() is deprecated and has poor performance. Use an alternative method instead.");
if (image) { __block NSData *imageData;
completionHandler(nil, image); dispatch_sync(_methodQueue, ^{
} else { imageData = _store[imageTag];
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load image from image store: %@", imageTag]; });
NSError *error = RCTErrorWithMessage(errorMessage); return [UIImage imageWithData:imageData];
completionHandler(error, nil); }
}
}];
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 @end

View File

@ -57,3 +57,12 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
CGSize destSize, CGSize destSize,
CGFloat destScale, CGFloat destScale,
UIViewContentMode resizeMode); 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);

View File

@ -10,9 +10,11 @@
#import "RCTImageUtils.h" #import "RCTImageUtils.h"
#import <ImageIO/ImageIO.h> #import <ImageIO/ImageIO.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <tgmath.h> #import <tgmath.h>
#import "RCTLog.h" #import "RCTLog.h"
#import "RCTUtils.h"
static CGFloat RCTCeilValue(CGFloat value, CGFloat scale) 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){ return (CGSize){
ceil(pointSize.width * scale), ceil(pointSize.width * scale),
@ -205,10 +207,10 @@ RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
}; };
} }
RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data, UIImage *RCTDecodeImageWithData(NSData *data,
CGSize destSize, CGSize destSize,
CGFloat destScale, CGFloat destScale,
UIViewContentMode resizeMode) UIViewContentMode resizeMode)
{ {
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
if (!sourceRef) { if (!sourceRef) {
@ -251,10 +253,9 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
return nil; return nil;
} }
//adjust scale // adjust scale
size_t actualWidth = CGImageGetWidth(imageRef); size_t actualWidth = CGImageGetWidth(imageRef);
CGFloat scale = actualWidth / targetSize.width; CGFloat scale = actualWidth / targetSize.width;
// return image // return image
UIImage *image = [UIImage imageWithCGImage:imageRef UIImage *image = [UIImage imageWithCGImage:imageRef
scale:scale scale:scale
@ -262,3 +263,26 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
CGImageRelease(imageRef); CGImageRelease(imageRef);
return image; 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;
}

View File

@ -61,8 +61,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
- (void)cancel - (void)cancel
{ {
if ([_handler respondsToSelector:@selector(cancelRequest:)]) { __strong id strongToken = _requestToken;
[_handler cancelRequest:_requestToken]; if (strongToken && [_handler respondsToSelector:@selector(cancelRequest:)]) {
[_handler cancelRequest:strongToken];
} }
[self invalidate]; [self invalidate];
} }