UIImageEffects/UIImageEffects.m
/* |
File: UIImageEffects.m |
Abstract: This class contains methods to apply blur and tint effects to an image. |
This is the code you’ll want to look out to find out how to use vImage to |
efficiently calculate a blur. |
Version: 1.1 |
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
*/ |
#import "UIImageEffects.h" |
@import Accelerate; |
@implementation UIImageEffects |
#pragma mark - |
#pragma mark - Effects |
//| ---------------------------------------------------------------------------- |
+ (UIImage *)imageByApplyingLightEffectToImage:(UIImage*)inputImage |
{ |
UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3]; |
return [self imageByApplyingBlurToImage:inputImage withRadius:60 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; |
} |
//| ---------------------------------------------------------------------------- |
+ (UIImage *)imageByApplyingExtraLightEffectToImage:(UIImage*)inputImage |
{ |
UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82]; |
return [self imageByApplyingBlurToImage:inputImage withRadius:40 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; |
} |
//| ---------------------------------------------------------------------------- |
+ (UIImage *)imageByApplyingDarkEffectToImage:(UIImage*)inputImage |
{ |
UIColor *tintColor = [UIColor colorWithWhite:0.11 alpha:0.73]; |
return [self imageByApplyingBlurToImage:inputImage withRadius:40 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; |
} |
//| ---------------------------------------------------------------------------- |
+ (UIImage *)imageByApplyingTintEffectWithColor:(UIColor *)tintColor toImage:(UIImage*)inputImage |
{ |
const CGFloat EffectColorAlpha = 0.6; |
UIColor *effectColor = tintColor; |
size_t componentCount = CGColorGetNumberOfComponents(tintColor.CGColor); |
if (componentCount == 2) { |
CGFloat b; |
if ([tintColor getWhite:&b alpha:NULL]) { |
effectColor = [UIColor colorWithWhite:b alpha:EffectColorAlpha]; |
} |
} |
else { |
CGFloat r, g, b; |
if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) { |
effectColor = [UIColor colorWithRed:r green:g blue:b alpha:EffectColorAlpha]; |
} |
} |
return [self imageByApplyingBlurToImage:inputImage withRadius:20 tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil]; |
} |
#pragma mark - |
#pragma mark - Implementation |
//| ---------------------------------------------------------------------------- |
+ (UIImage*)imageByApplyingBlurToImage:(UIImage*)inputImage withRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage |
{ |
#define ENABLE_BLUR 1 |
#define ENABLE_SATURATION_ADJUSTMENT 1 |
#define ENABLE_TINT 1 |
// Check pre-conditions. |
if (inputImage.size.width < 1 || inputImage.size.height < 1) |
{ |
NSLog(@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", inputImage.size.width, inputImage.size.height, inputImage); |
return nil; |
} |
if (!inputImage.CGImage) |
{ |
NSLog(@"*** error: inputImage must be backed by a CGImage: %@", inputImage); |
return nil; |
} |
if (maskImage && !maskImage.CGImage) |
{ |
NSLog(@"*** error: effectMaskImage must be backed by a CGImage: %@", maskImage); |
return nil; |
} |
BOOL hasBlur = blurRadius > __FLT_EPSILON__; |
BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__; |
CGImageRef inputCGImage = inputImage.CGImage; |
CGFloat inputImageScale = inputImage.scale; |
CGBitmapInfo inputImageBitmapInfo = CGImageGetBitmapInfo(inputCGImage); |
CGImageAlphaInfo inputImageAlphaInfo = (inputImageBitmapInfo & kCGBitmapAlphaInfoMask); |
CGSize outputImageSizeInPoints = inputImage.size; |
CGRect outputImageRectInPoints = { CGPointZero, outputImageSizeInPoints }; |
// Set up output context. |
BOOL useOpaqueContext; |
if (inputImageAlphaInfo == kCGImageAlphaNone || inputImageAlphaInfo == kCGImageAlphaNoneSkipLast || inputImageAlphaInfo == kCGImageAlphaNoneSkipFirst) |
useOpaqueContext = YES; |
else |
useOpaqueContext = NO; |
UIGraphicsBeginImageContextWithOptions(outputImageRectInPoints.size, useOpaqueContext, inputImageScale); |
CGContextRef outputContext = UIGraphicsGetCurrentContext(); |
CGContextScaleCTM(outputContext, 1.0, -1.0); |
CGContextTranslateCTM(outputContext, 0, -outputImageRectInPoints.size.height); |
if (hasBlur || hasSaturationChange) |
{ |
vImage_Buffer effectInBuffer; |
vImage_Buffer scratchBuffer1; |
vImage_Buffer *inputBuffer; |
vImage_Buffer *outputBuffer; |
vImage_CGImageFormat format = { |
.bitsPerComponent = 8, |
.bitsPerPixel = 32, |
.colorSpace = NULL, |
// (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little) |
// requests a BGRA buffer. |
.bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little, |
.version = 0, |
.decode = NULL, |
.renderingIntent = kCGRenderingIntentDefault |
}; |
vImage_Error e = vImageBuffer_InitWithCGImage(&effectInBuffer, &format, NULL, inputImage.CGImage, kvImagePrintDiagnosticsToConsole); |
if (e != kvImageNoError) |
{ |
NSLog(@"*** error: vImageBuffer_InitWithCGImage returned error code %zi for inputImage: %@", e, inputImage); |
UIGraphicsEndImageContext(); |
return nil; |
} |
vImageBuffer_Init(&scratchBuffer1, effectInBuffer.height, effectInBuffer.width, format.bitsPerPixel, kvImageNoFlags); |
inputBuffer = &effectInBuffer; |
outputBuffer = &scratchBuffer1; |
#if ENABLE_BLUR |
if (hasBlur) |
{ |
// 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 |
// |
// For larger values of 's' (s >= 2.0), an approximation can be used: Three |
// successive box-blurs build a piece-wise quadratic convolution kernel, which |
// approximates the Gaussian kernel to within roughly 3%. |
// |
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) |
// |
// ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. |
// |
CGFloat inputRadius = blurRadius * inputImageScale; |
if (inputRadius - 2. < __FLT_EPSILON__) |
inputRadius = 2.; |
uint32_t radius = floor((inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5) / 2); |
radius |= 1; // force radius to be odd so that the three box-blur methodology works. |
NSInteger tempBufferSize = vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, NULL, 0, 0, radius, radius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend); |
void *tempBuffer = malloc(tempBufferSize); |
vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, radius, radius, NULL, kvImageEdgeExtend); |
vImageBoxConvolve_ARGB8888(outputBuffer, inputBuffer, tempBuffer, 0, 0, radius, radius, NULL, kvImageEdgeExtend); |
vImageBoxConvolve_ARGB8888(inputBuffer, outputBuffer, tempBuffer, 0, 0, radius, radius, NULL, kvImageEdgeExtend); |
free(tempBuffer); |
vImage_Buffer *temp = inputBuffer; |
inputBuffer = outputBuffer; |
outputBuffer = temp; |
} |
#endif |
#if ENABLE_SATURATION_ADJUSTMENT |
if (hasSaturationChange) |
{ |
CGFloat s = saturationDeltaFactor; |
// These values appear in the W3C Filter Effects spec: |
// https://dvcs.w3.org/hg/FXTF/raw-file/default/filters/index.html#grayscaleEquivalent |
// |
CGFloat floatingPointSaturationMatrix[] = { |
0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0, |
0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0, |
0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0, |
0, 0, 0, 1, |
}; |
const int32_t divisor = 256; |
NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]); |
int16_t saturationMatrix[matrixSize]; |
for (NSUInteger i = 0; i < matrixSize; ++i) { |
saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor); |
} |
vImageMatrixMultiply_ARGB8888(inputBuffer, outputBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); |
vImage_Buffer *temp = inputBuffer; |
inputBuffer = outputBuffer; |
outputBuffer = temp; |
} |
#endif |
CGImageRef effectCGImage; |
if ( (effectCGImage = vImageCreateCGImageFromBuffer(inputBuffer, &format, &cleanupBuffer, NULL, kvImageNoAllocate, NULL)) == NULL ) { |
effectCGImage = vImageCreateCGImageFromBuffer(inputBuffer, &format, NULL, NULL, kvImageNoFlags, NULL); |
free(inputBuffer->data); |
} |
if (maskImage) { |
// Only need to draw the base image if the effect image will be masked. |
CGContextDrawImage(outputContext, outputImageRectInPoints, inputCGImage); |
} |
// draw effect image |
CGContextSaveGState(outputContext); |
if (maskImage) |
CGContextClipToMask(outputContext, outputImageRectInPoints, maskImage.CGImage); |
CGContextDrawImage(outputContext, outputImageRectInPoints, effectCGImage); |
CGContextRestoreGState(outputContext); |
// Cleanup |
CGImageRelease(effectCGImage); |
free(outputBuffer->data); |
} |
else |
{ |
// draw base image |
CGContextDrawImage(outputContext, outputImageRectInPoints, inputCGImage); |
} |
#if ENABLE_TINT |
// Add in color tint. |
if (tintColor) |
{ |
CGContextSaveGState(outputContext); |
CGContextSetFillColorWithColor(outputContext, tintColor.CGColor); |
CGContextFillRect(outputContext, outputImageRectInPoints); |
CGContextRestoreGState(outputContext); |
} |
#endif |
// Output image is ready. |
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext(); |
UIGraphicsEndImageContext(); |
return outputImage; |
#undef ENABLE_BLUR |
#undef ENABLE_SATURATION_ADJUSTMENT |
#undef ENABLE_TINT |
} |
//| ---------------------------------------------------------------------------- |
// Helper function to handle deferred cleanup of a buffer. |
// |
void cleanupBuffer(void *userData, void *buf_data) |
{ free(buf_data); } |
@end |
Copyright © 2014 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2014-07-31