2020-02-03 17:31:09 +04:00

745 lines
24 KiB
Plaintext

//
// ASImageNode.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASImageNode.h>
#import <tgmath.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASDimension.h>
#import "ASDisplayNode+FrameworkPrivate.h"
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import <AsyncDisplayKit/ASLayout.h>
#import "ASImageNode+CGExtras.h"
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASHashing.h>
#import "ASWeakMap.h"
#import <AsyncDisplayKit/CoreGraphics+ASConvenience.h>
#import "_ASCoreAnimationExtras.h"
// TODO: It would be nice to remove this dependency; it's the only subclass using more than +FrameworkSubclasses.h
#import "ASDisplayNodeInternal.h"
static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry);
@interface ASImageNodeDrawParameters : NSObject {
@package
UIImage *_image;
BOOL _opaque;
CGRect _bounds;
CGFloat _contentsScale;
UIColor *_backgroundColor;
UIViewContentMode _contentMode;
BOOL _cropEnabled;
BOOL _forceUpscaling;
CGSize _forcedSize;
CGRect _cropRect;
CGRect _cropDisplayBounds;
asimagenode_modification_block_t _imageModificationBlock;
ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext;
ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext;
ASImageNodeDrawParametersBlock _didDrawBlock;
}
@end
@implementation ASImageNodeDrawParameters
@end
/**
* Contains all data that is needed to generate the content bitmap.
*/
@interface ASImageNodeContentsKey : NSObject
@property (nonatomic) UIImage *image;
@property CGSize backingSize;
@property CGRect imageDrawRect;
@property BOOL isOpaque;
@property (nonatomic, copy) UIColor *backgroundColor;
@property (nonatomic) ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext;
@property (nonatomic) ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext;
@property (nonatomic) asimagenode_modification_block_t imageModificationBlock;
@end
@implementation ASImageNodeContentsKey
- (BOOL)isEqual:(id)object
{
if (self == object) {
return YES;
}
// Optimization opportunity: The `isKindOfClass` call here could be avoided by not using the NSObject `isEqual:`
// convention and instead using a custom comparison function that assumes all items are heterogeneous.
// However, profiling shows that our entire `isKindOfClass` expression is only ~1/40th of the total
// overheard of our caching, so it's likely not high-impact.
if ([object isKindOfClass:[ASImageNodeContentsKey class]]) {
ASImageNodeContentsKey *other = (ASImageNodeContentsKey *)object;
return [_image isEqual:other.image]
&& CGSizeEqualToSize(_backingSize, other.backingSize)
&& CGRectEqualToRect(_imageDrawRect, other.imageDrawRect)
&& _isOpaque == other.isOpaque
&& [_backgroundColor isEqual:other.backgroundColor]
&& _willDisplayNodeContentWithRenderingContext == other.willDisplayNodeContentWithRenderingContext
&& _didDisplayNodeContentWithRenderingContext == other.didDisplayNodeContentWithRenderingContext
&& _imageModificationBlock == other.imageModificationBlock;
} else {
return NO;
}
}
- (NSUInteger)hash
{
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wpadded"
struct {
NSUInteger imageHash;
CGSize backingSize;
CGRect imageDrawRect;
NSInteger isOpaque;
NSUInteger backgroundColorHash;
void *willDisplayNodeContentWithRenderingContext;
void *didDisplayNodeContentWithRenderingContext;
void *imageModificationBlock;
#pragma clang diagnostic pop
} data = {
_image.hash,
_backingSize,
_imageDrawRect,
_isOpaque,
_backgroundColor.hash,
(void *)_willDisplayNodeContentWithRenderingContext,
(void *)_didDisplayNodeContentWithRenderingContext,
(void *)_imageModificationBlock
};
return ASHashBytes(&data, sizeof(data));
}
@end
@implementation ASImageNode
{
@private
UIImage *_image;
ASWeakMapEntry *_weakCacheEntry; // Holds a reference that keeps our contents in cache.
UIColor *_placeholderColor;
void (^_displayCompletionBlock)(BOOL canceled);
// Cropping.
BOOL _cropEnabled; // Defaults to YES.
BOOL _forceUpscaling; //Defaults to NO.
CGSize _forcedSize; //Defaults to CGSizeZero, indicating no forced size.
CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0)
CGRect _cropDisplayBounds; // Defaults to CGRectNull
}
@synthesize image = _image;
@synthesize imageModificationBlock = _imageModificationBlock;
#pragma mark - Lifecycle
- (instancetype)init
{
if (!(self = [super init]))
return nil;
// TODO can this be removed?
self.contentsScale = ASScreenScale();
self.contentMode = UIViewContentModeScaleAspectFill;
self.opaque = NO;
self.clipsToBounds = YES;
// If no backgroundColor is set to the image node and it's a subview of UITableViewCell, UITableView is setting
// the opaque value of all subviews to YES if highlighting / selection is happening and does not set it back to the
// initial value. With setting a explicit backgroundColor we can prevent that change.
self.backgroundColor = [UIColor clearColor];
_cropEnabled = YES;
_forceUpscaling = NO;
_cropRect = CGRectMake(0.5, 0.5, 0, 0);
_cropDisplayBounds = CGRectNull;
_placeholderColor = ASDisplayNodeDefaultPlaceholderColor();
#ifndef MINIMAL_ASDK
_animatedImageRunLoopMode = ASAnimatedImageDefaultRunLoopMode;
#endif
return self;
}
- (void)dealloc
{
// Invalidate all components around animated images
#ifndef MINIMAL_ASDK
[self invalidateAnimatedImage];
#endif
}
#pragma mark - Placeholder
- (UIImage *)placeholderImage
{
// FIXME: Replace this implementation with reusable CALayers that have .backgroundColor set.
// This would completely eliminate the memory and performance cost of the backing store.
CGSize size = self.calculatedSize;
if ((size.width * size.height) < CGFLOAT_EPSILON) {
return nil;
}
AS::MutexLocker l(__instanceLock__);
ASGraphicsBeginImageContextWithOptions(size, NO, 1);
[self.placeholderColor setFill];
UIRectFill(CGRectMake(0, 0, size.width, size.height));
UIImage *image = ASGraphicsGetImageAndEndCurrentContext();
return image;
}
#pragma mark - Layout and Sizing
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
{
const auto image = ASLockedSelf(_image);
if (image == nil) {
return [super calculateSizeThatFits:constrainedSize];
}
return image.size;
}
#pragma mark - Setter / Getter
- (void)setImage:(UIImage *)image
{
AS::MutexLocker l(__instanceLock__);
[self _locked_setImage:image];
}
- (void)_locked_setImage:(UIImage *)image
{
ASAssertLocked(__instanceLock__);
if (ASObjectIsEqual(_image, image)) {
return;
}
UIImage *oldImage = _image;
_image = image;
if (image != nil) {
// We explicitly call setNeedsDisplay in this case, although we know setNeedsDisplay will be called with lock held.
// Therefore we have to be careful in methods that are involved with setNeedsDisplay to not run into a deadlock
[self setNeedsDisplay];
if (_displayWithoutProcessing && ASDisplayNodeThreadIsMain()) {
BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
if (stretchable) {
ASDisplayNodeSetResizableContents(self, image);
} else {
self.contents = (id)image.CGImage;
}
return;
}
} else {
self.contents = nil;
}
// Destruction of bigger images on the main thread can be expensive
// and can take some time, so we dispatch onto a bg queue to
// actually dealloc.
CGSize oldImageSize = oldImage.size;
BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width
|| oldImageSize.height > kMinReleaseImageOnBackgroundSize.height;
if (shouldReleaseImageOnBackgroundThread) {
ASPerformBackgroundDeallocation(&oldImage);
}
}
- (UIImage *)image
{
return ASLockedSelf(_image);
}
- (UIColor *)placeholderColor
{
return ASLockedSelf(_placeholderColor);
}
- (void)setPlaceholderColor:(UIColor *)placeholderColor
{
ASLockScopeSelf();
if (ASCompareAssignCopy(_placeholderColor, placeholderColor)) {
_placeholderEnabled = (placeholderColor != nil);
}
}
#pragma mark - Drawing
- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
{
ASLockScopeSelf();
ASImageNodeDrawParameters *drawParameters = [[ASImageNodeDrawParameters alloc] init];
drawParameters->_image = _image;
drawParameters->_bounds = [self threadSafeBounds];
drawParameters->_opaque = self.opaque;
drawParameters->_contentsScale = _contentsScaleForDisplay;
drawParameters->_backgroundColor = self.backgroundColor;
drawParameters->_contentMode = self.contentMode;
drawParameters->_cropEnabled = _cropEnabled;
drawParameters->_forceUpscaling = _forceUpscaling;
drawParameters->_forcedSize = _forcedSize;
drawParameters->_cropRect = _cropRect;
drawParameters->_cropDisplayBounds = _cropDisplayBounds;
drawParameters->_imageModificationBlock = _imageModificationBlock;
drawParameters->_willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext;
drawParameters->_didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
// Hack for now to retain the weak entry that was created while this drawing happened
drawParameters->_didDrawBlock = ^(ASWeakMapEntry *entry){
ASLockScopeSelf();
_weakCacheEntry = entry;
};
return drawParameters;
}
+ (UIImage *)displayWithParameters:(id<NSObject>)parameter isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled
{
ASImageNodeDrawParameters *drawParameter = (ASImageNodeDrawParameters *)parameter;
UIImage *image = drawParameter->_image;
if (image == nil) {
return nil;
}
if (true) {
return image;
}
CGRect drawParameterBounds = drawParameter->_bounds;
BOOL forceUpscaling = drawParameter->_forceUpscaling;
CGSize forcedSize = drawParameter->_forcedSize;
BOOL cropEnabled = drawParameter->_cropEnabled;
BOOL isOpaque = drawParameter->_opaque;
UIColor *backgroundColor = drawParameter->_backgroundColor;
UIViewContentMode contentMode = drawParameter->_contentMode;
CGFloat contentsScale = drawParameter->_contentsScale;
CGRect cropDisplayBounds = drawParameter->_cropDisplayBounds;
CGRect cropRect = drawParameter->_cropRect;
asimagenode_modification_block_t imageModificationBlock = drawParameter->_imageModificationBlock;
ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = drawParameter->_willDisplayNodeContentWithRenderingContext;
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = drawParameter->_didDisplayNodeContentWithRenderingContext;
BOOL hasValidCropBounds = cropEnabled && !CGRectIsEmpty(cropDisplayBounds);
CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds);
ASDisplayNodeAssert(contentsScale > 0, @"invalid contentsScale at display time");
// if the image is resizable, bail early since the image has likely already been configured
BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
if (stretchable) {
if (imageModificationBlock != NULL) {
image = imageModificationBlock(image);
}
return image;
}
CGSize imageSize = image.size;
CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale);
CGSize boundsSizeInPixels = CGSizeMake(std::floor(bounds.size.width * contentsScale), std::floor(bounds.size.height * contentsScale));
BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill ||
contentMode == UIViewContentModeScaleAspectFit ||
contentMode == UIViewContentModeCenter;
CGSize backingSize = CGSizeZero;
CGRect imageDrawRect = CGRectZero;
if (boundsSizeInPixels.width * contentsScale < 1.0f || boundsSizeInPixels.height * contentsScale < 1.0f ||
imageSizeInPixels.width < 1.0f || imageSizeInPixels.height < 1.0f) {
return nil;
}
// If we're not supposed to do any cropping, just decode image at original size
if (!cropEnabled || !contentModeSupported || stretchable) {
backingSize = imageSizeInPixels;
imageDrawRect = (CGRect){.size = backingSize};
} else {
if (CGSizeEqualToSize(CGSizeZero, forcedSize) == NO) {
//scale forced size
forcedSize.width *= contentsScale;
forcedSize.height *= contentsScale;
}
ASCroppedImageBackingSizeAndDrawRectInBounds(imageSizeInPixels,
boundsSizeInPixels,
contentMode,
cropRect,
forceUpscaling,
forcedSize,
&backingSize,
&imageDrawRect);
}
if (backingSize.width <= 0.0f || backingSize.height <= 0.0f ||
imageDrawRect.size.width <= 0.0f || imageDrawRect.size.height <= 0.0f) {
return nil;
}
ASImageNodeContentsKey *contentsKey = [[ASImageNodeContentsKey alloc] init];
contentsKey.image = image;
contentsKey.backingSize = backingSize;
contentsKey.imageDrawRect = imageDrawRect;
contentsKey.isOpaque = isOpaque;
contentsKey.backgroundColor = backgroundColor;
contentsKey.willDisplayNodeContentWithRenderingContext = willDisplayNodeContentWithRenderingContext;
contentsKey.didDisplayNodeContentWithRenderingContext = didDisplayNodeContentWithRenderingContext;
contentsKey.imageModificationBlock = imageModificationBlock;
if (isCancelled()) {
return nil;
}
ASWeakMapEntry<UIImage *> *entry = [self.class contentsForkey:contentsKey
drawParameters:parameter
isCancelled:isCancelled];
// If nil, we were cancelled.
if (entry == nil) {
return nil;
}
if (drawParameter->_didDrawBlock) {
drawParameter->_didDrawBlock(entry);
}
return entry.value;
}
static ASWeakMap<ASImageNodeContentsKey *, UIImage *> *cache = nil;
+ (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
{
static dispatch_once_t onceToken;
static AS::Mutex *cacheLock = nil;
dispatch_once(&onceToken, ^{
cacheLock = new AS::Mutex();
});
{
AS::MutexLocker l(*cacheLock);
if (!cache) {
cache = [[ASWeakMap alloc] init];
}
ASWeakMapEntry *entry = [cache entryForKey:key];
if (entry != nil) {
return entry;
}
}
// cache miss
UIImage *contents = [self createContentsForkey:key drawParameters:drawParameters isCancelled:isCancelled];
if (contents == nil) { // If nil, we were cancelled
return nil;
}
{
AS::MutexLocker l(*cacheLock);
return [cache setObject:contents forKey:key];
}
}
+ (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
{
// The following `ASGraphicsBeginImageContextWithOptions` call will sometimes take take longer than 5ms on an
// A5 processor for a 400x800 backingSize.
// Check for cancellation before we call it.
if (isCancelled()) {
return nil;
}
// Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds
// will do its rounding on pixel instead of point boundaries
ASGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0);
BOOL contextIsClean = YES;
CGContextRef context = UIGraphicsGetCurrentContext();
if (context && key.willDisplayNodeContentWithRenderingContext) {
key.willDisplayNodeContentWithRenderingContext(context, drawParameters);
contextIsClean = NO;
}
// if view is opaque, fill the context with background color
if (key.isOpaque && key.backgroundColor) {
[key.backgroundColor setFill];
UIRectFill({ .size = key.backingSize });
contextIsClean = NO;
}
// iOS 9 appears to contain a thread safety regression when drawing the same CGImageRef on
// multiple threads concurrently. In fact, instead of crashing, it appears to deadlock.
// The issue is present in Mac OS X El Capitan and has been seen hanging Pro apps like Adobe Premiere,
// as well as iOS games, and a small number of ASDK apps that provide the same image reference
// to many separate ASImageNodes. A workaround is to set .displaysAsynchronously = NO for the nodes
// that may get the same pointer for a given UI asset image, etc.
// FIXME: We should replace @synchronized here, probably using a global, locked NSMutableSet, and
// only if the object already exists in the set we should create a semaphore to signal waiting threads
// upon removal of the object from the set when the operation completes.
// Another option is to have ASDisplayNode+AsyncDisplay coordinate these cases, and share the decoded buffer.
// Details tracked in https://github.com/facebook/AsyncDisplayKit/issues/1068
UIImage *image = key.image;
BOOL canUseCopy = (contextIsClean || ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage)));
CGBlendMode blendMode = canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal;
@synchronized(image) {
[image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1];
}
if (context && key.didDisplayNodeContentWithRenderingContext) {
key.didDisplayNodeContentWithRenderingContext(context, drawParameters);
}
// Check cancellation one last time before forming image.
if (isCancelled()) {
ASGraphicsEndImageContext();
return nil;
}
UIImage *result = ASGraphicsGetImageAndEndCurrentContext();
if (key.imageModificationBlock) {
result = key.imageModificationBlock(result);
}
return result;
}
- (void)displayDidFinish
{
[super displayDidFinish];
__instanceLock__.lock();
UIImage *image = _image;
void (^displayCompletionBlock)(BOOL canceled) = _displayCompletionBlock;
BOOL shouldPerformDisplayCompletionBlock = (image && displayCompletionBlock);
// Clear the ivar now. The block is retained and will be executed shortly.
if (shouldPerformDisplayCompletionBlock) {
_displayCompletionBlock = nil;
}
__instanceLock__.unlock();
// If we've got a block to perform after displaying, do it.
if (shouldPerformDisplayCompletionBlock) {
displayCompletionBlock(NO);
}
}
- (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock
{
if (self.displaySuspended) {
if (displayCompletionBlock)
displayCompletionBlock(YES);
return;
}
// Stash the block and call-site queue. We'll invoke it in -displayDidFinish.
{
AS::MutexLocker l(__instanceLock__);
if (_displayCompletionBlock != displayCompletionBlock) {
_displayCompletionBlock = displayCompletionBlock;
}
}
[self setNeedsDisplay];
}
#pragma mark Interface State
- (void)clearContents
{
[super clearContents];
AS::MutexLocker l(__instanceLock__);
_weakCacheEntry = nil; // release contents from the cache.
}
#pragma mark - Cropping
- (BOOL)isCropEnabled
{
AS::MutexLocker l(__instanceLock__);
return _cropEnabled;
}
- (void)setCropEnabled:(BOOL)cropEnabled
{
[self setCropEnabled:cropEnabled recropImmediately:NO inBounds:self.bounds];
}
- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds
{
__instanceLock__.lock();
if (_cropEnabled == cropEnabled) {
__instanceLock__.unlock();
return;
}
_cropEnabled = cropEnabled;
_cropDisplayBounds = cropBounds;
UIImage *image = _image;
__instanceLock__.unlock();
// If we have an image to display, display it, respecting our recrop flag.
if (image != nil) {
ASPerformBlockOnMainThread(^{
if (recropImmediately)
[self displayImmediately];
else
[self setNeedsDisplay];
});
}
}
- (CGRect)cropRect
{
AS::MutexLocker l(__instanceLock__);
return _cropRect;
}
- (void)setCropRect:(CGRect)cropRect
{
{
AS::MutexLocker l(__instanceLock__);
if (CGRectEqualToRect(_cropRect, cropRect)) {
return;
}
_cropRect = cropRect;
}
// TODO: this logic needs to be updated to respect cropRect.
CGSize boundsSize = self.bounds.size;
CGSize imageSize = self.image.size;
BOOL isCroppingImage = ((boundsSize.width < imageSize.width) || (boundsSize.height < imageSize.height));
// Re-display if we need to.
ASPerformBlockOnMainThread(^{
if (self.nodeLoaded && self.contentMode == UIViewContentModeScaleAspectFill && isCroppingImage)
[self setNeedsDisplay];
});
}
- (BOOL)forceUpscaling
{
AS::MutexLocker l(__instanceLock__);
return _forceUpscaling;
}
- (void)setForceUpscaling:(BOOL)forceUpscaling
{
AS::MutexLocker l(__instanceLock__);
_forceUpscaling = forceUpscaling;
}
- (CGSize)forcedSize
{
AS::MutexLocker l(__instanceLock__);
return _forcedSize;
}
- (void)setForcedSize:(CGSize)forcedSize
{
AS::MutexLocker l(__instanceLock__);
_forcedSize = forcedSize;
}
- (asimagenode_modification_block_t)imageModificationBlock
{
AS::MutexLocker l(__instanceLock__);
return _imageModificationBlock;
}
- (void)setImageModificationBlock:(asimagenode_modification_block_t)imageModificationBlock
{
AS::MutexLocker l(__instanceLock__);
_imageModificationBlock = imageModificationBlock;
}
#pragma mark - Debug
- (void)layout
{
[super layout];
}
@end
#pragma mark - Extras
asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor)
{
return ^(UIImage *originalImage) {
ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale);
UIBezierPath *roundOutline = [UIBezierPath bezierPathWithOvalInRect:(CGRect){CGPointZero, originalImage.size}];
// Make the image round
[roundOutline addClip];
// Draw the original image
[originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1];
// Draw a border on top.
if (borderWidth > 0.0) {
[borderColor setStroke];
[roundOutline setLineWidth:borderWidth];
[roundOutline stroke];
}
return ASGraphicsGetImageAndEndCurrentContext();
};
}
asimagenode_modification_block_t ASImageNodeTintColorModificationBlock(UIColor *color)
{
return ^(UIImage *originalImage) {
ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale);
// Set color and render template
[color setFill];
UIImage *templateImage = [originalImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[templateImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1];
UIImage *modifiedImage = ASGraphicsGetImageAndEndCurrentContext();
// if the original image was stretchy, keep it stretchy
if (!UIEdgeInsetsEqualToEdgeInsets(originalImage.capInsets, UIEdgeInsetsZero)) {
modifiedImage = [modifiedImage resizableImageWithCapInsets:originalImage.capInsets resizingMode:originalImage.resizingMode];
}
return modifiedImage;
};
}