From 21fcbbc32cc822f6d2db3f099082cac6b225cb48 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 20 Jan 2016 11:03:22 -0800 Subject: [PATCH] Generalized image decoding and resizing logic Summary: public Standardises the image decoding logic for all image sources, meaning we get the benefits of efficient downscaling of images from all sources, not just ALAssets. Reviewed By: javache Differential Revision: D2647083 fb-gh-sync-id: e41456f838e4c6ab709b1c1523f651a86ff6e623 --- Examples/UIExplorer/ImageCapInsetsExample.js | 1 + .../xcschemes/UIExplorer.xcscheme | 10 +- .../RCTImageLoaderHelpers.h | 4 +- .../RCTImageLoaderHelpers.m | 4 +- .../UIExplorerUnitTests/RCTImageLoaderTests.m | 20 +-- .../UIExplorerUnitTests/RCTImageUtilTests.m | 28 +-- .../CameraRoll/RCTAssetsLibraryImageLoader.m | 168 ------------------ ...der.h => RCTAssetsLibraryRequestHandler.h} | 7 +- .../RCTAssetsLibraryRequestHandler.m | 114 ++++++++++++ .../RCTCameraRoll.xcodeproj/project.pbxproj | 12 +- Libraries/CameraRoll/RCTCameraRollManager.m | 2 +- .../CameraRoll/RCTPhotoLibraryImageLoader.m | 4 +- Libraries/Image/RCTGIFImageDecoder.m | 2 +- .../Image/RCTImage.xcodeproj/project.pbxproj | 12 +- Libraries/Image/RCTImageEditingManager.m | 66 ++----- Libraries/Image/RCTImageLoader.h | 19 +- Libraries/Image/RCTImageLoader.m | 119 +++++++++++-- Libraries/Image/RCTImageStoreManager.m | 1 + Libraries/Image/RCTImageUtils.h | 46 +++-- Libraries/Image/RCTImageUtils.m | 110 ++++++++---- Libraries/Image/RCTImageView.h | 1 + Libraries/Image/RCTImageView.m | 53 +++--- Libraries/Image/RCTImageViewManager.m | 2 +- Libraries/Image/RCTResizeMode.h | 22 +++ Libraries/Image/RCTResizeMode.m | 20 +++ Libraries/Image/RCTShadowVirtualImage.m | 2 +- Libraries/Image/RCTXCAssetImageLoader.m | 2 +- React/Base/RCTUtils.h | 6 +- React/Base/RCTUtils.m | 28 +-- React/Views/RCTComponentData.m | 4 + 30 files changed, 510 insertions(+), 379 deletions(-) delete mode 100644 Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m rename Libraries/CameraRoll/{RCTAssetsLibraryImageLoader.h => RCTAssetsLibraryRequestHandler.h} (75%) create mode 100644 Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m create mode 100644 Libraries/Image/RCTResizeMode.h create mode 100644 Libraries/Image/RCTResizeMode.m diff --git a/Examples/UIExplorer/ImageCapInsetsExample.js b/Examples/UIExplorer/ImageCapInsetsExample.js index 8c6ab243e..f75f7da98 100644 --- a/Examples/UIExplorer/ImageCapInsetsExample.js +++ b/Examples/UIExplorer/ImageCapInsetsExample.js @@ -46,6 +46,7 @@ var ImageCapInsetsExample = React.createClass({ diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index e2f84182e..52560f0d6 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -1,7 +1,7 @@ + version = "1.3"> @@ -51,10 +51,10 @@ + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -90,11 +90,11 @@ diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.h b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.h index 6ee68b5c3..fe68f0592 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.h +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.h @@ -15,7 +15,7 @@ #import "RCTImageLoader.h" typedef BOOL (^RCTImageURLLoaderCanLoadImageURLHandler)(NSURL *requestURL); -typedef RCTImageLoaderCancellationBlock (^RCTImageURLLoaderLoadImageURLHandler)(NSURL *imageURL, CGSize size, CGFloat scale, UIViewContentMode resizeMode, RCTImageLoaderProgressBlock progressHandler, RCTImageLoaderCompletionBlock completionHandler); +typedef RCTImageLoaderCancellationBlock (^RCTImageURLLoaderLoadImageURLHandler)(NSURL *imageURL, CGSize size, CGFloat scale, RCTResizeMode resizeMode, RCTImageLoaderProgressBlock progressHandler, RCTImageLoaderCompletionBlock completionHandler); @interface RCTConcreteImageURLLoader : NSObject @@ -26,7 +26,7 @@ typedef RCTImageLoaderCancellationBlock (^RCTImageURLLoaderLoadImageURLHandler)( @end typedef BOOL (^RCTImageDataDecoderCanDecodeImageDataHandler)(NSData *imageData); -typedef RCTImageLoaderCancellationBlock (^RCTImageDataDecoderDecodeImageDataHandler)(NSData *imageData, CGSize size, CGFloat scale, UIViewContentMode resizeMode, RCTImageLoaderCompletionBlock completionHandler); +typedef RCTImageLoaderCancellationBlock (^RCTImageDataDecoderDecodeImageDataHandler)(NSData *imageData, CGSize size, CGFloat scale, RCTResizeMode resizeMode, RCTImageLoaderCompletionBlock completionHandler); @interface RCTConcreteImageDecoder : NSObject diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.m index e41b09f3d..e3b4cb086 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderHelpers.m @@ -47,7 +47,7 @@ return _canLoadImageURLHandler(requestURL); } -- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { return _loadImageURLHandler(imageURL, size, scale, resizeMode, progressHandler, completionHandler); } @@ -92,7 +92,7 @@ return _canDecodeImageDataHandler(imageData); } -- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale resizeMode:(UIViewContentMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { return _decodeImageDataHandler(imageData, size, scale, resizeMode, completionHandler); } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m index 04f367312..5713d0347 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageLoaderTests.m @@ -41,7 +41,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) id loader = [[RCTImageLoaderTestsURLLoader1 alloc] initWithPriority:1.0 canLoadImageURLHandler:^BOOL(__unused NSURL *requestURL) { return YES; - } loadImageURLHandler:^RCTImageLoaderCancellationBlock(__unused NSURL *imageURL, __unused CGSize size, __unused CGFloat scale, __unused UIViewContentMode resizeMode, RCTImageLoaderProgressBlock progressHandler, RCTImageLoaderCompletionBlock completionHandler) { + } loadImageURLHandler:^RCTImageLoaderCancellationBlock(__unused NSURL *imageURL, __unused CGSize size, __unused CGFloat scale, __unused RCTResizeMode resizeMode, RCTImageLoaderProgressBlock progressHandler, RCTImageLoaderCompletionBlock completionHandler) { progressHandler(1, 1); completionHandler(nil, image); return nil; @@ -50,7 +50,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) RCTImageLoader *imageLoader = [RCTImageLoader new]; NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[loader, imageLoader]; } launchOptions:nil]; - [imageLoader loadImageWithTag:@"http://facebook.github.io/react/img/logo_og.png" size:CGSizeMake(100, 100) scale:1.0 resizeMode:UIViewContentModeScaleAspectFit progressBlock:^(int64_t progress, int64_t total) { + [imageLoader loadImageWithTag:@"http://facebook.github.io/react/img/logo_og.png" size:CGSizeMake(100, 100) scale:1.0 resizeMode:RCTResizeModeContain progressBlock:^(int64_t progress, int64_t total) { XCTAssertEqual(progress, 1); XCTAssertEqual(total, 1); } completionBlock:^(NSError *loadError, id loadedImage) { @@ -65,7 +65,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) id loader1 = [[RCTImageLoaderTestsURLLoader1 alloc] initWithPriority:1.0 canLoadImageURLHandler:^BOOL(__unused NSURL *requestURL) { return YES; - } loadImageURLHandler:^RCTImageLoaderCancellationBlock(__unused NSURL *imageURL, __unused CGSize size, __unused CGFloat scale, __unused UIViewContentMode resizeMode, RCTImageLoaderProgressBlock progressHandler, RCTImageLoaderCompletionBlock completionHandler) { + } loadImageURLHandler:^RCTImageLoaderCancellationBlock(__unused NSURL *imageURL, __unused CGSize size, __unused CGFloat scale, __unused RCTResizeMode resizeMode, RCTImageLoaderProgressBlock progressHandler, RCTImageLoaderCompletionBlock completionHandler) { progressHandler(1, 1); completionHandler(nil, image); return nil; @@ -73,7 +73,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) id loader2 = [[RCTImageLoaderTestsURLLoader2 alloc] initWithPriority:0.5 canLoadImageURLHandler:^BOOL(__unused NSURL *requestURL) { return YES; - } loadImageURLHandler:^RCTImageLoaderCancellationBlock(__unused NSURL *imageURL, __unused CGSize size, __unused CGFloat scale, __unused UIViewContentMode resizeMode, __unused RCTImageLoaderProgressBlock progressHandler, __unused RCTImageLoaderCompletionBlock completionHandler) { + } loadImageURLHandler:^RCTImageLoaderCancellationBlock(__unused NSURL *imageURL, __unused CGSize size, __unused CGFloat scale, __unused RCTResizeMode resizeMode, __unused RCTImageLoaderProgressBlock progressHandler, __unused RCTImageLoaderCompletionBlock completionHandler) { XCTFail(@"Should not have used loader2"); return nil; }]; @@ -81,7 +81,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) RCTImageLoader *imageLoader = [RCTImageLoader new]; NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[loader1, loader2, imageLoader]; } launchOptions:nil]; - [imageLoader loadImageWithTag:@"http://facebook.github.io/react/img/logo_og.png" size:CGSizeMake(100, 100) scale:1.0 resizeMode:UIViewContentModeScaleAspectFit progressBlock:^(int64_t progress, int64_t total) { + [imageLoader loadImageWithTag:@"http://facebook.github.io/react/img/logo_og.png" size:CGSizeMake(100, 100) scale:1.0 resizeMode:RCTResizeModeContain progressBlock:^(int64_t progress, int64_t total) { XCTAssertEqual(progress, 1); XCTAssertEqual(total, 1); } completionBlock:^(NSError *loadError, id loadedImage) { @@ -97,7 +97,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) id decoder = [[RCTImageLoaderTestsDecoder1 alloc] initWithPriority:1.0 canDecodeImageDataHandler:^BOOL(__unused NSData *imageData) { return YES; - } decodeImageDataHandler:^RCTImageLoaderCancellationBlock(NSData *imageData, __unused CGSize size, __unused CGFloat scale, __unused UIViewContentMode resizeMode, RCTImageLoaderCompletionBlock completionHandler) { + } decodeImageDataHandler:^RCTImageLoaderCancellationBlock(NSData *imageData, __unused CGSize size, __unused CGFloat scale, __unused RCTResizeMode resizeMode, RCTImageLoaderCompletionBlock completionHandler) { XCTAssertEqualObjects(imageData, data); completionHandler(nil, image); return nil; @@ -106,7 +106,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) RCTImageLoader *imageLoader = [RCTImageLoader new]; NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[decoder, imageLoader]; } launchOptions:nil]; - RCTImageLoaderCancellationBlock cancelBlock = [imageLoader decodeImageData:data size:CGSizeMake(1, 1) scale:1.0 resizeMode:UIViewContentModeScaleToFill completionBlock:^(NSError *decodeError, id decodedImage) { + RCTImageLoaderCancellationBlock cancelBlock = [imageLoader decodeImageData:data size:CGSizeMake(1, 1) scale:1.0 resizeMode:RCTResizeModeStretch completionBlock:^(NSError *decodeError, id decodedImage) { XCTAssertEqualObjects(decodedImage, image); XCTAssertNil(decodeError); }]; @@ -120,7 +120,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) id decoder1 = [[RCTImageLoaderTestsDecoder1 alloc] initWithPriority:1.0 canDecodeImageDataHandler:^BOOL(__unused NSData *imageData) { return YES; - } decodeImageDataHandler:^RCTImageLoaderCancellationBlock(NSData *imageData, __unused CGSize size, __unused CGFloat scale, __unused UIViewContentMode resizeMode, RCTImageLoaderCompletionBlock completionHandler) { + } decodeImageDataHandler:^RCTImageLoaderCancellationBlock(NSData *imageData, __unused CGSize size, __unused CGFloat scale, __unused RCTResizeMode resizeMode, RCTImageLoaderCompletionBlock completionHandler) { XCTAssertEqualObjects(imageData, data); completionHandler(nil, image); return nil; @@ -128,7 +128,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) id decoder2 = [[RCTImageLoaderTestsDecoder2 alloc] initWithPriority:0.5 canDecodeImageDataHandler:^BOOL(__unused NSData *imageData) { return YES; - } decodeImageDataHandler:^RCTImageLoaderCancellationBlock(__unused NSData *imageData, __unused CGSize size, __unused CGFloat scale, __unused UIViewContentMode resizeMode, __unused RCTImageLoaderCompletionBlock completionHandler) { + } decodeImageDataHandler:^RCTImageLoaderCancellationBlock(__unused NSData *imageData, __unused CGSize size, __unused CGFloat scale, __unused RCTResizeMode resizeMode, __unused RCTImageLoaderCompletionBlock completionHandler) { XCTFail(@"Should not have used decoder2"); return nil; }]; @@ -136,7 +136,7 @@ RCTDefineImageDecoder(RCTImageLoaderTestsDecoder2) RCTImageLoader *imageLoader = [RCTImageLoader new]; NS_VALID_UNTIL_END_OF_SCOPE RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[decoder1, decoder2, imageLoader]; } launchOptions:nil]; - RCTImageLoaderCancellationBlock cancelBlock = [imageLoader decodeImageData:data size:CGSizeMake(1, 1) scale:1.0 resizeMode:UIViewContentModeScaleToFill completionBlock:^(NSError *decodeError, id decodedImage) { + RCTImageLoaderCancellationBlock cancelBlock = [imageLoader decodeImageData:data size:CGSizeMake(1, 1) scale:1.0 resizeMode:RCTResizeModeStretch completionBlock:^(NSError *decodeError, id decodedImage) { XCTAssertEqualObjects(decodedImage, image); XCTAssertNil(decodeError); }]; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m index f1effc8d6..19229d045 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m @@ -46,19 +46,19 @@ RCTAssertEqualSizes(a.size, b.size); \ { CGRect expected = {CGPointZero, {100, 20}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeStretch); RCTAssertEqualRects(expected, result); } { - CGRect expected = {CGPointZero, {100, 10}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); + CGRect expected = {{0, 5}, {100, 10}}; + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeContain); RCTAssertEqualRects(expected, result); } { CGRect expected = {{-50, 0}, {200, 20}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeCover); RCTAssertEqualRects(expected, result); } } @@ -70,19 +70,19 @@ RCTAssertEqualSizes(a.size, b.size); \ { CGRect expected = {CGPointZero, {100, 20}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeStretch); RCTAssertEqualRects(expected, result); } { - CGRect expected = {CGPointZero, {2, 20}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); + CGRect expected = {{49, 0}, {2, 20}}; + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeContain); RCTAssertEqualRects(expected, result); } { CGRect expected = {{0, -490}, {100, 1000}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeCover); RCTAssertEqualRects(expected, result); } } @@ -94,19 +94,19 @@ RCTAssertEqualSizes(a.size, b.size); \ { CGRect expected = {CGPointZero, {20, 50}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeStretch); RCTAssertEqualRects(expected, result); } { - CGRect expected = {CGPointZero, {5, 50}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); + CGRect expected = {{7,0}, {5, 50}}; + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeContain); RCTAssertEqualRects(expected, result); } { CGRect expected = {{0, -75}, {20, 200}}; - CGRect result = RCTTargetRect(content, target, 2, UIViewContentModeScaleAspectFill); + CGRect result = RCTTargetRect(content, target, 2, RCTResizeModeCover); RCTAssertEqualRects(expected, result); } } @@ -118,7 +118,7 @@ RCTAssertEqualSizes(a.size, b.size); \ { CGRect expected = {{0, -75}, {20, 200}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeCover); RCTAssertEqualRects(expected, result); } } @@ -129,7 +129,7 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {3, 3}; CGRect expected = {CGPointZero, {3, 3}}; - CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, RCTResizeModeStretch); RCTAssertEqualRects(expected, result); } diff --git a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m b/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m deleted file mode 100644 index f5cd3cb25..000000000 --- a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.m +++ /dev/null @@ -1,168 +0,0 @@ -/** - * 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 "RCTAssetsLibraryImageLoader.h" - -#import -#import -#import -#import - -#import "RCTBridge.h" -#import "RCTConvert.h" -#import "RCTImageLoader.h" -#import "RCTImageUtils.h" -#import "RCTUtils.h" - -static dispatch_queue_t RCTAssetsLibraryImageLoaderQueue(void); - -@implementation RCTAssetsLibraryImageLoader -{ - ALAssetsLibrary *_assetsLibrary; -} - -RCT_EXPORT_MODULE() - -@synthesize bridge = _bridge; - -- (ALAssetsLibrary *)assetsLibrary -{ - return _assetsLibrary ?: (_assetsLibrary = [ALAssetsLibrary new]); -} - -#pragma mark - RCTImageLoader - -- (BOOL)canLoadImageURL:(NSURL *)requestURL -{ - return [requestURL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame; -} - -- (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL - size:(CGSize)size - scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode - progressHandler:(RCTImageLoaderProgressBlock)progressHandler - completionHandler:(RCTImageLoaderCompletionBlock)completionHandler -{ - __block volatile uint32_t cancelled = 0; - - [[self assetsLibrary] assetForURL:imageURL resultBlock:^(ALAsset *asset) { - if (cancelled) { - return; - } - - if (asset) { - // ALAssetLibrary API is async and will be multi-threaded. Loading a few full - // resolution images at once will spike the memory up to store the image data, - // and might trigger memory warnings and/or OOM crashes. - // To improve this, process the loaded asset in a serial queue. - dispatch_async(RCTAssetsLibraryImageLoaderQueue(), ^{ - if (cancelled) { - return; - } - - // Also make sure the image is released immediately after it's used so it - // doesn't spike the memory up during the process. - @autoreleasepool { - BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); - ALAssetRepresentation *representation = [asset defaultRepresentation]; - -#if RCT_DEV - - CGSize sizeBeingLoaded = size; - if (useMaximumSize) { - CGSize pointSize = representation.dimensions; - sizeBeingLoaded = CGSizeMake(pointSize.width * representation.scale, pointSize.height * representation.scale); - } - - CGSize screenSize; - if ([[[UIDevice currentDevice] systemVersion] compare:@"8.0" options:NSNumericSearch] == NSOrderedDescending) { - screenSize = [UIScreen mainScreen].nativeBounds.size; - } else { - CGSize mainScreenSize = [UIScreen mainScreen].bounds.size; - CGFloat mainScreenScale = [[UIScreen mainScreen] scale]; - screenSize = CGSizeMake(mainScreenSize.width * mainScreenScale, mainScreenSize.height * mainScreenScale); - } - CGFloat maximumPixelDimension = fmax(screenSize.width, screenSize.height); - - if (sizeBeingLoaded.width > maximumPixelDimension || sizeBeingLoaded.height > maximumPixelDimension) { - RCTLogInfo(@"[PERF ASSETS] Loading %@ at size %@, which is larger than screen size %@", - representation.filename, NSStringFromCGSize(sizeBeingLoaded), NSStringFromCGSize(screenSize)); - } - -#endif - - UIImage *image = nil; - NSError *error = nil; - if (useMaximumSize) { - - image = [UIImage imageWithCGImage:representation.fullResolutionImage - scale:scale - orientation:(UIImageOrientation)representation.orientation]; - } else { - - NSUInteger length = (NSUInteger)representation.size; - uint8_t *buffer = (uint8_t *)malloc((size_t)length); - if ([representation getBytes:buffer - fromOffset:0 - length:length - error:&error]) { - - NSData *data = [[NSData alloc] initWithBytesNoCopy:buffer - length:length - freeWhenDone:YES]; - - image = RCTDecodeImageWithData(data, size, scale, resizeMode); - } else { - free(buffer); - } - } - - completionHandler(error, image); - } - }); - } else { - NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageURL]; - completionHandler(RCTErrorWithMessage(errorText), nil); - } - } failureBlock:^(NSError *loadError) { - if (cancelled) { - return; - } - - NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@.\niOS Error: %@", imageURL, loadError]; - completionHandler(RCTErrorWithMessage(errorText), nil); - }]; - - return ^{ - OSAtomicOr32Barrier(1, &cancelled); - }; -} - -@end - -@implementation RCTBridge (RCTAssetsLibraryImageLoader) - -- (ALAssetsLibrary *)assetsLibrary -{ - return [[self moduleForClass:[RCTAssetsLibraryImageLoader class]] assetsLibrary]; -} - -@end - -static dispatch_queue_t RCTAssetsLibraryImageLoaderQueue(void) -{ - static dispatch_queue_t queue; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - queue = dispatch_queue_create("com.facebook.RCTAssetsLibraryImageLoader", DISPATCH_QUEUE_SERIAL); - }); - - return queue; -} diff --git a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h similarity index 75% rename from Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h rename to Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h index 0cc1c5353..4ce68bc3b 100644 --- a/Libraries/CameraRoll/RCTAssetsLibraryImageLoader.h +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.h @@ -7,9 +7,12 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTImageLoader.h" +#import "RCTBridge.h" +#import "RCTURLRequestHandler.h" -@interface RCTAssetsLibraryImageLoader : NSObject +@class ALAssetsLibrary; + +@interface RCTAssetsLibraryRequestHandler : NSObject @end diff --git a/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m new file mode 100644 index 000000000..d818f336d --- /dev/null +++ b/Libraries/CameraRoll/RCTAssetsLibraryRequestHandler.m @@ -0,0 +1,114 @@ +/** + * 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 "RCTAssetsLibraryRequestHandler.h" + +#import +#import + +#import "RCTBridge.h" +#import "RCTUtils.h" + +@implementation RCTAssetsLibraryRequestHandler +{ + ALAssetsLibrary *_assetsLibrary; +} + +RCT_EXPORT_MODULE() + +@synthesize bridge = _bridge; + +- (ALAssetsLibrary *)assetsLibrary +{ + return _assetsLibrary ?: (_assetsLibrary = [ALAssetsLibrary new]); +} + +#pragma mark - RCTURLRequestHandler + +- (BOOL)canHandleRequest:(NSURLRequest *)request +{ + return [request.URL.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame; +} + +- (id)sendRequest:(NSURLRequest *)request + withDelegate:(id)delegate +{ + __block volatile uint32_t cancelled = 0; + void (^cancellationBlock)(void) = ^{ + OSAtomicOr32Barrier(1, &cancelled); + }; + + [[self assetsLibrary] assetForURL:request.URL resultBlock:^(ALAsset *asset) { + if (cancelled) { + return; + } + + if (asset) { + + ALAssetRepresentation *representation = [asset defaultRepresentation]; + NSInteger length = (NSInteger)representation.size; + + NSURLResponse *response = + [[NSURLResponse alloc] initWithURL:request.URL + MIMEType:representation.UTI + expectedContentLength:length + textEncodingName:nil]; + + [delegate URLRequest:cancellationBlock didReceiveResponse:response]; + + NSError *error = nil; + uint8_t *buffer = (uint8_t *)malloc((size_t)length); + if ([representation getBytes:buffer + fromOffset:0 + length:length + error:&error]) { + + NSData *data = [[NSData alloc] initWithBytesNoCopy:buffer + length:length + freeWhenDone:YES]; + + [delegate URLRequest:cancellationBlock didReceiveData:data]; + [delegate URLRequest:cancellationBlock didCompleteWithError:nil]; + + } else { + free(buffer); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + } + + } else { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to load asset" + " at URL %@ with no error message.", request.URL]; + NSError *error = RCTErrorWithMessage(errorMessage); + [delegate URLRequest:cancellationBlock didCompleteWithError:error]; + } + } failureBlock:^(NSError *loadError) { + if (cancelled) { + return; + } + [delegate URLRequest:cancellationBlock didCompleteWithError:loadError]; + }]; + + return cancellationBlock; +} + +- (void)cancelRequest:(id)requestToken +{ + ((void (^)(void))requestToken)(); +} + +@end + +@implementation RCTBridge (RCTAssetsLibraryImageLoader) + +- (ALAssetsLibrary *)assetsLibrary +{ + return [[self moduleForClass:[RCTAssetsLibraryRequestHandler class]] assetsLibrary]; +} + +@end diff --git a/Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj b/Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj index 56cced214..a8bb79d92 100644 --- a/Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj +++ b/Libraries/CameraRoll/RCTCameraRoll.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.m */; }; 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; - 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */; }; + 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryRequestHandler.m */; }; 8312EAF11B85F071001867A2 /* RCTPhotoLibraryImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */; }; /* End PBXBuildFile section */ @@ -31,8 +31,8 @@ 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; }; 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; }; 58B5115D1A9E6B3D00147676 /* libRCTCameraRoll.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTCameraRoll.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssetsLibraryImageLoader.h; sourceTree = ""; }; - 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssetsLibraryImageLoader.m; sourceTree = ""; }; + 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssetsLibraryRequestHandler.h; sourceTree = ""; }; + 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssetsLibraryRequestHandler.m; sourceTree = ""; }; 8312EAEF1B85F071001867A2 /* RCTPhotoLibraryImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPhotoLibraryImageLoader.h; sourceTree = ""; }; 8312EAF01B85F071001867A2 /* RCTPhotoLibraryImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPhotoLibraryImageLoader.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -51,8 +51,8 @@ 58B511541A9E6B3D00147676 = { isa = PBXGroup; children = ( - 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.h */, - 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m */, + 8312EAEC1B85EB7C001867A2 /* RCTAssetsLibraryRequestHandler.h */, + 8312EAED1B85EB7C001867A2 /* RCTAssetsLibraryRequestHandler.m */, 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */, 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */, 137620331B31C53500677FF0 /* RCTImagePickerManager.h */, @@ -129,7 +129,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryImageLoader.m in Sources */, + 8312EAEE1B85EB7C001867A2 /* RCTAssetsLibraryRequestHandler.m in Sources */, 8312EAF11B85F071001867A2 /* RCTPhotoLibraryImageLoader.m in Sources */, 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */, 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */, diff --git a/Libraries/CameraRoll/RCTCameraRollManager.m b/Libraries/CameraRoll/RCTCameraRollManager.m index b1c285186..ad4ad1713 100644 --- a/Libraries/CameraRoll/RCTCameraRollManager.m +++ b/Libraries/CameraRoll/RCTCameraRollManager.m @@ -13,7 +13,7 @@ #import #import -#import "RCTAssetsLibraryImageLoader.h" +#import "RCTAssetsLibraryRequestHandler.h" #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTImageLoader.h" diff --git a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m index 33a9cb31b..3cb4d1c3b 100644 --- a/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m +++ b/Libraries/CameraRoll/RCTPhotoLibraryImageLoader.m @@ -30,7 +30,7 @@ RCT_EXPORT_MODULE() - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { @@ -71,7 +71,7 @@ RCT_EXPORT_MODULE() } PHImageContentMode contentMode = PHImageContentModeAspectFill; - if (resizeMode == UIViewContentModeScaleAspectFit) { + if (resizeMode == RCTResizeModeContain) { contentMode = PHImageContentModeAspectFit; } diff --git a/Libraries/Image/RCTGIFImageDecoder.m b/Libraries/Image/RCTGIFImageDecoder.m index b90375856..e578421a7 100644 --- a/Libraries/Image/RCTGIFImageDecoder.m +++ b/Libraries/Image/RCTGIFImageDecoder.m @@ -30,7 +30,7 @@ RCT_EXPORT_MODULE() - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageData, NULL); diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 0c404f31c..8c9c61861 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -11,9 +11,10 @@ 1304D5AC1AA8C4A30002E2BE /* RCTImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTImageViewManager.m */; }; 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */; }; 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */; }; - 13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */; settings = {ASSET_TAGS = (); }; }; - 13EF7F0C1BC42D4E003F47DD /* RCTVirtualImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */; settings = {ASSET_TAGS = (); }; }; - 13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */; settings = {ASSET_TAGS = (); }; }; + 139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 139A38831C4D587C00862840 /* RCTResizeMode.m */; }; + 13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */; }; + 13EF7F0C1BC42D4E003F47DD /* RCTVirtualImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F0A1BC42D4E003F47DD /* RCTVirtualImageManager.m */; }; + 13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */; }; 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; 35123E6B1B59C99D00EBAD80 /* RCTImageStoreManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35123E6A1B59C99D00EBAD80 /* RCTImageStoreManager.m */; }; 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 354631671B69857700AA0B86 /* RCTImageEditingManager.m */; }; @@ -40,6 +41,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImageDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImageDecoder.m; sourceTree = ""; }; 134B00A01B54232B00EC8DFB /* RCTImageUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageUtils.h; sourceTree = ""; }; 134B00A11B54232B00EC8DFB /* RCTImageUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtils.m; sourceTree = ""; }; + 139A38821C4D57AD00862840 /* RCTResizeMode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTResizeMode.h; sourceTree = ""; }; + 139A38831C4D587C00862840 /* RCTResizeMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTResizeMode.m; sourceTree = ""; }; 13EF7F071BC42D4E003F47DD /* RCTShadowVirtualImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowVirtualImage.h; sourceTree = ""; }; 13EF7F081BC42D4E003F47DD /* RCTShadowVirtualImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowVirtualImage.m; sourceTree = ""; }; 13EF7F091BC42D4E003F47DD /* RCTVirtualImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVirtualImageManager.h; sourceTree = ""; }; @@ -69,6 +72,8 @@ 58B511541A9E6B3D00147676 = { isa = PBXGroup; children = ( + 139A38821C4D57AD00862840 /* RCTResizeMode.h */, + 139A38831C4D587C00862840 /* RCTResizeMode.m */, 13EF7F7D1BC825B1003F47DD /* RCTXCAssetImageLoader.h */, 13EF7F7E1BC825B1003F47DD /* RCTXCAssetImageLoader.m */, 1304D5B01AA8C50D0002E2BE /* RCTGIFImageDecoder.h */, @@ -165,6 +170,7 @@ 1304D5B21AA8C50D0002E2BE /* RCTGIFImageDecoder.m in Sources */, 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */, + 139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, 13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */, 13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */, diff --git a/Libraries/Image/RCTImageEditingManager.m b/Libraries/Image/RCTImageEditingManager.m index 76fbc2703..2a2ea9671 100644 --- a/Libraries/Image/RCTImageEditingManager.m +++ b/Libraries/Image/RCTImageEditingManager.m @@ -14,6 +14,7 @@ #import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" +#import "RCTImageUtils.h" #import "RCTImageStoreManager.h" #import "RCTImageLoader.h" @@ -44,8 +45,6 @@ RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag [RCTConvert CGSize:cropData[@"size"]] }; - NSString *resizeMode = cropData[@"resizeMode"] ?: @"contain"; - [_bridge.imageLoader loadImageWithTag:imageTag callback:^(NSError *error, UIImage *image) { if (error) { errorCallback(error); @@ -53,17 +52,21 @@ RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag } // Crop image - CGRect rectToDrawIn = {{-rect.origin.x, -rect.origin.y}, image.size}; - UIGraphicsBeginImageContextWithOptions(rect.size, !RCTImageHasAlpha(image.CGImage), image.scale); - [image drawInRect:rectToDrawIn]; - UIImage *croppedImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + CGSize targetSize = rect.size; + CGRect targetRect = {{-rect.origin.x, -rect.origin.y}, image.size}; + CGAffineTransform transform = RCTTransformFromTargetRect(image.size, targetRect); + UIImage *croppedImage = RCTTransformImage(image, targetSize, image.scale, transform); + // Scale image if (cropData[@"displaySize"]) { - CGSize targetSize = [RCTConvert CGSize:cropData[@"displaySize"]]; - croppedImage = [self scaleImage:croppedImage targetSize:targetSize resizeMode:resizeMode]; + targetSize = [RCTConvert CGSize:cropData[@"displaySize"]]; // in pixels + RCTResizeMode resizeMode = [RCTConvert RCTResizeMode:cropData[@"resizeMode"] ?: @"contain"]; + targetRect = RCTTargetRect(croppedImage.size, targetSize, 1, resizeMode); + transform = RCTTransformFromTargetRect(image.size, targetRect); + croppedImage = RCTTransformImage(image, targetSize, image.scale, transform); } + // Store image [_bridge.imageStoreManager storeImage:croppedImage withBlock:^(NSString *croppedImageTag) { if (!croppedImageTag) { NSString *errorMessage = @"Error storing cropped image in RCTImageStoreManager"; @@ -76,49 +79,4 @@ RCT_EXPORT_METHOD(cropImage:(NSString *)imageTag }]; } -- (UIImage *)scaleImage:(UIImage *)image targetSize:(CGSize)targetSize resizeMode:(NSString *)resizeMode -{ - if (CGSizeEqualToSize(image.size, targetSize)) { - return image; - } - - CGFloat imageRatio = image.size.width / image.size.height; - CGFloat targetRatio = targetSize.width / targetSize.height; - - CGFloat newWidth = targetSize.width; - CGFloat newHeight = targetSize.height; - - // contain vs cover - // http://blog.vjeux.com/2013/image/css-container-and-cover.html - if ([resizeMode isEqualToString:@"contain"]) { - if (imageRatio <= targetRatio) { - newWidth = targetSize.height * imageRatio; - newHeight = targetSize.height; - } else { - newWidth = targetSize.width; - newHeight = targetSize.width / imageRatio; - } - } else if ([resizeMode isEqualToString:@"cover"]) { - if (imageRatio <= targetRatio) { - newWidth = targetSize.width; - newHeight = targetSize.width / imageRatio; - } else { - newWidth = targetSize.height * imageRatio; - newHeight = targetSize.height; - } - } // else assume we're stretching the image - - // prevent upscaling - newWidth = MIN(newWidth, image.size.width); - newHeight = MIN(newHeight, image.size.height); - - // perform the scaling @1x because targetSize is in actual pixel width/height - UIGraphicsBeginImageContextWithOptions(targetSize, NO, 1.0f); - [image drawInRect:CGRectMake(0.f, 0.f, newWidth, newHeight)]; - UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return scaledImage; -} - @end diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h index c9bea5437..e7805b135 100644 --- a/Libraries/Image/RCTImageLoader.h +++ b/Libraries/Image/RCTImageLoader.h @@ -11,6 +11,7 @@ #import "RCTBridge.h" #import "RCTURLRequestHandler.h" +#import "RCTResizeMode.h" @class ALAssetsLibrary; @@ -40,10 +41,20 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode progressBlock:(RCTImageLoaderProgressBlock)progressBlock completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; +/** + * Loads an image without clipping the result to fit - used by RCTImageView. + */ +- (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progressBlock + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; + /** * Finds an appropriate image decoder and passes the target size, scale and * resizeMode for optimal image decoding. Can be called from any thread, @@ -52,7 +63,7 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode completionBlock:(RCTImageLoaderCompletionBlock)completionBlock; /** @@ -96,7 +107,7 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler; @@ -134,7 +145,7 @@ typedef void (^RCTImageLoaderCancellationBlock)(void); - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode completionHandler:(RCTImageLoaderCompletionBlock)completionHandler; @optional diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index cd681b113..0d7b06f5c 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -173,13 +173,31 @@ RCT_EXPORT_MODULE() return nil; } +static UIImage *RCTResizeImageIfNeeded(UIImage *image, + CGSize size, + CGFloat scale, + RCTResizeMode resizeMode) +{ + if (CGSizeEqualToSize(size, CGSizeZero) || + CGSizeEqualToSize(image.size, CGSizeZero) || + CGSizeEqualToSize(image.size, size)) { + return image; + } + CAKeyframeAnimation *animation = image.reactKeyframeAnimation; + CGRect targetSize = RCTTargetRect(image.size, size, scale, resizeMode); + CGAffineTransform transform = RCTTransformFromTargetRect(image.size, targetSize); + image = RCTTransformImage(image, size, scale, transform); + image.reactKeyframeAnimation = animation; + return image; +} + - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag callback:(RCTImageLoaderCompletionBlock)callback { return [self loadImageWithTag:imageTag size:CGSizeZero scale:1 - resizeMode:UIViewContentModeScaleToFill + resizeMode:RCTResizeModeStretch progressBlock:nil completionBlock:callback]; } @@ -192,7 +210,7 @@ RCT_EXPORT_MODULE() - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode progressBlock:(RCTImageLoaderProgressBlock)progressHandler completionBlock:(void (^)(NSError *error, id imageOrData))completionBlock { @@ -352,9 +370,26 @@ RCT_EXPORT_MODULE() - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode progressBlock:(RCTImageLoaderProgressBlock)progressHandler completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +{ + return [self loadImageWithoutClipping:imageTag + size:size + scale:scale + resizeMode:resizeMode + progressBlock:progressHandler + completionBlock:^(NSError *error, UIImage *image) { + completionBlock(error, RCTResizeImageIfNeeded(image, size, scale, resizeMode)); + }]; +} + +- (RCTImageLoaderCancellationBlock)loadImageWithoutClipping:(NSString *)imageTag + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + progressBlock:(RCTImageLoaderProgressBlock)progressHandler + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock { __block volatile uint32_t cancelled = 0; __block void(^cancelLoad)(void) = nil; @@ -365,11 +400,11 @@ RCT_EXPORT_MODULE() if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) { completionBlock(error, imageOrData); } else { - cancelLoad = [weakSelf decodeImageData:imageOrData - size:size - scale:scale - resizeMode:resizeMode - completionBlock:completionBlock] ?: ^{}; + cancelLoad = [weakSelf decodeImageDataWithoutClipping:imageOrData + size:size + scale:scale + resizeMode:resizeMode + completionBlock:completionBlock] ?: ^{}; } } }; @@ -391,17 +426,47 @@ RCT_EXPORT_MODULE() - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)data size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode - completionBlock:(RCTImageLoaderCompletionBlock)completionHandler + resizeMode:(RCTResizeMode)resizeMode + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock +{ + return [self decodeImageDataWithoutClipping:data + size:size + scale:scale + resizeMode:resizeMode + completionBlock:^(NSError *error, UIImage *image) { + completionBlock(error, RCTResizeImageIfNeeded(image, size, scale, resizeMode)); + }]; +} + +- (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + completionBlock:(RCTImageLoaderCompletionBlock)completionBlock { if (data.length == 0) { - completionHandler(RCTErrorWithMessage(@"No image data"), nil); + completionBlock(RCTErrorWithMessage(@"No image data"), nil); return ^{}; } + __block volatile uint32_t cancelled = 0; + void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) { + if ([NSThread isMainThread]) { + + // Most loaders do not return on the main thread, so caller is probably not + // expecting it, and may do expensive post-processing in the callback + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (!cancelled) { + completionBlock(error, image); + } + }); + } else if (!cancelled) { + completionBlock(error, image); + } + }; + id imageDecoder = [self imageDataDecoderForData:data]; if (imageDecoder) { - return [imageDecoder decodeImageData:data size:size scale:scale @@ -409,12 +474,26 @@ RCT_EXPORT_MODULE() completionHandler:completionHandler]; } else { - __block volatile uint32_t cancelled = 0; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if (cancelled) { return; } + UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode); + +#if RCT_DEV + + CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale); + CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale()); + if (imagePixelSize.width * imagePixelSize.height > + screenPixelSize.width * screenPixelSize.height) { + RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger " + "than the screen size %@", NSStringFromCGSize(imagePixelSize), + NSStringFromCGSize(screenPixelSize)); + } + +#endif + if (image) { completionHandler(nil, image); } else { @@ -436,7 +515,7 @@ RCT_EXPORT_MODULE() return [self loadImageOrDataWithTag:imageTag size:CGSizeZero scale:1 - resizeMode:UIViewContentModeScaleToFill + resizeMode:RCTResizeModeStretch progressBlock:nil completionBlock:^(NSError *error, id imageOrData) { CGSize size; @@ -461,7 +540,17 @@ RCT_EXPORT_MODULE() - (BOOL)canHandleRequest:(NSURLRequest *)request { - return [self imageURLLoaderForURL:request.URL] != nil; + NSURL *requestURL = request.URL; + for (id loader in _loaders) { + // Don't use RCTImageURLLoader protocol for modules that already conform to + // RCTURLRequestHandler as it's inefficient to decode an image and then + // convert it back into data + if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] && + [loader canLoadImageURL:requestURL]) { + return YES; + } + } + return NO; } - (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate diff --git a/Libraries/Image/RCTImageStoreManager.m b/Libraries/Image/RCTImageStoreManager.m index 1827bd285..4c6441a64 100644 --- a/Libraries/Image/RCTImageStoreManager.m +++ b/Libraries/Image/RCTImageStoreManager.m @@ -217,6 +217,7 @@ RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String dispatch_async(_methodQueue, ^{ NSData *imageData = _store[imageTag]; dispatch_async(dispatch_get_main_queue(), ^{ + // imageWithData: is not thread-safe, so we can't do this on methodQueue block([UIImage imageWithData:imageData]); }); }); diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h index cb65449db..f870bda67 100644 --- a/Libraries/Image/RCTImageUtils.h +++ b/Libraries/Image/RCTImageUtils.h @@ -11,28 +11,36 @@ #import #import "RCTDefines.h" +#import "RCTResizeMode.h" NS_ASSUME_NONNULL_BEGIN /** - * This function takes an input content size (typically from an image), a target - * size and scale that it will be drawn at (typically in a CGContext) and then + * This function takes an source size (typically from an image), a target size + * and scale that it will be drawn at (typically in a CGContext) and then * calculates the rectangle to draw the image into so that it will be sized and - * positioned correctly if drawn using the specified content mode. + * positioned correctly according to the specified resizeMode. */ RCT_EXTERN CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, - CGFloat destScale, UIViewContentMode resizeMode); + CGFloat destScale, RCTResizeMode resizeMode); + +/** + * This function takes a source size (typically from an image), a target rect + * that it will be drawn into (typically relative to a CGContext), and works out + * the transform needed to draw the image at the correct scale and position. + */ +RCT_EXTERN CGAffineTransform RCTTransformFromTargetRect(CGSize sourceSize, + CGRect targetRect); /** * This function takes an input content size & scale (typically from an image), * a target size & scale at which it will be displayed (typically in a * UIImageView) and then calculates the optimal size at which to redraw the - * image so that it will be displayed correctly with the specified content mode. + * image so that it will be displayed correctly with the specified resizeMode. */ RCT_EXTERN CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode, - BOOL allowUpscaling); + RCTResizeMode resizeMode, BOOL allowUpscaling); /** * This function takes an input content size & scale (typically from an image), @@ -41,12 +49,7 @@ RCT_EXTERN CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, */ RCT_EXTERN BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode); -/** - * This function takes a source size and scale and returns the size in pixels. - * Note that the pixel width/height is rounded up to the nearest integral size. - */ -RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale); + RCTResizeMode resizeMode); /** * This function takes the source data for an image and decodes it at the @@ -58,7 +61,7 @@ RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale); RCT_EXTERN UIImage *__nullable RCTDecodeImageWithData(NSData *data, CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode); + RCTResizeMode resizeMode); /** * This function takes the source data for an image and decodes just the @@ -75,4 +78,19 @@ RCT_EXTERN NSDictionary *__nullable RCTGetImageMetadata(NSData * */ RCT_EXTERN NSData *__nullable RCTGetImageData(CGImageRef image, float quality); +/** + * This function transforms an image. `destSize` is the size of the final image, + * and `destScale` is its scale. The `transform` argument controls how the + * source image will be mapped to the destination image. + */ +RCT_EXTERN UIImage *__nullable RCTTransformImage(UIImage *image, + CGSize destSize, + CGFloat destScale, + CGAffineTransform transform); + +/* + * Return YES if image has an alpha component + */ +RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); + NS_ASSUME_NONNULL_END diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index 0b51eca72..d484ea238 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -35,7 +35,7 @@ static CGSize RCTCeilSize(CGSize size, CGFloat scale) } CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, - CGFloat destScale, UIViewContentMode resizeMode) + CGFloat destScale, RCTResizeMode resizeMode) { if (CGSizeEqualToSize(destSize, CGSizeZero)) { // Assume we require the largest size available @@ -58,16 +58,16 @@ CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, if (resizeMode != UIViewContentModeScaleToFill) { targetAspect = destSize.width / destSize.height; if (aspect == targetAspect) { - resizeMode = UIViewContentModeScaleToFill; + resizeMode = RCTResizeModeStretch; } } switch (resizeMode) { - case UIViewContentModeScaleToFill: // stretch + case RCTResizeModeStretch: return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)}; - case UIViewContentModeScaleAspectFit: // contain + case RCTResizeModeContain: if (targetAspect <= aspect) { // target is taller than content @@ -79,9 +79,15 @@ CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, sourceSize.height = destSize.height = destSize.height; sourceSize.width = sourceSize.height * aspect; } - return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)}; + return (CGRect){ + { + RCTFloorValue((destSize.width - sourceSize.width) / 2, destScale), + RCTFloorValue((destSize.height - sourceSize.height) / 2, destScale), + }, + RCTCeilSize(sourceSize, destScale) + }; - case UIViewContentModeScaleAspectFill: // cover + case RCTResizeModeCover: if (targetAspect <= aspect) { // target is taller than content @@ -103,21 +109,28 @@ CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, RCTCeilSize(sourceSize, destScale) }; } - - default: - - RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode); - return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)}; } } +CGAffineTransform RCTTransformFromTargetRect(CGSize sourceSize, CGRect targetRect) +{ + CGAffineTransform transform = CGAffineTransformIdentity; + transform = CGAffineTransformTranslate(transform, + targetRect.origin.x, + targetRect.origin.y); + transform = CGAffineTransformScale(transform, + targetRect.size.width / sourceSize.width, + targetRect.size.height / sourceSize.height); + return transform; +} + CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode, + RCTResizeMode resizeMode, BOOL allowUpscaling) { switch (resizeMode) { - case UIViewContentModeScaleToFill: // stretch + case RCTResizeModeStretch: if (!allowUpscaling) { CGFloat scale = sourceScale / destScale; @@ -143,7 +156,7 @@ CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode) + RCTResizeMode resizeMode) { if (CGSizeEqualToSize(destSize, CGSizeZero)) { // Assume we require the largest size available @@ -161,16 +174,16 @@ BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, aspect = sourceSize.width / sourceSize.height; targetAspect = destSize.width / destSize.height; if (aspect == targetAspect) { - resizeMode = UIViewContentModeScaleToFill; + resizeMode = RCTResizeModeStretch; } } switch (resizeMode) { - case UIViewContentModeScaleToFill: // stretch + case RCTResizeModeStretch: return destSize.width > sourceSize.width || destSize.height > sourceSize.height; - case UIViewContentModeScaleAspectFit: // contain + case RCTResizeModeContain: if (targetAspect <= aspect) { // target is taller than content @@ -181,7 +194,7 @@ BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, return destSize.height > sourceSize.height; } - case UIViewContentModeScaleAspectFill: // cover + case RCTResizeModeCover: if (targetAspect <= aspect) { // target is taller than content @@ -191,33 +204,20 @@ BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, return destSize.width > sourceSize.width; } - - default: - - RCTLogError(@"A resizeMode value of %zd is not supported", resizeMode); - return NO; } } -CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale) -{ - return (CGSize){ - ceil(pointSize.width * scale), - ceil(pointSize.height * scale), - }; -} - UIImage *__nullable RCTDecodeImageWithData(NSData *data, CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode) + RCTResizeMode resizeMode) { CGImageSourceRef sourceRef = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); if (!sourceRef) { return nil; } - // get original image size + // Get original image size CFDictionaryRef imageProperties = CGImageSourceCopyPropertiesAtIndex(sourceRef, 0, NULL); if (!imageProperties) { CFRelease(sourceRef); @@ -237,8 +237,14 @@ UIImage *__nullable RCTDecodeImageWithData(NSData *data, destScale = RCTScreenScale(); } - // calculate target size - CGSize targetSize = RCTTargetSize(sourceSize, 1, destSize, destScale, resizeMode, YES); + if (resizeMode == UIViewContentModeScaleToFill) { + // Decoder cannot change aspect ratio, so RCTResizeModeStretch is equivalent + // to RCTResizeModeCover for our purposes + resizeMode = RCTResizeModeCover; + } + + // Calculate target size + CGSize targetSize = RCTTargetSize(sourceSize, 1, destSize, destScale, resizeMode, NO); CGSize targetPixelSize = RCTSizeInPixels(targetSize, destScale); CGFloat maxPixelSize = fmax(fmin(sourceSize.width, targetPixelSize.width), fmin(sourceSize.height, targetPixelSize.height)); @@ -250,14 +256,14 @@ UIImage *__nullable RCTDecodeImageWithData(NSData *data, (id)kCGImageSourceThumbnailMaxPixelSize: @(maxPixelSize), }; - // get thumbnail + // Get thumbnail CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(sourceRef, 0, (__bridge CFDictionaryRef)options); CFRelease(sourceRef); if (!imageRef) { return nil; } - // return image + // Return image UIImage *image = [UIImage imageWithCGImage:imageRef scale:destScale orientation:UIImageOrientationUp]; @@ -298,3 +304,33 @@ NSData *__nullable RCTGetImageData(CGImageRef image, float quality) CFRelease(destination); return (__bridge_transfer NSData *)imageData; } + +UIImage *__nullable RCTTransformImage(UIImage *image, + CGSize destSize, + CGFloat destScale, + CGAffineTransform transform) +{ + if (destSize.width <= 0 | destSize.height <= 0 || destScale <= 0) { + return nil; + } + + UIGraphicsBeginImageContextWithOptions(destSize, NO, destScale); + CGContextRef currentContext = UIGraphicsGetCurrentContext(); + CGContextConcatCTM(currentContext, transform); + [image drawAtPoint:CGPointZero]; + UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsGetCurrentContext(); + return result; +} + +BOOL RCTImageHasAlpha(CGImageRef image) +{ + switch (CGImageGetAlphaInfo(image)) { + case kCGImageAlphaNone: + case kCGImageAlphaNoneSkipLast: + case kCGImageAlphaNoneSkipFirst: + return NO; + default: + return YES; + } +} diff --git a/Libraries/Image/RCTImageView.h b/Libraries/Image/RCTImageView.h index 8b5b993a7..769f9c965 100644 --- a/Libraries/Image/RCTImageView.h +++ b/Libraries/Image/RCTImageView.h @@ -9,6 +9,7 @@ #import #import "RCTImageComponent.h" +#import "RCTResizeMode.h" @class RCTBridge; @class RCTImageSource; diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 712f7e7a5..36506d348 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -103,8 +103,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)setCapInsets:(UIEdgeInsets)capInsets { if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, capInsets)) { - _capInsets = capInsets; - [self updateImage]; + if (UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero) || + UIEdgeInsetsEqualToEdgeInsets(capInsets, UIEdgeInsetsZero)) { + _capInsets = capInsets; + // Need to reload image when enabling or disabling capInsets + [self reloadImage]; + } else { + _capInsets = capInsets; + [self updateImage]; + } } } @@ -126,12 +133,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) - (BOOL)sourceNeedsReload { - NSString *scheme = _source.imageURL.scheme; - return - [scheme isEqualToString:@"http"] || - [scheme isEqualToString:@"https"] || - [scheme isEqualToString:@"assets-library"] || - [scheme isEqualToString:@"ph"]; + // If capInsets are set, image doesn't need reloading when resized + return UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero); } - (void)setContentMode:(UIViewContentMode)contentMode @@ -179,14 +182,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) }; } + CGSize imageSize = self.bounds.size; + CGFloat imageScale = RCTScreenScale(); + if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, UIEdgeInsetsZero)) { + // Don't resize images that use capInsets + imageSize = CGSizeZero; + imageScale = _source.scale; + } + RCTImageSource *source = _source; __weak RCTImageView *weakSelf = self; - _reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithTag:_source.imageURL.absoluteString - size:self.bounds.size - scale:RCTScreenScale() - resizeMode:self.contentMode - progressBlock:progressHandler - completionBlock:^(NSError *error, UIImage *image) { + _reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithoutClipping:_source.imageURL.absoluteString + size:imageSize + scale:imageScale + resizeMode:(RCTResizeMode)self.contentMode + progressBlock:progressHandler + completionBlock:^(NSError *error, UIImage *image) { dispatch_async(dispatch_get_main_queue(), ^{ RCTImageView *strongSelf = weakSelf; if (![source isEqual:strongSelf.source]) { @@ -227,14 +238,15 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) [self reloadImage]; } else if ([self sourceNeedsReload]) { CGSize imageSize = self.image.size; - CGSize idealSize = RCTTargetSize(imageSize, self.image.scale, frame.size, RCTScreenScale(), self.contentMode, YES); + CGSize idealSize = RCTTargetSize(imageSize, self.image.scale, frame.size, + RCTScreenScale(), (RCTResizeMode)self.contentMode, YES); if (RCTShouldReloadImageForSizeChange(imageSize, idealSize)) { if (RCTShouldReloadImageForSizeChange(_targetSize, idealSize)) { RCTLogInfo(@"[PERF IMAGEVIEW] Reloading image %@ as size %@", _source.imageURL, NSStringFromCGSize(idealSize)); - // If the existing image or an image being loaded are not the right size, reload the asset in case there is a - // better size available. + // If the existing image or an image being loaded are not the right + // size, reload the asset in case there is a better size available. _targetSize = idealSize; [self reloadImage]; } @@ -251,8 +263,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) [super didMoveToWindow]; if (!self.window) { - // Don't keep self alive through the asynchronous dispatch, if the intention was to remove the view so it would - // deallocate. + // Don't keep self alive through the asynchronous dispatch, if the intention + // was to remove the view so it would deallocate. __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ @@ -261,7 +273,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) return; } - // If we haven't been re-added to a window by this run loop iteration, clear out the image to save memory. + // If we haven't been re-added to a window by this run loop iteration, + // clear out the image to save memory. if (!strongSelf.window) { [strongSelf clearImage]; } diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m index 7ef593195..65e18b5a2 100644 --- a/Libraries/Image/RCTImageViewManager.m +++ b/Libraries/Image/RCTImageViewManager.m @@ -32,7 +32,7 @@ RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadEnd, RCTDirectEventBlock) -RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode) +RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, RCTResizeMode) RCT_EXPORT_VIEW_PROPERTY(source, RCTImageSource) RCT_CUSTOM_VIEW_PROPERTY(tintColor, UIColor, RCTImageView) { diff --git a/Libraries/Image/RCTResizeMode.h b/Libraries/Image/RCTResizeMode.h new file mode 100644 index 000000000..81e9d925f --- /dev/null +++ b/Libraries/Image/RCTResizeMode.h @@ -0,0 +1,22 @@ +/** + * 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 "RCTConvert.h" + +typedef NS_ENUM(NSInteger, RCTResizeMode) { + RCTResizeModeCover = UIViewContentModeScaleAspectFill, + RCTResizeModeContain = UIViewContentModeScaleAspectFit, + RCTResizeModeStretch = UIViewContentModeScaleToFill, +}; + +@interface RCTConvert(RCTResizeMode) + ++ (RCTResizeMode)RCTResizeMode:(id)json; + +@end diff --git a/Libraries/Image/RCTResizeMode.m b/Libraries/Image/RCTResizeMode.m new file mode 100644 index 000000000..b80839bb4 --- /dev/null +++ b/Libraries/Image/RCTResizeMode.m @@ -0,0 +1,20 @@ +/** + * 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 "RCTResizeMode.h" + +@implementation RCTConvert(RCTResizeMode) + +RCT_ENUM_CONVERTER(RCTResizeMode, (@{ + @"cover": @(RCTResizeModeCover), + @"contain": @(RCTResizeModeContain), + @"stretch": @(RCTResizeModeStretch), +}), RCTResizeModeStretch, integerValue) + +@end diff --git a/Libraries/Image/RCTShadowVirtualImage.m b/Libraries/Image/RCTShadowVirtualImage.m index f3486f9a2..d6b0153d1 100644 --- a/Libraries/Image/RCTShadowVirtualImage.m +++ b/Libraries/Image/RCTShadowVirtualImage.m @@ -46,7 +46,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) _cancellationBlock = [_bridge.imageLoader loadImageWithTag:source.imageURL.absoluteString size:source.size scale:source.scale - resizeMode:UIViewContentModeScaleToFill + resizeMode:RCTResizeModeStretch progressBlock:nil completionBlock:^(NSError *error, UIImage *image) { diff --git a/Libraries/Image/RCTXCAssetImageLoader.m b/Libraries/Image/RCTXCAssetImageLoader.m index 799798a36..750a1f848 100644 --- a/Libraries/Image/RCTXCAssetImageLoader.m +++ b/Libraries/Image/RCTXCAssetImageLoader.m @@ -25,7 +25,7 @@ RCT_EXPORT_MODULE() - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale - resizeMode:(UIViewContentMode)resizeMode + resizeMode:(RCTResizeMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler { diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 36383e8c6..7c8aaa511 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -40,6 +40,9 @@ RCT_EXTERN CGFloat RCTRoundPixelValue(CGFloat value); RCT_EXTERN CGFloat RCTCeilPixelValue(CGFloat value); RCT_EXTERN CGFloat RCTFloorPixelValue(CGFloat value); +// Convert a size in points to pixels, rounded up to the nearest integral size +RCT_EXTERN CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale); + // Method swizzling RCT_EXTERN void RCTSwapClassMethods(Class cls, SEL original, SEL replacement); RCT_EXTERN void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement); @@ -78,9 +81,6 @@ RCT_EXTERN UIAlertView *RCTAlertView(NSString *title, NSString *cancelButtonTitle, NSArray *otherButtonTitles); -// Return YES if image has an alpha component -RCT_EXTERN BOOL RCTImageHasAlpha(CGImageRef image); - // Create an NSError in the RCTErrorDomain RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index cf0d8b8b0..b13b3f951 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -219,7 +219,13 @@ CGSize RCTScreenSize() static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ RCTExecuteOnMainThread(^{ - size = [UIScreen mainScreen].bounds.size; + if ([[[UIDevice currentDevice] systemVersion] compare:@"8.0" options:NSNumericSearch] == NSOrderedDescending) { + CGSize pixelSize = [UIScreen mainScreen].nativeBounds.size; + CGFloat scale = RCTScreenScale(); + size = (CGSize){pixelSize.width / scale, pixelSize.height / scale}; + } else { + size = [UIScreen mainScreen].bounds.size; + } }, YES); }); @@ -244,6 +250,14 @@ CGFloat RCTFloorPixelValue(CGFloat value) return floor(value * scale) / scale; } +CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale) +{ + return (CGSize){ + ceil(pointSize.width * scale), + ceil(pointSize.height * scale), + }; +} + void RCTSwapClassMethods(Class cls, SEL original, SEL replacement) { Method originalMethod = class_getClassMethod(cls, original); @@ -401,18 +415,6 @@ UIAlertView *RCTAlertView(NSString *title, return alertView; } -BOOL RCTImageHasAlpha(CGImageRef image) -{ - switch (CGImageGetAlphaInfo(image)) { - case kCGImageAlphaNone: - case kCGImageAlphaNoneSkipLast: - case kCGImageAlphaNoneSkipFirst: - return NO; - default: - return YES; - } -} - NSError *RCTErrorWithMessage(NSString *message) { NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index 789527006..34f2742a6 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -172,6 +172,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) // Ordinary property handlers NSMethodSignature *typeSignature = [[RCTConvert class] methodSignatureForSelector:type]; + if (!typeSignature) { + RCTLogError(@"No +[RCTConvert %@] function found.", NSStringFromSelector(type)); + return ^(__unused id view, __unused id json){}; + } switch (typeSignature.methodReturnType[0]) { #define RCT_CASE(_value, _type) \