Skip to content

Commit 2b65700

Browse files
Matthieu Achardfacebook-github-bot-2
authored andcommitted
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 react-native-camera/react-native-camera#100. Closes facebook#3290 Reviewed By: javache Differential Revision: D2647271 Pulled By: nicklockwood fb-gh-sync-id: e66353ae3005423beee72ec22189dcb117fc719f
1 parent 83c9741 commit 2b65700

File tree

5 files changed

+217
-72
lines changed

5 files changed

+217
-72
lines changed

Libraries/Image/RCTImageStoreManager.h

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,34 @@
33
#import <UIKit/UIKit.h>
44

55
#import "RCTBridge.h"
6-
#import "RCTImageLoader.h"
76
#import "RCTURLRequestHandler.h"
87

9-
@interface RCTImageStoreManager : NSObject <RCTImageURLLoader>
8+
@interface RCTImageStoreManager : NSObject <RCTURLRequestHandler>
109

1110
/**
12-
* Set and get cached images. These must be called from the main thread.
11+
* Set and get cached image data asynchronously. It is safe to call these from any
12+
* thread. The callbacks will be called on an unspecified thread.
1313
*/
14-
- (NSString *)storeImage:(UIImage *)image;
15-
- (UIImage *)imageForTag:(NSString *)imageTag;
14+
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block;
15+
- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block;
16+
- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block;
1617

1718
/**
18-
* Set and get cached images asynchronously. It is safe to call these from any
19-
* thread. The callbacks will be called on the main thread.
19+
* Convenience method to store an image directly (image is converted to data
20+
* internally, so any metadata such as scale or orientation will be lost).
2021
*/
2122
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block;
22-
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block;
23+
24+
@end
25+
26+
@interface RCTImageStoreManager (Deprecated)
27+
28+
/**
29+
* These methods are deprecated - use the data-based alternatives instead.
30+
*/
31+
- (NSString *)storeImage:(UIImage *)image __deprecated;
32+
- (UIImage *)imageForTag:(NSString *)imageTag __deprecated;
33+
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block __deprecated;
2334

2435
@end
2536

Libraries/Image/RCTImageStoreManager.m

Lines changed: 155 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@
99

1010
#import "RCTImageStoreManager.h"
1111

12+
#import <ImageIO/ImageIO.h>
13+
#import <libkern/OSAtomic.h>
14+
#import <MobileCoreServices/UTType.h>
15+
1216
#import "RCTAssert.h"
17+
#import "RCTImageUtils.h"
18+
#import "RCTLog.h"
1319
#import "RCTUtils.h"
1420

21+
static NSString *const RCTImageStoreURLScheme = @"rct-image-store";
22+
1523
@implementation RCTImageStoreManager
1624
{
17-
NSMutableDictionary<NSString *, UIImage *> *_store;
25+
NSMutableDictionary<NSString *, NSData *> *_store;
26+
NSUInteger *_id;
1827
}
1928

2029
@synthesize methodQueue = _methodQueue;
@@ -24,100 +33,191 @@ @implementation RCTImageStoreManager
2433
- (instancetype)init
2534
{
2635
if ((self = [super init])) {
27-
28-
// TODO: need a way to clear this store
2936
_store = [NSMutableDictionary new];
37+
_id = 0;
3038
}
3139
return self;
3240
}
3341

34-
- (NSString *)storeImage:(UIImage *)image
42+
- (void)removeImageForTag:(NSString *)imageTag withBlock:(void (^)())block
3543
{
36-
RCTAssertMainThread();
37-
NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", _store.count];
38-
_store[tag] = image;
39-
return tag;
44+
dispatch_async(_methodQueue, ^{
45+
[self removeImageForTag:imageTag];
46+
if (block) {
47+
block();
48+
}
49+
});
4050
}
4151

42-
- (UIImage *)imageForTag:(NSString *)imageTag
52+
- (NSString *)_storeImageData:(NSData *)imageData
4353
{
44-
RCTAssertMainThread();
45-
return _store[imageTag];
54+
RCTAssertThread(_methodQueue, @"Must be called on RCTImageStoreManager thread");
55+
NSString *imageTag = [NSString stringWithFormat:@"%@://%tu", RCTImageStoreURLScheme, _id++];
56+
_store[imageTag] = imageData;
57+
return imageTag;
58+
}
59+
60+
- (void)storeImageData:(NSData *)imageData withBlock:(void (^)(NSString *imageTag))block
61+
{
62+
RCTAssertParam(block);
63+
dispatch_async(_methodQueue, ^{
64+
block([self _storeImageData:imageData]);
65+
});
66+
}
67+
68+
- (void)getImageDataForTag:(NSString *)imageTag withBlock:(void (^)(NSData *imageData))block
69+
{
70+
RCTAssertParam(block);
71+
dispatch_async(_methodQueue, ^{
72+
block(_store[imageTag]);
73+
});
4674
}
4775

4876
- (void)storeImage:(UIImage *)image withBlock:(void (^)(NSString *imageTag))block
4977
{
50-
dispatch_async(dispatch_get_main_queue(), ^{
51-
NSString *imageTag = [self storeImage:image];
52-
if (block) {
78+
RCTAssertParam(block);
79+
dispatch_async(_methodQueue, ^{
80+
NSString *imageTag = [self _storeImageData:RCTGetImageData(image.CGImage, 0.75)];
81+
dispatch_async(dispatch_get_main_queue(), ^{
5382
block(imageTag);
54-
}
83+
});
5584
});
5685
}
5786

58-
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
87+
RCT_EXPORT_METHOD(removeImageForTag:(NSString *)imageTag)
5988
{
60-
RCTAssert(block != nil, @"block must not be nil");
61-
dispatch_async(dispatch_get_main_queue(), ^{
62-
block([self imageForTag:imageTag]);
63-
});
89+
[_store removeObjectForKey:imageTag];
6490
}
6591

66-
// TODO (#5906496): Name could be more explicit - something like getBase64EncodedJPEGDataForTag:?
92+
// TODO (#5906496): Name could be more explicit - something like getBase64EncodedDataForTag:?
6793
RCT_EXPORT_METHOD(getBase64ForTag:(NSString *)imageTag
6894
successCallback:(RCTResponseSenderBlock)successCallback
6995
errorCallback:(RCTResponseErrorBlock)errorCallback)
7096
{
71-
[self getImageForTag:imageTag withBlock:^(UIImage *image) {
72-
if (!image) {
73-
errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]));
74-
return;
75-
}
76-
dispatch_async(_methodQueue, ^{
77-
NSData *imageData = UIImageJPEGRepresentation(image, 1.0);
78-
NSString *base64 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
79-
successCallback(@[[base64 stringByReplacingOccurrencesOfString:@"\n" withString:@""]]);
80-
});
81-
}];
97+
NSData *imageData = _store[imageTag];
98+
if (!imageData) {
99+
errorCallback(RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]));
100+
return;
101+
}
102+
// Dispatching to a background thread to perform base64 encoding
103+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
104+
successCallback(@[[imageData base64EncodedStringWithOptions:0]]);
105+
});
82106
}
83107

84108
RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String
85109
successCallback:(RCTResponseSenderBlock)successCallback
86110
errorCallback:(RCTResponseErrorBlock)errorCallback)
87111

88112
{
89-
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
90-
if (imageData) {
91-
UIImage *image = [[UIImage alloc] initWithData:imageData];
92-
[self storeImage:image withBlock:^(NSString *imageTag) {
93-
successCallback(@[imageTag]);
94-
}];
95-
} else {
96-
errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String"));
97-
}
113+
// Dispatching to a background thread to perform base64 decoding
114+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
115+
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];
116+
if (imageData) {
117+
dispatch_async(_methodQueue, ^{
118+
successCallback(@[[self _storeImageData:imageData]]);
119+
});
120+
} else {
121+
errorCallback(RCTErrorWithMessage(@"Failed to add image from base64String"));
122+
}
123+
});
98124
}
99125

100-
#pragma mark - RCTImageLoader
126+
#pragma mark - RCTURLRequestHandler
101127

102-
- (BOOL)canLoadImageURL:(NSURL *)requestURL
128+
- (BOOL)canHandleRequest:(NSURLRequest *)request
103129
{
104-
return [requestURL.scheme.lowercaseString isEqualToString:@"rct-image-store"];
130+
return [request.URL.scheme caseInsensitiveCompare:RCTImageStoreURLScheme] == NSOrderedSame;
105131
}
106132

107-
- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
133+
- (id)sendRequest:(NSURLRequest *)request withDelegate:(id<RCTURLRequestDelegate>)delegate
108134
{
109-
NSString *imageTag = imageURL.absoluteString;
110-
[self getImageForTag:imageTag withBlock:^(UIImage *image) {
111-
if (image) {
112-
completionHandler(nil, image);
113-
} else {
114-
NSString *errorMessage = [NSString stringWithFormat:@"Unable to load image from image store: %@", imageTag];
115-
NSError *error = RCTErrorWithMessage(errorMessage);
116-
completionHandler(error, nil);
135+
__block volatile uint32_t cancelled = 0;
136+
void (^cancellationBlock)(void) = ^{
137+
OSAtomicOr32Barrier(1, &cancelled);
138+
};
139+
140+
// Dispatch async to give caller time to cancel the request
141+
dispatch_async(_methodQueue, ^{
142+
if (cancelled) {
143+
return;
117144
}
118-
}];
119145

120-
return nil;
146+
NSString *imageTag = request.URL.absoluteString;
147+
NSData *imageData = _store[imageTag];
148+
if (!imageData) {
149+
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]);
150+
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
151+
return;
152+
}
153+
154+
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
155+
if (!sourceRef) {
156+
NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Unable to decode data for imageTag: %@", imageTag]);
157+
[delegate URLRequest:cancellationBlock didCompleteWithError:error];
158+
return;
159+
}
160+
CFStringRef UTI = CGImageSourceGetType(sourceRef);
161+
CFRelease(sourceRef);
162+
163+
NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
164+
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL
165+
MIMEType:MIMEType
166+
expectedContentLength:imageData.length
167+
textEncodingName:nil];
168+
169+
[delegate URLRequest:cancellationBlock didReceiveResponse:response];
170+
[delegate URLRequest:cancellationBlock didReceiveData:imageData];
171+
[delegate URLRequest:cancellationBlock didCompleteWithError:nil];
172+
173+
});
174+
175+
return cancellationBlock;
176+
}
177+
178+
- (void)cancelRequest:(id)requestToken
179+
{
180+
if (requestToken) {
181+
((void (^)(void))requestToken)();
182+
}
183+
}
184+
185+
@end
186+
187+
@implementation RCTImageStoreManager (Deprecated)
188+
189+
- (NSString *)storeImage:(UIImage *)image
190+
{
191+
RCTAssertMainThread();
192+
RCTLogWarn(@"RCTImageStoreManager.storeImage() is deprecated and has poor performance. Use an alternative method instead.");
193+
__block NSString *imageTag;
194+
dispatch_sync(_methodQueue, ^{
195+
imageTag = [self _storeImageData:RCTGetImageData(image.CGImage, 0.75)];
196+
});
197+
return imageTag;
198+
}
199+
200+
- (UIImage *)imageForTag:(NSString *)imageTag
201+
{
202+
RCTAssertMainThread();
203+
RCTLogWarn(@"RCTImageStoreManager.imageForTag() is deprecated and has poor performance. Use an alternative method instead.");
204+
__block NSData *imageData;
205+
dispatch_sync(_methodQueue, ^{
206+
imageData = _store[imageTag];
207+
});
208+
return [UIImage imageWithData:imageData];
209+
}
210+
211+
- (void)getImageForTag:(NSString *)imageTag withBlock:(void (^)(UIImage *image))block
212+
{
213+
RCTAssertParam(block);
214+
dispatch_async(_methodQueue, ^{
215+
NSData *imageData = _store[imageTag];
216+
UIImage *image = [UIImage imageWithData:imageData];
217+
dispatch_async(dispatch_get_main_queue(), ^{
218+
block(image);
219+
});
220+
});
121221
}
122222

123223
@end

Libraries/Image/RCTImageUtils.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,12 @@ RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
5757
CGSize destSize,
5858
CGFloat destScale,
5959
UIViewContentMode resizeMode);
60+
61+
/**
62+
* Convert an image back into data. Images with an alpha channel will be
63+
* converted to lossless PNG data. Images without alpha will be converted to
64+
* JPEG. The `quality` argument controls the compression ratio of the JPEG
65+
* conversion, with 1.0 being maximum quality. It has no effect for images
66+
* using PNG compression.
67+
*/
68+
RCT_EXTERN NSData *RCTGetImageData(CGImageRef image, float quality);

Libraries/Image/RCTImageUtils.m

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
#import "RCTImageUtils.h"
1111

1212
#import <ImageIO/ImageIO.h>
13+
#import <MobileCoreServices/UTCoreTypes.h>
1314
#import <tgmath.h>
1415

1516
#import "RCTLog.h"
17+
#import "RCTUtils.h"
1618

1719
static CGFloat RCTCeilValue(CGFloat value, CGFloat scale)
1820
{
@@ -197,18 +199,18 @@ BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale,
197199
}
198200
}
199201

200-
RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
202+
CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
201203
{
202204
return (CGSize){
203205
ceil(pointSize.width * scale),
204206
ceil(pointSize.height * scale),
205207
};
206208
}
207209

208-
RCT_EXTERN UIImage *RCTDecodeImageWithData(NSData *data,
209-
CGSize destSize,
210-
CGFloat destScale,
211-
UIViewContentMode resizeMode)
210+
UIImage *RCTDecodeImageWithData(NSData *data,
211+
CGSize destSize,
212+
CGFloat destScale,
213+
UIViewContentMode resizeMode)
212214
{
213215
CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
214216
if (!sourceRef) {
@@ -251,14 +253,36 @@ RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
251253
return nil;
252254
}
253255

254-
//adjust scale
256+
// adjust scale
255257
size_t actualWidth = CGImageGetWidth(imageRef);
256258
CGFloat scale = actualWidth / targetSize.width;
257-
258259
// return image
259260
UIImage *image = [UIImage imageWithCGImage:imageRef
260261
scale:scale
261262
orientation:UIImageOrientationUp];
262263
CGImageRelease(imageRef);
263264
return image;
264265
}
266+
267+
NSData *RCTGetImageData(CGImageRef image, float quality)
268+
{
269+
NSDictionary *properties;
270+
CGImageDestinationRef destination;
271+
CFMutableDataRef imageData = CFDataCreateMutable(NULL, 0);
272+
if (RCTImageHasAlpha(image)) {
273+
// get png data
274+
destination = CGImageDestinationCreateWithData(imageData, kUTTypePNG, 1, NULL);
275+
} else {
276+
// get jpeg data
277+
destination = CGImageDestinationCreateWithData(imageData, kUTTypeJPEG, 1, NULL);
278+
properties = @{(NSString *)kCGImageDestinationLossyCompressionQuality: @(quality)};
279+
}
280+
CGImageDestinationAddImage(destination, image, (__bridge CFDictionaryRef)properties);
281+
if (!CGImageDestinationFinalize(destination))
282+
{
283+
CFRelease(imageData);
284+
imageData = NULL;
285+
}
286+
CFRelease(destination);
287+
return (__bridge_transfer NSData *)imageData;
288+
}

0 commit comments

Comments
 (0)