blob: fb8dcafd768ddcc8baed772e464799890b057a8e [file] [log] [blame]
/*
Copyright 2016-present Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MDFTextAccessibility.h"
#import "MDFColorCalculations.h"
#import "NSArray+MDFUtils.h"
static const CGFloat kMinContrastRatioNormalText = 4.5f;
static const CGFloat kMinContrastRatioLargeText = 3.0f;
static const CGFloat kMinContrastRatioNormalTextEnhanced = 7.0f;
static const CGFloat kMinContrastRatioLargeTextEnhanced = 4.5f;
@implementation MDFTextAccessibility
+ (nonnull UIColor *)textColorOnBackgroundColor:(nonnull UIColor *)backgroundColor
targetTextAlpha:(CGFloat)targetTextAlpha
font:(nullable UIFont *)font {
MDFTextAccessibilityOptions options = 0;
if ([self isLargeForContrastRatios:font]) {
options |= MDFTextAccessibilityOptionsLargeFont;
}
return [self textColorOnBackgroundColor:backgroundColor
targetTextAlpha:targetTextAlpha
options:options];
}
+ (nullable UIColor *)textColorOnBackgroundColor:(nonnull UIColor *)backgroundColor
targetTextAlpha:(CGFloat)targetTextAlpha
options:(MDFTextAccessibilityOptions)options {
NSArray *colors = @[
[UIColor colorWithWhite:1 alpha:targetTextAlpha], [UIColor colorWithWhite:0
alpha:targetTextAlpha]
];
UIColor *textColor = [self textColorFromChoices:colors
onBackgroundColor:backgroundColor
options:options];
return textColor;
}
+ (nullable UIColor *)textColorFromChoices:(nonnull NSArray<UIColor *> *)choices
onBackgroundColor:(nonnull UIColor *)backgroundColor
options:(MDFTextAccessibilityOptions)options {
[choices enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSAssert([obj isKindOfClass:[UIColor class]], @"Choices must be UIColors.");
}];
// Sort by luminance if requested.
if ((options & MDFTextAccessibilityOptionsPreferLighter) ||
(options & MDFTextAccessibilityOptionsPreferDarker)) {
NSArray *luminances = [choices mdf_arrayByMappingObjects:^id(id object) {
return @([self luminanceOfColor:object]);
}];
BOOL inverse = (options & MDFTextAccessibilityOptionsPreferDarker) ? YES : NO;
choices = [luminances mdf_sortArray:choices
usingComparator:^NSComparisonResult(id obj1, id obj2) {
float first = inverse ? [obj1 floatValue] : [obj2 floatValue];
float second = inverse ? [obj2 floatValue] : [obj1 floatValue];
if (first < second) {
return NSOrderedAscending;
} else if (first > second) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
}];
}
// Search the array for a color that can be used, adjusting alpha values upwards if requested.
// The first acceptable color (adjusted or not) is returned.
BOOL adjustAlphas = (options & MDFTextAccessibilityOptionsPreserveAlpha) ? NO : YES;
for (UIColor *choice in choices) {
if ([self textColor:choice passesOnBackgroundColor:backgroundColor options:options]) {
return choice;
}
if (!adjustAlphas) {
continue;
}
CGFloat alpha = CGColorGetAlpha(choice.CGColor);
CGFloat minAlpha = [self minAlphaOfTextColor:choice
onBackgroundColor:backgroundColor
options:options];
if (minAlpha > 0) {
if (alpha > minAlpha) {
NSAssert(NO,
@"Logic error: computed an acceptable minimum alpha (%f) that is *less* than the "
@"unacceptable current alpha (%f).",
minAlpha, alpha);
continue;
}
return [choice colorWithAlphaComponent:minAlpha];
}
}
return nil;
}
+ (CGFloat)minAlphaOfTextColor:(nonnull UIColor *)textColor
onBackgroundColor:(nonnull UIColor *)backgroundColor
options:(MDFTextAccessibilityOptions)options {
CGFloat minContrastRatio = [self minContrastRatioForOptions:options];
return MDFMinAlphaOfColorOnBackgroundColor(textColor, backgroundColor, minContrastRatio);
}
+ (CGFloat)contrastRatioForTextColor:(UIColor *)textColor
onBackgroundColor:(UIColor *)backgroundColor {
CGFloat colorComponents[4];
CGFloat backgroundColorComponents[4];
MDFCopyRGBAComponents(textColor.CGColor, colorComponents);
MDFCopyRGBAComponents(backgroundColor.CGColor, backgroundColorComponents);
NSAssert(backgroundColorComponents[3] == 1,
@"Background color %@ must be opaque for a valid contrast ratio calculation.",
backgroundColor);
backgroundColorComponents[3] = 1;
return MDFContrastRatioOfRGBAComponents(colorComponents, backgroundColorComponents);
}
+ (BOOL)textColor:(nonnull UIColor *)textColor
passesOnBackgroundColor:(nonnull UIColor *)backgroundColor
options:(MDFTextAccessibilityOptions)options {
CGFloat minContrastRatio = [self minContrastRatioForOptions:options];
CGFloat ratio = [self contrastRatioForTextColor:textColor onBackgroundColor:backgroundColor];
return ratio >= minContrastRatio ? YES : NO;
}
+ (BOOL)isLargeForContrastRatios:(nullable UIFont *)font {
UIFontDescriptor *fontDescriptor = font.fontDescriptor;
BOOL isBold =
(fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) == UIFontDescriptorTraitBold;
return font.pointSize >= 18 || (isBold && font.pointSize >= 14);
}
#pragma mark - Private methods
+ (CGFloat)luminanceOfColor:(UIColor *)color {
CGFloat colorComponents[4];
MDFCopyRGBAComponents(color.CGColor, colorComponents);
return MDFRelativeLuminanceOfRGBComponents(colorComponents);
}
+ (CGFloat)minContrastRatioForOptions:(MDFTextAccessibilityOptions)options {
BOOL isLarge =
(options & MDFTextAccessibilityOptionsLargeFont) == MDFTextAccessibilityOptionsLargeFont;
BOOL isEnhanced = (options & MDFTextAccessibilityOptionsEnhancedContrast) ==
MDFTextAccessibilityOptionsEnhancedContrast;
if (isEnhanced) {
return isLarge ? kMinContrastRatioLargeTextEnhanced : kMinContrastRatioNormalTextEnhanced;
} else {
return isLarge ? kMinContrastRatioLargeText : kMinContrastRatioNormalText;
}
}
@end