Merge pull request #3749 from dreampiggy/feature/blend_mode
[Behavior changes] Add blend mode to UIImage+Transform tint color API, default blend mode changed to sourceIn
This commit is contained in:
commit
d5732787b7
|
@ -223,11 +223,14 @@ FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullab
|
||||||
The tint color.
|
The tint color.
|
||||||
*/
|
*/
|
||||||
@property (nonatomic, strong, readonly, nonnull) UIColor *tintColor;
|
@property (nonatomic, strong, readonly, nonnull) UIColor *tintColor;
|
||||||
|
/// The blend mode, defaults to `sourceIn` if you use the initializer without blend mode
|
||||||
|
@property (nonatomic, assign, readonly) CGBlendMode blendMode;
|
||||||
|
|
||||||
- (nonnull instancetype)init NS_UNAVAILABLE;
|
- (nonnull instancetype)init NS_UNAVAILABLE;
|
||||||
+ (nonnull instancetype)new NS_UNAVAILABLE;
|
+ (nonnull instancetype)new NS_UNAVAILABLE;
|
||||||
|
|
||||||
+ (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor;
|
+ (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor;
|
||||||
|
+ (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -245,20 +245,26 @@ NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullable key, CGSize thu
|
||||||
@interface SDImageTintTransformer ()
|
@interface SDImageTintTransformer ()
|
||||||
|
|
||||||
@property (nonatomic, strong, nonnull) UIColor *tintColor;
|
@property (nonatomic, strong, nonnull) UIColor *tintColor;
|
||||||
|
@property (nonatomic, assign) CGBlendMode blendMode;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation SDImageTintTransformer
|
@implementation SDImageTintTransformer
|
||||||
|
|
||||||
+ (instancetype)transformerWithColor:(UIColor *)tintColor {
|
+ (instancetype)transformerWithColor:(UIColor *)tintColor {
|
||||||
|
return [self transformerWithColor:tintColor blendMode:kCGBlendModeSourceIn];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (instancetype)transformerWithColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode {
|
||||||
SDImageTintTransformer *transformer = [SDImageTintTransformer new];
|
SDImageTintTransformer *transformer = [SDImageTintTransformer new];
|
||||||
transformer.tintColor = tintColor;
|
transformer.tintColor = tintColor;
|
||||||
|
transformer.blendMode = blendMode;
|
||||||
|
|
||||||
return transformer;
|
return transformer;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSString *)transformerKey {
|
- (NSString *)transformerKey {
|
||||||
return [NSString stringWithFormat:@"SDImageTintTransformer(%@)", self.tintColor.sd_hexString];
|
return [NSString stringWithFormat:@"SDImageTintTransformer(%@,%d)", self.tintColor.sd_hexString, self.blendMode];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key {
|
- (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key {
|
||||||
|
|
|
@ -98,13 +98,24 @@ typedef NS_OPTIONS(NSUInteger, SDRectCorner) {
|
||||||
#pragma mark - Image Blending
|
#pragma mark - Image Blending
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Return a tinted image with the given color. This actually use alpha blending of current image and the tint color.
|
Return a tinted image with the given color. This actually use `sourceIn` blend mode.
|
||||||
|
@note Before 5.20, this API actually use `sourceAtop` and cause naming confusing. After 5.20, we match UIKit's behavior using `sourceIn`.
|
||||||
|
|
||||||
@param tintColor The tint color.
|
@param tintColor The tint color.
|
||||||
@return The new image with the tint color.
|
@return The new image with the tint color.
|
||||||
*/
|
*/
|
||||||
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor;
|
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Return a tinted image with the given color and blend mode.
|
||||||
|
@note The blend mode treat `self` as background image (destination), treat `tintColor` as input image (source). So mostly you need `source` variant blend mode (use `sourceIn` not `destinationIn`), which is different from UIKit's `+[UIImage imageWithTintColor:]`.
|
||||||
|
|
||||||
|
@param tintColor The tint color.
|
||||||
|
@param blendMode The blend mode.
|
||||||
|
@return The new image with the tint color.
|
||||||
|
*/
|
||||||
|
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Return the pixel color at specify position. The point is from the top-left to the bottom-right and 0-based. The returned the color is always be RGBA format. The image must be CG-based.
|
Return the pixel color at specify position. The point is from the top-left to the bottom-right and 0-based. The returned the color is always be RGBA format. The image must be CG-based.
|
||||||
@note The point's x/y will be converted into integer.
|
@note The point's x/y will be converted into integer.
|
||||||
|
|
|
@ -536,22 +536,170 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
|
||||||
|
|
||||||
#pragma mark - Image Blending
|
#pragma mark - Image Blending
|
||||||
|
|
||||||
|
static NSString * _Nullable SDGetCIFilterNameFromBlendMode(CGBlendMode blendMode) {
|
||||||
|
// CGBlendMode: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-CJBIJEFG
|
||||||
|
// CIFilter: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW71
|
||||||
|
NSString *filterName;
|
||||||
|
switch (blendMode) {
|
||||||
|
case kCGBlendModeMultiply:
|
||||||
|
filterName = @"CIMultiplyBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeScreen:
|
||||||
|
filterName = @"CIScreenBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeOverlay:
|
||||||
|
filterName = @"CIOverlayBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeDarken:
|
||||||
|
filterName = @"CIDarkenBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeLighten:
|
||||||
|
filterName = @"CILightenBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeColorDodge:
|
||||||
|
filterName = @"CIColorDodgeBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeColorBurn:
|
||||||
|
filterName = @"CIColorBurnBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeSoftLight:
|
||||||
|
filterName = @"CISoftLightBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeHardLight:
|
||||||
|
filterName = @"CIHardLightBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeDifference:
|
||||||
|
filterName = @"CIDifferenceBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeExclusion:
|
||||||
|
filterName = @"CIExclusionBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeHue:
|
||||||
|
filterName = @"CIHueBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeSaturation:
|
||||||
|
filterName = @"CISaturationBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeColor:
|
||||||
|
// Color blend mode uses the luminance values of the background with the hue and saturation values of the source image.
|
||||||
|
filterName = @"CIColorBlendMode";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeLuminosity:
|
||||||
|
filterName = @"CILuminosityBlendMode";
|
||||||
|
break;
|
||||||
|
|
||||||
|
// macOS 10.5+
|
||||||
|
case kCGBlendModeSourceAtop:
|
||||||
|
case kCGBlendModeDestinationAtop:
|
||||||
|
filterName = @"CISourceAtopCompositing";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeSourceIn:
|
||||||
|
case kCGBlendModeDestinationIn:
|
||||||
|
filterName = @"CISourceInCompositing";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeSourceOut:
|
||||||
|
case kCGBlendModeDestinationOut:
|
||||||
|
filterName = @"CISourceOutCompositing";
|
||||||
|
break;
|
||||||
|
case kCGBlendModeNormal: // SourceOver
|
||||||
|
case kCGBlendModeDestinationOver:
|
||||||
|
filterName = @"CISourceOverCompositing";
|
||||||
|
break;
|
||||||
|
|
||||||
|
// need special handling
|
||||||
|
case kCGBlendModeClear:
|
||||||
|
// use clear color instead
|
||||||
|
break;
|
||||||
|
case kCGBlendModeCopy:
|
||||||
|
// use input color instead
|
||||||
|
break;
|
||||||
|
case kCGBlendModeXOR:
|
||||||
|
// unsupported
|
||||||
|
break;
|
||||||
|
case kCGBlendModePlusDarker:
|
||||||
|
// chain filters
|
||||||
|
break;
|
||||||
|
case kCGBlendModePlusLighter:
|
||||||
|
// chain filters
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return filterName;
|
||||||
|
}
|
||||||
|
|
||||||
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor {
|
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor {
|
||||||
|
return [self sd_tintedImageWithColor:tintColor blendMode:kCGBlendModeSourceIn];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode {
|
||||||
BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__;
|
BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__;
|
||||||
if (!hasTint) {
|
if (!hasTint) {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing
|
||||||
#if SD_UIKIT || SD_MAC
|
#if SD_UIKIT || SD_MAC
|
||||||
// CIImage shortcut
|
// CIImage shortcut
|
||||||
if (self.CIImage) {
|
|
||||||
CIImage *ciImage = self.CIImage;
|
CIImage *ciImage = self.CIImage;
|
||||||
|
if (ciImage) {
|
||||||
CIImage *colorImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:tintColor]];
|
CIImage *colorImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:tintColor]];
|
||||||
colorImage = [colorImage imageByCroppingToRect:ciImage.extent];
|
colorImage = [colorImage imageByCroppingToRect:ciImage.extent];
|
||||||
CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"];
|
NSString *filterName = SDGetCIFilterNameFromBlendMode(blendMode);
|
||||||
|
// Some blend mode is not nativelly supported
|
||||||
|
if (filterName) {
|
||||||
|
CIFilter *filter = [CIFilter filterWithName:filterName];
|
||||||
[filter setValue:colorImage forKey:kCIInputImageKey];
|
[filter setValue:colorImage forKey:kCIInputImageKey];
|
||||||
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
|
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
|
||||||
ciImage = filter.outputImage;
|
ciImage = filter.outputImage;
|
||||||
|
} else {
|
||||||
|
if (blendMode == kCGBlendModeClear) {
|
||||||
|
// R = 0
|
||||||
|
CIColor *clearColor;
|
||||||
|
if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)) {
|
||||||
|
clearColor = CIColor.clearColor;
|
||||||
|
} else {
|
||||||
|
clearColor = [[CIColor alloc] initWithColor:UIColor.clearColor];
|
||||||
|
}
|
||||||
|
colorImage = [CIImage imageWithColor:clearColor];
|
||||||
|
colorImage = [colorImage imageByCroppingToRect:ciImage.extent];
|
||||||
|
ciImage = colorImage;
|
||||||
|
} else if (blendMode == kCGBlendModeCopy) {
|
||||||
|
// R = S
|
||||||
|
ciImage = colorImage;
|
||||||
|
} else if (blendMode == kCGBlendModePlusLighter) {
|
||||||
|
// R = MIN(1, S + D)
|
||||||
|
// S + D
|
||||||
|
CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"];
|
||||||
|
[filter setValue:colorImage forKey:kCIInputImageKey];
|
||||||
|
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
|
||||||
|
ciImage = filter.outputImage;
|
||||||
|
// MIN
|
||||||
|
ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil];
|
||||||
|
} else if (blendMode == kCGBlendModePlusDarker) {
|
||||||
|
// R = MAX(0, (1 - D) + (1 - S))
|
||||||
|
// (1 - D)
|
||||||
|
CIFilter *filter1 = [CIFilter filterWithName:@"CIColorControls"];
|
||||||
|
[filter1 setValue:ciImage forKey:kCIInputImageKey];
|
||||||
|
[filter1 setValue:@(-0.5) forKey:kCIInputBrightnessKey];
|
||||||
|
ciImage = filter1.outputImage;
|
||||||
|
// (1 - S)
|
||||||
|
CIFilter *filter2 = [CIFilter filterWithName:@"CIColorControls"];
|
||||||
|
[filter2 setValue:colorImage forKey:kCIInputImageKey];
|
||||||
|
[filter2 setValue:@(-0.5) forKey:kCIInputBrightnessKey];
|
||||||
|
colorImage = filter2.outputImage;
|
||||||
|
// +
|
||||||
|
CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"];
|
||||||
|
[filter setValue:colorImage forKey:kCIInputImageKey];
|
||||||
|
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
|
||||||
|
ciImage = filter.outputImage;
|
||||||
|
// MAX
|
||||||
|
ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil];
|
||||||
|
} else {
|
||||||
|
SD_LOG("UIImage+Transform error: Unsupported blend mode: %d", blendMode);
|
||||||
|
ciImage = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ciImage) {
|
||||||
#if SD_UIKIT
|
#if SD_UIKIT
|
||||||
UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation];
|
UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation];
|
||||||
#else
|
#else
|
||||||
|
@ -559,15 +707,13 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
|
||||||
#endif
|
#endif
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
CGSize size = self.size;
|
CGSize size = self.size;
|
||||||
CGRect rect = { CGPointZero, size };
|
CGRect rect = { CGPointZero, size };
|
||||||
CGFloat scale = self.scale;
|
CGFloat scale = self.scale;
|
||||||
|
|
||||||
// blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing
|
|
||||||
CGBlendMode blendMode = kCGBlendModeSourceAtop;
|
|
||||||
|
|
||||||
SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init];
|
SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init];
|
||||||
format.scale = scale;
|
format.scale = scale;
|
||||||
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];
|
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];
|
||||||
|
|
|
@ -242,7 +242,35 @@ static void SDAssertCGImageFirstComponentWhite(CGImageRef image, OSType pixelTyp
|
||||||
// Check rounded corner operation not inversion the image
|
// Check rounded corner operation not inversion the image
|
||||||
UIColor *topCenterColor = [tintedImage sd_colorAtPoint:CGPointMake(150, 20)];
|
UIColor *topCenterColor = [tintedImage sd_colorAtPoint:CGPointMake(150, 20)];
|
||||||
expect([topCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy();
|
expect([topCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy();
|
||||||
|
|
||||||
|
UIImage *tintedSourceInImage = [testImage sd_tintedImageWithColor:tintColor blendMode:kCGBlendModeSourceIn];
|
||||||
|
topCenterColor = [tintedSourceInImage sd_colorAtPoint:CGPointMake(150, 20)];
|
||||||
|
#if SD_UIKIT
|
||||||
|
// Test UIKit's tint color behavior
|
||||||
|
if (@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
|
||||||
|
UIImage *tintedSystemImage = [testImage imageWithTintColor:tintColor renderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||||
|
UIGraphicsImageRendererFormat *format = UIGraphicsImageRendererFormat.preferredFormat;
|
||||||
|
format.scale = tintedSourceInImage.scale;
|
||||||
|
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:tintedSystemImage.size format:format];
|
||||||
|
// Draw template image
|
||||||
|
tintedSystemImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
|
||||||
|
[tintedSystemImage drawInRect:CGRectMake(0, 0, tintedSystemImage.size.width, tintedSystemImage.size.height)];
|
||||||
|
}];
|
||||||
|
UIColor *testColor1 = [tintedSourceInImage sd_colorAtPoint:CGPointMake(150, 20)];
|
||||||
|
UIColor *testColor2 = [tintedSystemImage sd_colorAtPoint:CGPointMake(150, 20)];
|
||||||
|
CGFloat r1, g1, b1, a1;
|
||||||
|
CGFloat r2, g2, b2, a2;
|
||||||
|
[testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
|
||||||
|
[testColor2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
|
||||||
|
expect(r1).beCloseToWithin(r2, 0.01);
|
||||||
|
expect(g1).beCloseToWithin(g2, 0.01);
|
||||||
|
expect(b1).beCloseToWithin(b2, 0.01);
|
||||||
|
expect(a1).beCloseToWithin(a2, 0.01);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
expect([topCenterColor.sd_hexString isEqualToString:tintColor.sd_hexString]).beTruthy();
|
||||||
|
}
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
|
||||||
- (void)test07UIImageTransformBlurCG {
|
- (void)test07UIImageTransformBlurCG {
|
||||||
[self test07UIImageTransformBlurWithImage:self.testImageCG];
|
[self test07UIImageTransformBlurWithImage:self.testImageCG];
|
||||||
|
@ -353,7 +381,7 @@ static void SDAssertCGImageFirstComponentWhite(CGImageRef image, OSType pixelTyp
|
||||||
@"SDImageRoundCornerTransformer(50.000000,18446744073709551615,1.000000,#ff000000)",
|
@"SDImageRoundCornerTransformer(50.000000,18446744073709551615,1.000000,#ff000000)",
|
||||||
@"SDImageFlippingTransformer(1,1)",
|
@"SDImageFlippingTransformer(1,1)",
|
||||||
@"SDImageCroppingTransformer({0.000000,0.000000,50.000000,50.000000})",
|
@"SDImageCroppingTransformer({0.000000,0.000000,50.000000,50.000000})",
|
||||||
@"SDImageTintTransformer(#00000000)",
|
@"SDImageTintTransformer(#00000000,18)",
|
||||||
@"SDImageBlurTransformer(5.000000)",
|
@"SDImageBlurTransformer(5.000000)",
|
||||||
@"SDImageFilterTransformer(CIColorInvert)"
|
@"SDImageFilterTransformer(CIColorInvert)"
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue