Skip to content

Commit d573278

Browse files
authored
Merge pull request SDWebImage#3749 from dreampiggy/feature/blend_mode
[Behavior changes] Add blend mode to UIImage+Transform tint color API, default blend mode changed to sourceIn
2 parents c8f74d2 + 05e1840 commit d573278

File tree

5 files changed

+206
-12
lines changed

5 files changed

+206
-12
lines changed

SDWebImage/Core/SDImageTransformer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,14 @@ FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullab
223223
The tint color.
224224
*/
225225
@property (nonatomic, strong, readonly, nonnull) UIColor *tintColor;
226+
/// The blend mode, defaults to `sourceIn` if you use the initializer without blend mode
227+
@property (nonatomic, assign, readonly) CGBlendMode blendMode;
226228

227229
- (nonnull instancetype)init NS_UNAVAILABLE;
228230
+ (nonnull instancetype)new NS_UNAVAILABLE;
229231

230232
+ (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor;
233+
+ (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode;
231234

232235
@end
233236

SDWebImage/Core/SDImageTransformer.m

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,20 +245,26 @@ - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key {
245245
@interface SDImageTintTransformer ()
246246

247247
@property (nonatomic, strong, nonnull) UIColor *tintColor;
248+
@property (nonatomic, assign) CGBlendMode blendMode;
248249

249250
@end
250251

251252
@implementation SDImageTintTransformer
252253

253254
+ (instancetype)transformerWithColor:(UIColor *)tintColor {
255+
return [self transformerWithColor:tintColor blendMode:kCGBlendModeSourceIn];
256+
}
257+
258+
+ (instancetype)transformerWithColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode {
254259
SDImageTintTransformer *transformer = [SDImageTintTransformer new];
255260
transformer.tintColor = tintColor;
261+
transformer.blendMode = blendMode;
256262

257263
return transformer;
258264
}
259265

260266
- (NSString *)transformerKey {
261-
return [NSString stringWithFormat:@"SDImageTintTransformer(%@)", self.tintColor.sd_hexString];
267+
return [NSString stringWithFormat:@"SDImageTintTransformer(%@,%d)", self.tintColor.sd_hexString, self.blendMode];
262268
}
263269

264270
- (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key {

SDWebImage/Core/UIImage+Transform.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,24 @@ typedef NS_OPTIONS(NSUInteger, SDRectCorner) {
9898
#pragma mark - Image Blending
9999

100100
/**
101-
Return a tinted image with the given color. This actually use alpha blending of current image and the tint color.
101+
Return a tinted image with the given color. This actually use `sourceIn` blend mode.
102+
@note Before 5.20, this API actually use `sourceAtop` and cause naming confusing. After 5.20, we match UIKit's behavior using `sourceIn`.
102103
103104
@param tintColor The tint color.
104105
@return The new image with the tint color.
105106
*/
106107
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor;
107108

109+
/**
110+
Return a tinted image with the given color and blend mode.
111+
@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:]`.
112+
113+
@param tintColor The tint color.
114+
@param blendMode The blend mode.
115+
@return The new image with the tint color.
116+
*/
117+
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode;
118+
108119
/**
109120
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.
110121
@note The point's x/y will be converted into integer.

SDWebImage/Core/UIImage+Transform.m

Lines changed: 155 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -536,38 +536,184 @@ - (nullable UIImage *)sd_flippedImageWithHorizontal:(BOOL)horizontal vertical:(B
536536

537537
#pragma mark - Image Blending
538538

539+
static NSString * _Nullable SDGetCIFilterNameFromBlendMode(CGBlendMode blendMode) {
540+
// CGBlendMode: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-CJBIJEFG
541+
// CIFilter: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW71
542+
NSString *filterName;
543+
switch (blendMode) {
544+
case kCGBlendModeMultiply:
545+
filterName = @"CIMultiplyBlendMode";
546+
break;
547+
case kCGBlendModeScreen:
548+
filterName = @"CIScreenBlendMode";
549+
break;
550+
case kCGBlendModeOverlay:
551+
filterName = @"CIOverlayBlendMode";
552+
break;
553+
case kCGBlendModeDarken:
554+
filterName = @"CIDarkenBlendMode";
555+
break;
556+
case kCGBlendModeLighten:
557+
filterName = @"CILightenBlendMode";
558+
break;
559+
case kCGBlendModeColorDodge:
560+
filterName = @"CIColorDodgeBlendMode";
561+
break;
562+
case kCGBlendModeColorBurn:
563+
filterName = @"CIColorBurnBlendMode";
564+
break;
565+
case kCGBlendModeSoftLight:
566+
filterName = @"CISoftLightBlendMode";
567+
break;
568+
case kCGBlendModeHardLight:
569+
filterName = @"CIHardLightBlendMode";
570+
break;
571+
case kCGBlendModeDifference:
572+
filterName = @"CIDifferenceBlendMode";
573+
break;
574+
case kCGBlendModeExclusion:
575+
filterName = @"CIExclusionBlendMode";
576+
break;
577+
case kCGBlendModeHue:
578+
filterName = @"CIHueBlendMode";
579+
break;
580+
case kCGBlendModeSaturation:
581+
filterName = @"CISaturationBlendMode";
582+
break;
583+
case kCGBlendModeColor:
584+
// Color blend mode uses the luminance values of the background with the hue and saturation values of the source image.
585+
filterName = @"CIColorBlendMode";
586+
break;
587+
case kCGBlendModeLuminosity:
588+
filterName = @"CILuminosityBlendMode";
589+
break;
590+
591+
// macOS 10.5+
592+
case kCGBlendModeSourceAtop:
593+
case kCGBlendModeDestinationAtop:
594+
filterName = @"CISourceAtopCompositing";
595+
break;
596+
case kCGBlendModeSourceIn:
597+
case kCGBlendModeDestinationIn:
598+
filterName = @"CISourceInCompositing";
599+
break;
600+
case kCGBlendModeSourceOut:
601+
case kCGBlendModeDestinationOut:
602+
filterName = @"CISourceOutCompositing";
603+
break;
604+
case kCGBlendModeNormal: // SourceOver
605+
case kCGBlendModeDestinationOver:
606+
filterName = @"CISourceOverCompositing";
607+
break;
608+
609+
// need special handling
610+
case kCGBlendModeClear:
611+
// use clear color instead
612+
break;
613+
case kCGBlendModeCopy:
614+
// use input color instead
615+
break;
616+
case kCGBlendModeXOR:
617+
// unsupported
618+
break;
619+
case kCGBlendModePlusDarker:
620+
// chain filters
621+
break;
622+
case kCGBlendModePlusLighter:
623+
// chain filters
624+
break;
625+
}
626+
return filterName;
627+
}
628+
539629
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor {
630+
return [self sd_tintedImageWithColor:tintColor blendMode:kCGBlendModeSourceIn];
631+
}
632+
633+
- (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode {
540634
BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__;
541635
if (!hasTint) {
542636
return self;
543637
}
544638

639+
// blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing
545640
#if SD_UIKIT || SD_MAC
546641
// CIImage shortcut
547-
if (self.CIImage) {
548-
CIImage *ciImage = self.CIImage;
642+
CIImage *ciImage = self.CIImage;
643+
if (ciImage) {
549644
CIImage *colorImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:tintColor]];
550645
colorImage = [colorImage imageByCroppingToRect:ciImage.extent];
551-
CIFilter *filter = [CIFilter filterWithName:@"CISourceAtopCompositing"];
552-
[filter setValue:colorImage forKey:kCIInputImageKey];
553-
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
554-
ciImage = filter.outputImage;
646+
NSString *filterName = SDGetCIFilterNameFromBlendMode(blendMode);
647+
// Some blend mode is not nativelly supported
648+
if (filterName) {
649+
CIFilter *filter = [CIFilter filterWithName:filterName];
650+
[filter setValue:colorImage forKey:kCIInputImageKey];
651+
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
652+
ciImage = filter.outputImage;
653+
} else {
654+
if (blendMode == kCGBlendModeClear) {
655+
// R = 0
656+
CIColor *clearColor;
657+
if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)) {
658+
clearColor = CIColor.clearColor;
659+
} else {
660+
clearColor = [[CIColor alloc] initWithColor:UIColor.clearColor];
661+
}
662+
colorImage = [CIImage imageWithColor:clearColor];
663+
colorImage = [colorImage imageByCroppingToRect:ciImage.extent];
664+
ciImage = colorImage;
665+
} else if (blendMode == kCGBlendModeCopy) {
666+
// R = S
667+
ciImage = colorImage;
668+
} else if (blendMode == kCGBlendModePlusLighter) {
669+
// R = MIN(1, S + D)
670+
// S + D
671+
CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"];
672+
[filter setValue:colorImage forKey:kCIInputImageKey];
673+
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
674+
ciImage = filter.outputImage;
675+
// MIN
676+
ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil];
677+
} else if (blendMode == kCGBlendModePlusDarker) {
678+
// R = MAX(0, (1 - D) + (1 - S))
679+
// (1 - D)
680+
CIFilter *filter1 = [CIFilter filterWithName:@"CIColorControls"];
681+
[filter1 setValue:ciImage forKey:kCIInputImageKey];
682+
[filter1 setValue:@(-0.5) forKey:kCIInputBrightnessKey];
683+
ciImage = filter1.outputImage;
684+
// (1 - S)
685+
CIFilter *filter2 = [CIFilter filterWithName:@"CIColorControls"];
686+
[filter2 setValue:colorImage forKey:kCIInputImageKey];
687+
[filter2 setValue:@(-0.5) forKey:kCIInputBrightnessKey];
688+
colorImage = filter2.outputImage;
689+
// +
690+
CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"];
691+
[filter setValue:colorImage forKey:kCIInputImageKey];
692+
[filter setValue:ciImage forKey:kCIInputBackgroundImageKey];
693+
ciImage = filter.outputImage;
694+
// MAX
695+
ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil];
696+
} else {
697+
SD_LOG("UIImage+Transform error: Unsupported blend mode: %d", blendMode);
698+
ciImage = nil;
699+
}
700+
}
701+
702+
if (ciImage) {
555703
#if SD_UIKIT
556704
UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation];
557705
#else
558706
UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp];
559707
#endif
560708
return image;
709+
}
561710
}
562711
#endif
563712

564713
CGSize size = self.size;
565714
CGRect rect = { CGPointZero, size };
566715
CGFloat scale = self.scale;
567716

568-
// blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing
569-
CGBlendMode blendMode = kCGBlendModeSourceAtop;
570-
571717
SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init];
572718
format.scale = scale;
573719
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];

Tests/Tests/SDImageTransformerTests.m

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,35 @@ - (void)test06UIImageTransformTintWithImage:(UIImage *)testImage {
242242
// Check rounded corner operation not inversion the image
243243
UIColor *topCenterColor = [tintedImage sd_colorAtPoint:CGPointMake(150, 20)];
244244
expect([topCenterColor.sd_hexString isEqualToString:[UIColor blackColor].sd_hexString]).beTruthy();
245+
246+
UIImage *tintedSourceInImage = [testImage sd_tintedImageWithColor:tintColor blendMode:kCGBlendModeSourceIn];
247+
topCenterColor = [tintedSourceInImage sd_colorAtPoint:CGPointMake(150, 20)];
248+
#if SD_UIKIT
249+
// Test UIKit's tint color behavior
250+
if (@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)) {
251+
UIImage *tintedSystemImage = [testImage imageWithTintColor:tintColor renderingMode:UIImageRenderingModeAlwaysTemplate];
252+
UIGraphicsImageRendererFormat *format = UIGraphicsImageRendererFormat.preferredFormat;
253+
format.scale = tintedSourceInImage.scale;
254+
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:tintedSystemImage.size format:format];
255+
// Draw template image
256+
tintedSystemImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
257+
[tintedSystemImage drawInRect:CGRectMake(0, 0, tintedSystemImage.size.width, tintedSystemImage.size.height)];
258+
}];
259+
UIColor *testColor1 = [tintedSourceInImage sd_colorAtPoint:CGPointMake(150, 20)];
260+
UIColor *testColor2 = [tintedSystemImage sd_colorAtPoint:CGPointMake(150, 20)];
261+
CGFloat r1, g1, b1, a1;
262+
CGFloat r2, g2, b2, a2;
263+
[testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
264+
[testColor2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
265+
expect(r1).beCloseToWithin(r2, 0.01);
266+
expect(g1).beCloseToWithin(g2, 0.01);
267+
expect(b1).beCloseToWithin(b2, 0.01);
268+
expect(a1).beCloseToWithin(a2, 0.01);
269+
}
270+
#endif
271+
expect([topCenterColor.sd_hexString isEqualToString:tintColor.sd_hexString]).beTruthy();
245272
}
273+
#pragma clang diagnostic pop
246274

247275
- (void)test07UIImageTransformBlurCG {
248276
[self test07UIImageTransformBlurWithImage:self.testImageCG];
@@ -353,7 +381,7 @@ - (void)test09ImagePipelineTransformer {
353381
@"SDImageRoundCornerTransformer(50.000000,18446744073709551615,1.000000,#ff000000)",
354382
@"SDImageFlippingTransformer(1,1)",
355383
@"SDImageCroppingTransformer({0.000000,0.000000,50.000000,50.000000})",
356-
@"SDImageTintTransformer(#00000000)",
384+
@"SDImageTintTransformer(#00000000,18)",
357385
@"SDImageBlurTransformer(5.000000)",
358386
@"SDImageFilterTransformer(CIColorInvert)"
359387
];

0 commit comments

Comments
 (0)