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 "RCTBridge.h"
#import "RCTImageLoader.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;
- (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

View File

@ -9,12 +9,21 @@
#import "RCTImageStoreManager.h"
#import <ImageIO/ImageIO.h>
#import <libkern/OSAtomic.h>
#import <MobileCoreServices/UTType.h>
#import "RCTAssert.h"
#import "RCTImageUtils.h"
#import "RCTLog.h"
#import "RCTUtils.h"
static NSString *const RCTImageStoreURLScheme = @"rct-image-store";
@implementation RCTImageStoreManager
{
NSMutableDictionary<NSString *, UIImage *> *_store;
NSMutableDictionary<NSString *, NSData *> *_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<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;
[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

View File

@ -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);

View File

@ -10,9 +10,11 @@
#import "RCTImageUtils.h"
#import <ImageIO/ImageIO.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <tgmath.h>
#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;
}

View File

@ -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];
}