|  |  |  | // | 
					
						
							|  |  |  | //  Copyright (c) 2018 Open Whisper Systems. All rights reserved. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #import "OWSAvatarBuilder.h" | 
					
						
							|  |  |  | #import "OWSContactAvatarBuilder.h" | 
					
						
							|  |  |  | #import "OWSGroupAvatarBuilder.h" | 
					
						
							|  |  |  | #import "TSContactThread.h" | 
					
						
							|  |  |  | #import "TSGroupThread.h" | 
					
						
							|  |  |  | #import "Theme.h" | 
					
						
							|  |  |  | #import "UIColor+OWS.h" | 
					
						
							|  |  |  | #import "UIFont+OWS.h" | 
					
						
							|  |  |  | #import "UIView+OWS.h" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | NS_ASSUME_NONNULL_BEGIN | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const NSUInteger kStandardAvatarSize = 48; | 
					
						
							|  |  |  | const NSUInteger kLargeAvatarSize = 68; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | typedef void (^OWSAvatarDrawBlock)(CGContextRef context); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @implementation OWSAvatarBuilder | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (nullable UIImage *)buildImageForThread:(TSThread *)thread | 
					
						
							|  |  |  |                                  diameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertDebug(thread); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     OWSAvatarBuilder *avatarBuilder; | 
					
						
							|  |  |  |     if ([thread isKindOfClass:[TSContactThread class]]) { | 
					
						
							|  |  |  |         TSContactThread *contactThread = (TSContactThread *)thread; | 
					
						
							|  |  |  |         ConversationColorName colorName = thread.conversationColorName; | 
					
						
							|  |  |  |         avatarBuilder = [[OWSContactAvatarBuilder alloc] initWithSignalId:contactThread.contactIdentifier | 
					
						
							|  |  |  |                                                                 colorName:colorName | 
					
						
							|  |  |  |                                                                  diameter:diameter]; | 
					
						
							|  |  |  |     } else if ([thread isKindOfClass:[TSGroupThread class]]) { | 
					
						
							|  |  |  |         avatarBuilder = [[OWSGroupAvatarBuilder alloc] initWithThread:(TSGroupThread *)thread diameter:diameter]; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |         OWSLogError(@"called with unsupported thread: %@", thread); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return [avatarBuilder build]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (nullable UIImage *)buildRandomAvatarWithDiameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     NSArray<NSString *> *eyes = @[ @":", @"=", @"8", @"B" ]; | 
					
						
							|  |  |  |     NSArray<NSString *> *mouths = @[ @"3", @")", @"(", @"|", @"\\", @"P", @"D", @"o" ]; | 
					
						
							|  |  |  |     // eyebrows are rare | 
					
						
							|  |  |  |     NSArray<NSString *> *eyebrows = @[ @">", @"", @"", @"", @"" ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     NSString *randomEye = eyes[arc4random_uniform((uint32_t)eyes.count)]; | 
					
						
							|  |  |  |     NSString *randomMouth = mouths[arc4random_uniform((uint32_t)mouths.count)]; | 
					
						
							|  |  |  |     NSString *randomEyebrow = eyebrows[arc4random_uniform((uint32_t)eyebrows.count)]; | 
					
						
							|  |  |  |     NSString *face = [NSString stringWithFormat:@"%@%@%@", randomEyebrow, randomEye, randomMouth]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     UIColor *backgroundColor = [UIColor colorWithRGBHex:0xaca6633]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return [self avatarImageWithDiameter:diameter | 
					
						
							|  |  |  |                          backgroundColor:backgroundColor | 
					
						
							|  |  |  |                                drawBlock:^(CGContextRef context) { | 
					
						
							|  |  |  |                                    CGContextTranslateCTM(context, diameter / 2, diameter / 2); | 
					
						
							|  |  |  |                                    CGContextRotateCTM(context, (CGFloat)M_PI_2); | 
					
						
							|  |  |  |                                    CGContextTranslateCTM(context, -diameter / 2, -diameter / 2); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                                    [self drawInitialsInAvatar:face | 
					
						
							|  |  |  |                                                     textColor:self.avatarForegroundColor | 
					
						
							|  |  |  |                                                          font:[self avatarTextFontForDiameter:diameter] | 
					
						
							|  |  |  |                                                      diameter:diameter]; | 
					
						
							|  |  |  |                                }]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (UIColor *)avatarForegroundColor | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return (Theme.isDarkThemeEnabled ? UIColor.ows_gray05Color : UIColor.ows_whiteColor); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (UIFont *)avatarTextFontForDiameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     // Adapt the font size to reflect the diameter. | 
					
						
							|  |  |  |     CGFloat fontSize = 20.f * diameter / kStandardAvatarSize; | 
					
						
							|  |  |  |     return [UIFont ows_mediumFontWithSize:fontSize]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (nullable UIImage *)avatarImageWithInitials:(NSString *)initials | 
					
						
							|  |  |  |                               backgroundColor:(UIColor *)backgroundColor | 
					
						
							|  |  |  |                                      diameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return [self avatarImageWithInitials:initials | 
					
						
							|  |  |  |                          backgroundColor:backgroundColor | 
					
						
							|  |  |  |                                textColor:self.avatarForegroundColor | 
					
						
							|  |  |  |                                     font:[self avatarTextFontForDiameter:diameter] | 
					
						
							|  |  |  |                                 diameter:diameter]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (nullable UIImage *)avatarImageWithInitials:(NSString *)initials | 
					
						
							|  |  |  |                               backgroundColor:(UIColor *)backgroundColor | 
					
						
							|  |  |  |                                     textColor:(UIColor *)textColor | 
					
						
							|  |  |  |                                          font:(UIFont *)font | 
					
						
							|  |  |  |                                      diameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertDebug(initials); | 
					
						
							|  |  |  |     OWSAssertDebug(textColor); | 
					
						
							|  |  |  |     OWSAssertDebug(font); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return [self avatarImageWithDiameter:diameter | 
					
						
							|  |  |  |                          backgroundColor:backgroundColor | 
					
						
							|  |  |  |                                drawBlock:^(CGContextRef context) { | 
					
						
							|  |  |  |                                    [self drawInitialsInAvatar:initials textColor:textColor font:font diameter:diameter]; | 
					
						
							|  |  |  |                                }]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (nullable UIImage *)avatarImageWithIcon:(UIImage *)icon | 
					
						
							|  |  |  |                                  iconSize:(CGSize)iconSize | 
					
						
							|  |  |  |                           backgroundColor:(UIColor *)backgroundColor | 
					
						
							|  |  |  |                                  diameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     return [self avatarImageWithIcon:icon | 
					
						
							|  |  |  |                             iconSize:iconSize | 
					
						
							|  |  |  |                            iconColor:self.avatarForegroundColor | 
					
						
							|  |  |  |                      backgroundColor:backgroundColor | 
					
						
							|  |  |  |                             diameter:diameter]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (nullable UIImage *)avatarImageWithIcon:(UIImage *)icon | 
					
						
							|  |  |  |                                  iconSize:(CGSize)iconSize | 
					
						
							|  |  |  |                                 iconColor:(UIColor *)iconColor | 
					
						
							|  |  |  |                           backgroundColor:(UIColor *)backgroundColor | 
					
						
							|  |  |  |                                  diameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertDebug(icon); | 
					
						
							|  |  |  |     OWSAssertDebug(iconColor); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return [self avatarImageWithDiameter:diameter | 
					
						
							|  |  |  |                          backgroundColor:backgroundColor | 
					
						
							|  |  |  |                                drawBlock:^(CGContextRef context) { | 
					
						
							|  |  |  |                                    [self drawIconInAvatar:icon | 
					
						
							|  |  |  |                                                  iconSize:iconSize | 
					
						
							|  |  |  |                                                 iconColor:iconColor | 
					
						
							|  |  |  |                                                  diameter:diameter | 
					
						
							|  |  |  |                                                   context:context]; | 
					
						
							|  |  |  |                                }]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (nullable UIImage *)avatarImageWithDiameter:(NSUInteger)diameter | 
					
						
							|  |  |  |                               backgroundColor:(UIColor *)backgroundColor | 
					
						
							|  |  |  |                                     drawBlock:(OWSAvatarDrawBlock)drawBlock | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertDebug(drawBlock); | 
					
						
							|  |  |  |     OWSAssertDebug(backgroundColor); | 
					
						
							|  |  |  |     OWSAssertDebug(diameter > 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     CGRect frame = CGRectMake(0.0f, 0.0f, diameter, diameter); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     UIGraphicsBeginImageContextWithOptions(frame.size, NO, [UIScreen mainScreen].scale); | 
					
						
							|  |  |  |     CGContextRef _Nullable context = UIGraphicsGetCurrentContext(); | 
					
						
							|  |  |  |     if (!context) { | 
					
						
							|  |  |  |         return nil; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     CGContextSetFillColorWithColor(context, backgroundColor.CGColor); | 
					
						
							|  |  |  |     CGContextFillRect(context, frame); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Gradient | 
					
						
							|  |  |  |     CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB(); | 
					
						
							|  |  |  |     CGFloat gradientLocations[] = { 0.0, 1.0 }; | 
					
						
							|  |  |  |     CGGradientRef _Nullable gradient = CGGradientCreateWithColors(colorspace, | 
					
						
							|  |  |  |         (__bridge CFArrayRef) @[ | 
					
						
							|  |  |  |             (id)[UIColor colorWithWhite:0.f alpha:0.f].CGColor, | 
					
						
							|  |  |  |             (id)[UIColor colorWithWhite:0.f alpha:0.15f].CGColor, | 
					
						
							|  |  |  |         ], | 
					
						
							|  |  |  |         gradientLocations); | 
					
						
							|  |  |  |     if (!gradient) { | 
					
						
							|  |  |  |         return nil; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     CGPoint startPoint = CGPointMake(diameter * 0.5f, 0); | 
					
						
							|  |  |  |     CGPoint endPoint = CGPointMake(diameter * 0.5f, diameter); | 
					
						
							|  |  |  |     CGContextDrawLinearGradient(context, | 
					
						
							|  |  |  |         gradient, | 
					
						
							|  |  |  |         startPoint, | 
					
						
							|  |  |  |         endPoint, | 
					
						
							|  |  |  |         kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); | 
					
						
							|  |  |  |     CFRelease(gradient); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     CGContextSaveGState(context); | 
					
						
							|  |  |  |     drawBlock(context); | 
					
						
							|  |  |  |     CGContextRestoreGState(context); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     UIImage *_Nullable image = UIGraphicsGetImageFromCurrentImageContext(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     UIGraphicsEndImageContext(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return image; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (void)drawInitialsInAvatar:(NSString *)initials | 
					
						
							|  |  |  |                    textColor:(UIColor *)textColor | 
					
						
							|  |  |  |                         font:(UIFont *)font | 
					
						
							|  |  |  |                     diameter:(NSUInteger)diameter | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertDebug(initials); | 
					
						
							|  |  |  |     OWSAssertDebug(textColor); | 
					
						
							|  |  |  |     OWSAssertDebug(font); | 
					
						
							|  |  |  |     OWSAssertDebug(diameter > 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     CGRect frame = CGRectMake(0.0f, 0.0f, diameter, diameter); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     NSDictionary *textAttributes = @{ | 
					
						
							|  |  |  |         NSFontAttributeName : font, | 
					
						
							|  |  |  |         NSForegroundColorAttributeName : textColor, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     CGSize textSize = | 
					
						
							|  |  |  |         [initials boundingRectWithSize:frame.size | 
					
						
							|  |  |  |                                options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) | 
					
						
							|  |  |  |                             attributes:textAttributes | 
					
						
							|  |  |  |                                context:nil] | 
					
						
							|  |  |  |             .size; | 
					
						
							|  |  |  |     // Ensure that the text fits within the avatar bounds, with a margin. | 
					
						
							|  |  |  |     if (textSize.width > 0 && textSize.height > 0) { | 
					
						
							|  |  |  |         CGFloat textDiameter = (CGFloat)sqrt(textSize.width * textSize.width + textSize.height * textSize.height); | 
					
						
							|  |  |  |         // Leave a 10% margin. | 
					
						
							|  |  |  |         CGFloat maxTextDiameter = diameter * 0.9f; | 
					
						
							|  |  |  |         if (textDiameter > maxTextDiameter) { | 
					
						
							|  |  |  |             font = [font fontWithSize:font.pointSize * maxTextDiameter / textDiameter]; | 
					
						
							|  |  |  |             textAttributes = @{ | 
					
						
							|  |  |  |                 NSFontAttributeName : font, | 
					
						
							|  |  |  |                 NSForegroundColorAttributeName : textColor, | 
					
						
							|  |  |  |             }; | 
					
						
							|  |  |  |             textSize = | 
					
						
							|  |  |  |                 [initials boundingRectWithSize:frame.size | 
					
						
							|  |  |  |                                        options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) | 
					
						
							|  |  |  |                                     attributes:textAttributes | 
					
						
							|  |  |  |                                        context:nil] | 
					
						
							|  |  |  |                     .size; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |         OWSFailDebug(@"Text has invalid bounds."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     CGPoint drawPoint = CGPointMake((diameter - textSize.width) * 0.5f, (diameter - textSize.height) * 0.5f); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     [initials drawAtPoint:drawPoint withAttributes:textAttributes]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | + (void)drawIconInAvatar:(UIImage *)icon | 
					
						
							|  |  |  |                 iconSize:(CGSize)iconSize | 
					
						
							|  |  |  |                iconColor:(UIColor *)iconColor | 
					
						
							|  |  |  |                 diameter:(NSUInteger)diameter | 
					
						
							|  |  |  |                  context:(CGContextRef)context | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAssertDebug(icon); | 
					
						
							|  |  |  |     OWSAssertDebug(iconColor); | 
					
						
							|  |  |  |     OWSAssertDebug(diameter > 0); | 
					
						
							|  |  |  |     OWSAssertDebug(context); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // UIKit uses an ULO coordinate system (upper-left-origin). | 
					
						
							|  |  |  |     // Core Graphics uses an LLO coordinate system (lower-left-origin). | 
					
						
							|  |  |  |     CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, diameter); | 
					
						
							|  |  |  |     CGContextConcatCTM(context, flipVertical); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     CGRect imageRect = CGRectZero; | 
					
						
							|  |  |  |     imageRect.size = CGSizeMake(diameter, diameter); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // The programmatic equivalent of UIImageRenderingModeAlwaysTemplate/tintColor. | 
					
						
							|  |  |  |     CGContextSetBlendMode(context, kCGBlendModeNormal); | 
					
						
							|  |  |  |     CGRect maskRect = CGRectZero; | 
					
						
							|  |  |  |     maskRect.origin = CGPointScale( | 
					
						
							|  |  |  |         CGPointSubtract(CGPointMake(diameter, diameter), CGPointMake(iconSize.width, iconSize.height)), 0.5f); | 
					
						
							|  |  |  |     maskRect.size = iconSize; | 
					
						
							|  |  |  |     CGContextClipToMask(context, maskRect, icon.CGImage); | 
					
						
							|  |  |  |     CGContextSetFillColor(context, CGColorGetComponents(iconColor.CGColor)); | 
					
						
							|  |  |  |     CGContextFillRect(context, imageRect); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (nullable UIImage *)build | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     UIImage *_Nullable savedImage = [self buildSavedImage]; | 
					
						
							|  |  |  |     if (savedImage) { | 
					
						
							|  |  |  |         return savedImage; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |         return [self buildDefaultImage]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (nullable UIImage *)buildSavedImage | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAbstractMethod(); | 
					
						
							|  |  |  |     return nil; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - (nullable UIImage *)buildDefaultImage | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     OWSAbstractMethod(); | 
					
						
							|  |  |  |     return nil; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | NS_ASSUME_NONNULL_END |