|
10 | 10 | #import "RCTImageDownloader.h" |
11 | 11 |
|
12 | 12 | #import "RCTCache.h" |
| 13 | +#import "RCTLog.h" |
13 | 14 | #import "RCTUtils.h" |
14 | 15 |
|
15 | 16 | typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *error); |
@@ -121,34 +122,134 @@ - (id)downloadDataForURL:(NSURL *)url block:(RCTDataDownloadBlock)block |
121 | 122 | }]; |
122 | 123 | } |
123 | 124 |
|
124 | | -- (id)downloadImageForURL:(NSURL *)url size:(CGSize)size |
125 | | - scale:(CGFloat)scale block:(RCTImageDownloadBlock)block |
| 125 | +/** |
| 126 | + * Returns the optimal context size for an image drawn using the clip rect |
| 127 | + * returned by RCTClipRect. |
| 128 | + */ |
| 129 | +CGSize RCTTargetSizeForClipRect(CGRect); |
| 130 | +CGSize RCTTargetSizeForClipRect(CGRect clipRect) |
| 131 | +{ |
| 132 | + return (CGSize){ |
| 133 | + clipRect.size.width + clipRect.origin.x * 2, |
| 134 | + clipRect.size.height + clipRect.origin.y * 2 |
| 135 | + }; |
| 136 | +} |
| 137 | + |
| 138 | +/** |
| 139 | + * This function takes an input content size & scale (typically from an image), |
| 140 | + * a target size & scale that it will be drawn into (typically a CGContext) and |
| 141 | + * then calculates the optimal rectangle to draw the image into so that it will |
| 142 | + * be sized and positioned correctly if drawn using the specified content mode. |
| 143 | + */ |
| 144 | +CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode); |
| 145 | +CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, |
| 146 | + CGSize destSize, CGFloat destScale, |
| 147 | + UIViewContentMode resizeMode) |
| 148 | +{ |
| 149 | + // Precompensate for scale |
| 150 | + CGFloat scale = sourceScale / destScale; |
| 151 | + sourceSize.width *= scale; |
| 152 | + sourceSize.height *= scale; |
| 153 | + |
| 154 | + // Calculate aspect ratios if needed (don't bother is resizeMode == stretch) |
| 155 | + CGFloat aspect = 0.0, targetAspect = 0.0; |
| 156 | + if (resizeMode != UIViewContentModeScaleToFill) { |
| 157 | + aspect = sourceSize.width / sourceSize.height; |
| 158 | + targetAspect = destSize.width / destSize.height; |
| 159 | + if (aspect == targetAspect) { |
| 160 | + resizeMode = UIViewContentModeScaleToFill; |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + switch (resizeMode) { |
| 165 | + case UIViewContentModeScaleToFill: // stretch |
| 166 | + |
| 167 | + sourceSize.width = MIN(destSize.width, sourceSize.width); |
| 168 | + sourceSize.height = MIN(destSize.height, sourceSize.height); |
| 169 | + return (CGRect){CGPointZero, sourceSize}; |
| 170 | + |
| 171 | + case UIViewContentModeScaleAspectFit: // contain |
| 172 | + |
| 173 | + if (targetAspect <= aspect) { // target is taller than content |
| 174 | + sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); |
| 175 | + sourceSize.height = sourceSize.width / aspect; |
| 176 | + } else { // target is wider than content |
| 177 | + sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); |
| 178 | + sourceSize.width = sourceSize.height * aspect; |
| 179 | + } |
| 180 | + return (CGRect){CGPointZero, sourceSize}; |
| 181 | + |
| 182 | + case UIViewContentModeScaleAspectFill: // cover |
| 183 | + |
| 184 | + if (targetAspect <= aspect) { // target is taller than content |
| 185 | + |
| 186 | + sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); |
| 187 | + sourceSize.width = sourceSize.height * aspect; |
| 188 | + destSize.width = destSize.height * targetAspect; |
| 189 | + return (CGRect){{(destSize.width - sourceSize.width) / 2, 0}, sourceSize}; |
| 190 | + |
| 191 | + } else { // target is wider than content |
| 192 | + |
| 193 | + sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); |
| 194 | + sourceSize.height = sourceSize.width / aspect; |
| 195 | + destSize.height = destSize.width / targetAspect; |
| 196 | + return (CGRect){{0, (destSize.height - sourceSize.height) / 2}, sourceSize}; |
| 197 | + } |
| 198 | + |
| 199 | + default: |
| 200 | + |
| 201 | + RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode); |
| 202 | + return (CGRect){CGPointZero, destSize}; |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +- (id)downloadImageForURL:(NSURL *)url |
| 207 | + size:(CGSize)size |
| 208 | + scale:(CGFloat)scale |
| 209 | + resizeMode:(UIViewContentMode)resizeMode |
| 210 | + backgroundColor:(UIColor *)backgroundColor |
| 211 | + block:(RCTImageDownloadBlock)block |
126 | 212 | { |
127 | 213 | return [self downloadDataForURL:url block:^(NSData *data, NSError *error) { |
128 | 214 |
|
| 215 | + if (!data || error) { |
| 216 | + block(nil, error); |
| 217 | + return; |
| 218 | + } |
| 219 | + |
| 220 | + if (CGSizeEqualToSize(size, CGSizeZero)) { |
| 221 | + // Target size wasn't available yet, so abort image drawing |
| 222 | + block(nil, nil); |
| 223 | + return; |
| 224 | + } |
| 225 | + |
129 | 226 | UIImage *image = [UIImage imageWithData:data scale:scale]; |
130 | 227 | if (image) { |
131 | 228 |
|
132 | | - // Resize (TODO: should we take aspect ratio into account?) |
133 | | - CGSize imageSize = size; |
134 | | - if (CGSizeEqualToSize(imageSize, CGSizeZero)) { |
135 | | - imageSize = image.size; |
136 | | - } else { |
137 | | - imageSize = (CGSize){ |
138 | | - MIN(size.width, image.size.width), |
139 | | - MIN(size.height, image.size.height) |
140 | | - }; |
141 | | - } |
| 229 | + // Get scale and size |
| 230 | + CGFloat destScale = scale ?: RCTScreenScale(); |
| 231 | + CGRect imageRect = RCTClipRect(image.size, image.scale, size, destScale, resizeMode); |
| 232 | + CGSize destSize = RCTTargetSizeForClipRect(imageRect); |
142 | 233 |
|
143 | | - // Rescale image if required size is smaller |
144 | | - CGFloat imageScale = scale; |
145 | | - if (imageScale == 0 || imageScale < image.scale) { |
146 | | - imageScale = image.scale; |
| 234 | + // Opacity optimizations |
| 235 | + UIColor *blendColor = nil; |
| 236 | + BOOL opaque = !RCTImageHasAlpha(image.CGImage); |
| 237 | + if (!opaque && backgroundColor) { |
| 238 | + CGFloat alpha; |
| 239 | + [backgroundColor getRed:NULL green:NULL blue:NULL alpha:&alpha]; |
| 240 | + if (alpha > 0.999) { // no benefit to blending if background is translucent |
| 241 | + opaque = YES; |
| 242 | + blendColor = backgroundColor; |
| 243 | + } |
147 | 244 | } |
148 | 245 |
|
149 | 246 | // Decompress image at required size |
150 | | - UIGraphicsBeginImageContextWithOptions(imageSize, NO, imageScale); |
151 | | - [image drawInRect:(CGRect){{0, 0}, imageSize}]; |
| 247 | + UIGraphicsBeginImageContextWithOptions(destSize, opaque, destScale); |
| 248 | + if (blendColor) { |
| 249 | + [blendColor setFill]; |
| 250 | + UIRectFill((CGRect){CGPointZero, destSize}); |
| 251 | + } |
| 252 | + [image drawInRect:imageRect]; |
152 | 253 | image = UIGraphicsGetImageFromCurrentImageContext(); |
153 | 254 | UIGraphicsEndImageContext(); |
154 | 255 | } |
|
0 commit comments