diff --git a/SDWebImage/Core/SDImageTransformer.h b/SDWebImage/Core/SDImageTransformer.h index 31b4370e..e01550a6 100644 --- a/SDWebImage/Core/SDImageTransformer.h +++ b/SDWebImage/Core/SDImageTransformer.h @@ -223,11 +223,14 @@ FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullab The tint color. */ @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)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor; ++ (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode; @end diff --git a/SDWebImage/Core/SDImageTransformer.m b/SDWebImage/Core/SDImageTransformer.m index 8e7a3e2a..813be2e8 100644 --- a/SDWebImage/Core/SDImageTransformer.m +++ b/SDWebImage/Core/SDImageTransformer.m @@ -245,20 +245,26 @@ NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullable key, CGSize thu @interface SDImageTintTransformer () @property (nonatomic, strong, nonnull) UIColor *tintColor; +@property (nonatomic, assign) CGBlendMode blendMode; @end @implementation SDImageTintTransformer + (instancetype)transformerWithColor:(UIColor *)tintColor { + return [self transformerWithColor:tintColor blendMode:kCGBlendModeSourceIn]; +} + ++ (instancetype)transformerWithColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode { SDImageTintTransformer *transformer = [SDImageTintTransformer new]; transformer.tintColor = tintColor; + transformer.blendMode = blendMode; return transformer; } - (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 { diff --git a/SDWebImage/Core/UIImage+Transform.h b/SDWebImage/Core/UIImage+Transform.h index 60b488ae..f4c50d3a 100644 --- a/SDWebImage/Core/UIImage+Transform.h +++ b/SDWebImage/Core/UIImage+Transform.h @@ -98,13 +98,24 @@ typedef NS_OPTIONS(NSUInteger, SDRectCorner) { #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. @return The new image with the tint color. */ - (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. @note The point's x/y will be converted into integer. diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 65c7d638..bed7279d 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -536,28 +536,177 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull #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 { + return [self sd_tintedImageWithColor:tintColor blendMode:kCGBlendModeSourceIn]; +} + +- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode { BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__; if (!hasTint) { return self; } + // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing #if SD_UIKIT || SD_MAC // CIImage shortcut - if (self.CIImage) { - CIImage *ciImage = self.CIImage; + CIImage *ciImage = self.CIImage; + if (ciImage) { CIImage *colorImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:tintColor]]; colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; - CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"]; - [filter setValue:colorImage forKey:kCIInputImageKey]; - [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; - ciImage = filter.outputImage; + 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:ciImage forKey:kCIInputBackgroundImageKey]; + 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 UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif return image; + } } #endif @@ -565,9 +714,6 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull CGRect rect = { CGPointZero, size }; CGFloat scale = self.scale; - // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing - CGBlendMode blendMode = kCGBlendModeSourceAtop; - SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; diff --git a/Tests/Tests/SDImageTransformerTests.m b/Tests/Tests/SDImageTransformerTests.m index 7ad802f9..550a854b 100644 --- a/Tests/Tests/SDImageTransformerTests.m +++ b/Tests/Tests/SDImageTransformerTests.m @@ -242,7 +242,35 @@ static void SDAssertCGImageFirstComponentWhite(CGImageRef image, OSType pixelTyp // Check rounded corner operation not inversion the image UIColor *topCenterColor = [tintedImage sd_colorAtPoint:CGPointMake(150, 20)]; 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 { [self test07UIImageTransformBlurWithImage:self.testImageCG]; @@ -353,7 +381,7 @@ static void SDAssertCGImageFirstComponentWhite(CGImageRef image, OSType pixelTyp @"SDImageRoundCornerTransformer(50.000000,18446744073709551615,1.000000,#ff000000)", @"SDImageFlippingTransformer(1,1)", @"SDImageCroppingTransformer({0.000000,0.000000,50.000000,50.000000})", - @"SDImageTintTransformer(#00000000)", + @"SDImageTintTransformer(#00000000,18)", @"SDImageBlurTransformer(5.000000)", @"SDImageFilterTransformer(CIColorInvert)" ];