From 9cb3ec94246e26d8c8291f7f90160b72a3009d0d Mon Sep 17 00:00:00 2001 From: Reem Helou Date: Thu, 17 Mar 2016 12:25:07 -0700 Subject: [PATCH] Add blur effect to RCTImageView Summary: This diff introduces a blur radius property to the Image component on ios. If the radius specified is greater then 0 then native will apply a blur filter to the image Reviewed By: nicklockwood Differential Revision: D3054671 fb-gh-sync-id: d7a81ce5a08a3a2091c583f5053c6a86638b21b2 shipit-source-id: d7a81ce5a08a3a2091c583f5053c6a86638b21b2 --- Libraries/Image/Image.ios.js | 7 +- .../Image/RCTImage.xcodeproj/project.pbxproj | 6 ++ Libraries/Image/RCTImageBlurUtils.h | 16 ++++ Libraries/Image/RCTImageBlurUtils.m | 77 +++++++++++++++++++ Libraries/Image/RCTImageView.h | 1 + Libraries/Image/RCTImageView.m | 16 +++- Libraries/Image/RCTImageViewManager.m | 1 + 7 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 Libraries/Image/RCTImageBlurUtils.h create mode 100644 Libraries/Image/RCTImageBlurUtils.m diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 785c9f0837..940725b91b 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -94,6 +94,11 @@ var Image = React.createClass({ * @platform ios */ accessibilityLabel: PropTypes.string, + /** + * blurRadius: the blur radius of the blur filter added to the image + * @platform ios + */ + blurRadius: PropTypes.number, /** * When the image is resized, the corners of the size specified * by capInsets will stay a fixed size, but the center content and borders @@ -206,7 +211,7 @@ var Image = React.createClass({ // This is a workaround for #8243665. RCTNetworkImageView does not support tintColor // TODO: Remove this hack once we have one image implementation #8389274 - if (isNetwork && tintColor) { + if (isNetwork && (tintColor || this.props.blurRadius)) { RawImage = RCTImageView; } diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 8c9c61861a..2ab62b1f2a 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 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 */; }; + EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -56,6 +57,8 @@ 354631661B69857700AA0B86 /* RCTImageEditingManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageEditingManager.h; sourceTree = ""; }; 354631671B69857700AA0B86 /* RCTImageEditingManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageEditingManager.m; sourceTree = ""; }; 58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; }; + EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageBlurUtils.h; sourceTree = ""; }; + EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageBlurUtils.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,6 +75,8 @@ 58B511541A9E6B3D00147676 = { isa = PBXGroup; children = ( + EEF314701C9B0DD30049118E /* RCTImageBlurUtils.h */, + EEF314711C9B0DD30049118E /* RCTImageBlurUtils.m */, 139A38821C4D57AD00862840 /* RCTResizeMode.h */, 139A38831C4D587C00862840 /* RCTResizeMode.m */, 13EF7F7D1BC825B1003F47DD /* RCTXCAssetImageLoader.h */, @@ -172,6 +177,7 @@ 354631681B69857700AA0B86 /* RCTImageEditingManager.m in Sources */, 139A38841C4D587C00862840 /* RCTResizeMode.m in Sources */, 1304D5AB1AA8C4A30002E2BE /* RCTImageView.m in Sources */, + EEF314721C9B0DD30049118E /* RCTImageBlurUtils.m in Sources */, 13EF7F0B1BC42D4E003F47DD /* RCTShadowVirtualImage.m in Sources */, 13EF7F7F1BC825B1003F47DD /* RCTXCAssetImageLoader.m in Sources */, 134B00A21B54232B00EC8DFB /* RCTImageUtils.m in Sources */, diff --git a/Libraries/Image/RCTImageBlurUtils.h b/Libraries/Image/RCTImageBlurUtils.h new file mode 100644 index 0000000000..d6c6904d91 --- /dev/null +++ b/Libraries/Image/RCTImageBlurUtils.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2013, 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 +#import + +#import "RCTDefines.h" + +RCT_EXTERN UIImage *RCTBlurredImageWithRadius(UIImage *inputImage, CGFloat radius); diff --git a/Libraries/Image/RCTImageBlurUtils.m b/Libraries/Image/RCTImageBlurUtils.m new file mode 100644 index 0000000000..193c042770 --- /dev/null +++ b/Libraries/Image/RCTImageBlurUtils.m @@ -0,0 +1,77 @@ +/** + * 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 "RCTImageBlurUtils.h" + +UIImage *RCTBlurredImageWithRadius(UIImage *inputImage, CGFloat radius) +{ + CGImageRef imageRef = inputImage.CGImage; + CGFloat imageScale = inputImage.scale; + UIImageOrientation imageOrientation = inputImage.imageOrientation; + + // Image must be nonzero size + if (CGImageGetWidth(imageRef) * CGImageGetHeight(imageRef) == 0) { + return inputImage; + } + + //convert to ARGB if it isn't + if (CGImageGetBitsPerPixel(imageRef) != 32 || + CGImageGetBitsPerComponent(imageRef) != 8 || + !((CGImageGetBitmapInfo(imageRef) & kCGBitmapAlphaInfoMask))) { + UIGraphicsBeginImageContextWithOptions(inputImage.size, NO, inputImage.scale); + [inputImage drawAtPoint:CGPointZero]; + imageRef = UIGraphicsGetImageFromCurrentImageContext().CGImage; + UIGraphicsEndImageContext(); + } + + vImage_Buffer buffer1, buffer2; + buffer1.width = buffer2.width = CGImageGetWidth(imageRef); + buffer1.height = buffer2.height = CGImageGetHeight(imageRef); + buffer1.rowBytes = buffer2.rowBytes = CGImageGetBytesPerRow(imageRef); + size_t bytes = buffer1.rowBytes * buffer1.height; + buffer1.data = malloc(bytes); + buffer2.data = malloc(bytes); + + // A description of how to compute the box kernel width from the Gaussian + // radius (aka standard deviation) appears in the SVG spec: + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + uint32_t boxSize = floor((radius * imageScale * 3 * sqrt(2 * M_PI) / 4 + 0.5) / 2); + boxSize |= 1; // Ensure boxSize is odd + + //create temp buffer + void *tempBuffer = malloc((size_t)vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, NULL, 0, 0, boxSize, boxSize, + NULL, kvImageEdgeExtend + kvImageGetTempBufferSize)); + + //copy image data + CFDataRef dataSource = CGDataProviderCopyData(CGImageGetDataProvider(imageRef)); + memcpy(buffer1.data, CFDataGetBytePtr(dataSource), bytes); + CFRelease(dataSource); + + //perform blur + vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&buffer2, &buffer1, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&buffer1, &buffer2, tempBuffer, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend); + + //free buffers + free(buffer2.data); + free(tempBuffer); + + //create image context from buffer + CGContextRef ctx = CGBitmapContextCreate(buffer1.data, buffer1.width, buffer1.height, + 8, buffer1.rowBytes, CGImageGetColorSpace(imageRef), + CGImageGetBitmapInfo(imageRef)); + + //create image from context + imageRef = CGBitmapContextCreateImage(ctx); + UIImage *outputImage = [UIImage imageWithCGImage:imageRef scale:imageScale orientation:imageOrientation]; + CGImageRelease(imageRef); + CGContextRelease(ctx); + free(buffer1.data); + return outputImage; +} diff --git a/Libraries/Image/RCTImageView.h b/Libraries/Image/RCTImageView.h index 769f9c965d..06564e58fb 100644 --- a/Libraries/Image/RCTImageView.h +++ b/Libraries/Image/RCTImageView.h @@ -22,5 +22,6 @@ @property (nonatomic, strong) UIImage *defaultImage; @property (nonatomic, assign) UIImageRenderingMode renderingMode; @property (nonatomic, strong) RCTImageSource *source; +@property (nonatomic, assign) CGFloat blurRadius; @end diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 36506d348d..b78ce896ce 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -16,6 +16,7 @@ #import "RCTImageSource.h" #import "RCTImageUtils.h" #import "RCTUtils.h" +#import "RCTImageBlurUtils.h" #import "UIView+React.h" @@ -100,6 +101,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) } } +- (void)setBlurRadius:(CGFloat)blurRadius +{ + if (blurRadius != _blurRadius) { + _blurRadius = blurRadius; + [self reloadImage]; + } +} + - (void)setCapInsets:(UIEdgeInsets)capInsets { if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, capInsets)) { @@ -191,6 +200,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) } RCTImageSource *source = _source; + CGFloat blurRadius = _blurRadius; __weak RCTImageView *weakSelf = self; _reloadImageCancellationBlock = [_bridge.imageLoader loadImageWithoutClipping:_source.imageURL.absoluteString size:imageSize @@ -198,8 +208,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init) resizeMode:(RCTResizeMode)self.contentMode progressBlock:progressHandler completionBlock:^(NSError *error, UIImage *image) { + RCTImageView *strongSelf = weakSelf; + if (blurRadius > __FLT_EPSILON__) { + // Do this on the background thread to avoid blocking interaction + image = RCTBlurredImageWithRadius(image, blurRadius); + } dispatch_async(dispatch_get_main_queue(), ^{ - RCTImageView *strongSelf = weakSelf; if (![source isEqual:strongSelf.source]) { // Bail out if source has changed since we started loading return; diff --git a/Libraries/Image/RCTImageViewManager.m b/Libraries/Image/RCTImageViewManager.m index 65e18b5a2a..2bf7a0fe33 100644 --- a/Libraries/Image/RCTImageViewManager.m +++ b/Libraries/Image/RCTImageViewManager.m @@ -25,6 +25,7 @@ RCT_EXPORT_MODULE() return [[RCTImageView alloc] initWithBridge:self.bridge]; } +RCT_EXPORT_VIEW_PROPERTY(blurRadius, CGFloat) RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets) RCT_REMAP_VIEW_PROPERTY(defaultSource, defaultImage, UIImage) RCT_EXPORT_VIEW_PROPERTY(onLoadStart, RCTDirectEventBlock)