// // 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; avatarBuilder = [[OWSContactAvatarBuilder alloc] initWithSignalId:contactThread.contactIdentifier colorName:contactThread.conversationColorName 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 *eyes = @[ @":", @"=", @"8", @"B" ]; NSArray *mouths = @[ @"3", @")", @"(", @"|", @"\\", @"P", @"D", @"o" ]; // eyebrows are rare NSArray *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