forked from facebook/react-native
/
RCTImageDownloader.m
executable file
·270 lines (226 loc) · 8.87 KB
/
RCTImageDownloader.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTImageDownloader.h"
#import "RCTLog.h"
#import "RCTUtils.h"
#import "RCTDownloadTaskWrapper.h"
typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *error);
CGSize RCTTargetSizeForClipRect(CGRect);
CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode);
@implementation RCTImageDownloader
{
NSURLCache *_cache;
dispatch_queue_t _processingQueue;
NSMutableDictionary *_pendingBlocks;
}
+ (RCTImageDownloader *)sharedInstance
{
static RCTImageDownloader *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[RCTImageDownloader alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
if ((self = [super init])) {
_cache = [[NSURLCache alloc] initWithMemoryCapacity:5 * 1024 * 1024 diskCapacity:200 * 1024 * 1024 diskPath:@"React/RCTImageDownloader"];
_processingQueue = dispatch_queue_create("com.facebook.React.DownloadProcessingQueue", DISPATCH_QUEUE_SERIAL);
_pendingBlocks = [[NSMutableDictionary alloc] init];
}
return self;
}
- (id)_downloadDataForURL:(NSURL *)url progressBlock:progressBlock block:(RCTCachedDataDownloadBlock)block
{
NSString *cacheKey = url.absoluteString;
__block BOOL cancelled = NO;
__block NSURLSessionDownloadTask *task = nil;
RCTDownloadTaskWrapper *_downloadTaskWrapper = [[RCTDownloadTaskWrapper alloc]
initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegateQueue:nil];
dispatch_block_t cancel = ^{
cancelled = YES;
dispatch_async(_processingQueue, ^{
NSMutableArray *pendingBlocks = self->_pendingBlocks[cacheKey];
[pendingBlocks removeObject:block];
});
if (task) {
[task cancel];
task = nil;
}
};
dispatch_async(_processingQueue, ^{
NSMutableArray *pendingBlocks = _pendingBlocks[cacheKey];
if (pendingBlocks) {
[pendingBlocks addObject:block];
} else {
_pendingBlocks[cacheKey] = [NSMutableArray arrayWithObject:block];
__weak RCTImageDownloader *weakSelf = self;
RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSData *data, NSError *error) {
dispatch_async(_processingQueue, ^{
RCTImageDownloader *strongSelf = weakSelf;
NSArray *blocks = strongSelf->_pendingBlocks[cacheKey];
[strongSelf->_pendingBlocks removeObjectForKey:cacheKey];
for (RCTCachedDataDownloadBlock downloadBlock in blocks) {
downloadBlock(cached, data, error);
}
});
};
NSURLRequest *request = [NSURLRequest requestWithURL:url];
task = [_downloadTaskWrapper downloadData:url progressBlock:progressBlock
completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!cancelled) {
runBlocks(NO, data, error);
}
if (response) {
RCTImageDownloader *strongSelf = weakSelf;
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:data userInfo:nil storagePolicy:NSURLCacheStorageAllowed];
[strongSelf->_cache storeCachedResponse:cachedResponse forRequest:request];
}
task = nil;
}];
NSCachedURLResponse *cachedResponse = [_cache cachedResponseForRequest:request];
if (cancelled) {
return;
}
if (cachedResponse) {
runBlocks(YES, cachedResponse.data, nil);
} else {
[task resume];
}
}
});
return [cancel copy];
}
- (id)downloadDataForURL:(NSURL *)url progressBlock:(RCTDataProgressBlock)progressBlock block:(RCTDataDownloadBlock)block
{
return [self _downloadDataForURL:url progressBlock:progressBlock block:^(BOOL cached, NSData *data, NSError *error) {
block(data, error);
}];
}
- (id)downloadImageForURL:(NSURL *)url
size:(CGSize)size
scale:(CGFloat)scale
resizeMode:(UIViewContentMode)resizeMode
backgroundColor:(UIColor *)backgroundColor
progressBlock:(RCTDataProgressBlock)progressBlock
block:(RCTImageDownloadBlock)block
{
return [self downloadDataForURL:url progressBlock:progressBlock block:^(NSData *data, NSError *error) {
if (!data || error) {
block(nil, error);
return;
}
if (CGSizeEqualToSize(size, CGSizeZero)) {
// Target size wasn't available yet, so abort image drawing
block(nil, nil);
return;
}
UIImage *image = [UIImage imageWithData:data scale:scale];
if (image) {
// Get scale and size
CGFloat destScale = scale ?: RCTScreenScale();
CGRect imageRect = RCTClipRect(image.size, image.scale, size, destScale, resizeMode);
CGSize destSize = RCTTargetSizeForClipRect(imageRect);
// Opacity optimizations
UIColor *blendColor = nil;
BOOL opaque = !RCTImageHasAlpha(image.CGImage);
if (!opaque && backgroundColor) {
CGFloat alpha;
[backgroundColor getRed:NULL green:NULL blue:NULL alpha:&alpha];
if (alpha > 0.999) { // no benefit to blending if background is translucent
opaque = YES;
blendColor = backgroundColor;
}
}
// Decompress image at required size
UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale);
if (blendColor) {
[blendColor setFill];
UIRectFill((CGRect){CGPointZero, destSize});
}
[image drawInRect:imageRect];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
block(image, nil);
}];
}
- (void)cancelDownload:(id)downloadToken
{
if (downloadToken) {
((dispatch_block_t)downloadToken)();
}
}
@end
/**
* Returns the optimal context size for an image drawn using the clip rect
* returned by RCTClipRect.
*/
CGSize RCTTargetSizeForClipRect(CGRect clipRect)
{
return (CGSize){
clipRect.size.width + clipRect.origin.x * 2,
clipRect.size.height + clipRect.origin.y * 2
};
}
/**
* This function takes an input content size & scale (typically from an image),
* a target size & scale that it will be drawn into (typically a CGContext) and
* then calculates the optimal rectangle to draw the image into so that it will
* be sized and positioned correctly if drawn using the specified content mode.
*/
CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale,
CGSize destSize, CGFloat destScale,
UIViewContentMode resizeMode)
{
// Precompensate for scale
CGFloat scale = sourceScale / destScale;
sourceSize.width *= scale;
sourceSize.height *= scale;
// Calculate aspect ratios if needed (don't bother is resizeMode == stretch)
CGFloat aspect = 0.0, targetAspect = 0.0;
if (resizeMode != UIViewContentModeScaleToFill) {
aspect = sourceSize.width / sourceSize.height;
targetAspect = destSize.width / destSize.height;
if (aspect == targetAspect) {
resizeMode = UIViewContentModeScaleToFill;
}
}
switch (resizeMode) {
case UIViewContentModeScaleToFill: // stretch
sourceSize.width = MIN(destSize.width, sourceSize.width);
sourceSize.height = MIN(destSize.height, sourceSize.height);
return (CGRect){CGPointZero, sourceSize};
case UIViewContentModeScaleAspectFit: // contain
if (targetAspect <= aspect) { // target is taller than content
sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width);
sourceSize.height = sourceSize.width / aspect;
} else { // target is wider than content
sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height);
sourceSize.width = sourceSize.height * aspect;
}
return (CGRect){CGPointZero, sourceSize};
case UIViewContentModeScaleAspectFill: // cover
if (targetAspect <= aspect) { // target is taller than content
sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height);
sourceSize.width = sourceSize.height * aspect;
destSize.width = destSize.height * targetAspect;
return (CGRect){{(destSize.width - sourceSize.width) / 2, 0}, sourceSize};
} else { // target is wider than content
sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width);
sourceSize.height = sourceSize.width / aspect;
destSize.height = destSize.width / targetAspect;
return (CGRect){{0, (destSize.height - sourceSize.height) / 2}, sourceSize};
}
default:
RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode);
return (CGRect){CGPointZero, destSize};
}
}