Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The color contrast check should use the most contrasting pixel instead of the average #11

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Classes/GTXChecksCollection.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,21 @@ typedef NS_ENUM(NSUInteger, GTXVersion) {
*/
+ (id<GTXChecking>)checkForSufficientContrastRatio;

/**
* @return a check that verifies that contrast of all UILabel elements using the most contrasting
* pixel is at least 3.0.
*/
+ (id<GTXChecking>)checkForSufficientContrastRatioWithMostContrastingPixel;

/**
* @return a check that verifies that contrast of all UITextView elements is at least 3.0.
*/
+ (id<GTXChecking>)checkForSufficientTextViewContrastRatio;

/**
* @return a check that verifies that contrast of all UITextView elements using the most contrasting
* pixel is at least 3.0.
*/
+ (id<GTXChecking>)checkForSufficientTextViewContrastRatioWithMostContrastingPixel;

@end
36 changes: 29 additions & 7 deletions Classes/GTXChecksCollection.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
@"Accessibility Traits Don't Conflict";
NSString *const kGTXCheckNameMinimumTappableArea = @"Element has Minimum Tappable Area";
NSString *const kGTXCheckNameLabelMinimumContrastRatio = @"Label has Minimum Contrast Ratio";
NSString *const kGTXCheckNameLabelMinimumContrastRatioWithMostContrastingPixel = @"Label has Minimum Contrast Ratio Comparing The Most Contrasting Pixel";
NSString *const kGTXCheckNameTextViewMinimumContrastRatio = @"TextView has Minimum Contrast Ratio";
NSString *const kGTXCheckNameTextViewMinimumContrastRatioWithMostContrastingPixel = @"TextView has Minimum Contrast Ratio Comparing The Most Contrasting Pixel";


#pragma mark - Globals
Expand Down Expand Up @@ -310,15 +312,25 @@ @implementation GTXChecksCollection
}

+ (id<GTXChecking>)checkForSufficientContrastRatio {
return [self checkForSufficientContrastRatioWithMostContrastingPixel:NO];
}

+ (id<GTXChecking>)checkForSufficientContrastRatioWithMostContrastingPixel {
return [self checkForSufficientContrastRatioWithMostContrastingPixel:YES];
}

+ (id<GTXChecking>)checkForSufficientContrastRatioWithMostContrastingPixel:(BOOL)useMostContrastingPixel {
NSString *checkName = useMostContrastingPixel ? kGTXCheckNameLabelMinimumContrastRatioWithMostContrastingPixel : kGTXCheckNameLabelMinimumContrastRatio;
id<GTXChecking> check =
[GTXCheckBlock GTXCheckWithName:kGTXCheckNameLabelMinimumContrastRatio
block:^BOOL(id element, GTXErrorRefType errorOrNil) {
[GTXCheckBlock GTXCheckWithName:checkName
block:^BOOL(id element, GTXErrorRefType errorOrNil) {
if (![element isKindOfClass:[UILabel class]]) {
return YES;
} else if ([[(UILabel *)element text] length] == 0) {
return YES;
}
CGFloat ratio = [GTXImageAndColorUtils contrastRatioOfUILabel:element];
CGFloat ratio = [GTXImageAndColorUtils contrastRatioOfUILabel:element
useMostContrastingPixel:useMostContrastingPixel];
BOOL hasSufficientContrast =
(ratio >= kGTXMinContrastRatioForAccessibleText - kGTXContrastRatioAccuracy);
if (!hasSufficientContrast) {
Expand All @@ -329,7 +341,7 @@ @implementation GTXChecksCollection
(float)kGTXMinContrastRatioForAccessibleText, (float)ratio];
[NSError gtx_logOrSetGTXCheckFailedError:errorOrNil
element:element
name:kGTXCheckNameLabelMinimumContrastRatio
name:checkName
description:description];
}
return hasSufficientContrast;
Expand All @@ -338,15 +350,25 @@ @implementation GTXChecksCollection
}

+ (id<GTXChecking>)checkForSufficientTextViewContrastRatio {
return [self checkForSufficientTextViewContrastRatioWithMostContrastingPixel:NO];
}

+ (id<GTXChecking>)checkForSufficientTextViewContrastRatioWithMostContrastingPixel {
return [self checkForSufficientTextViewContrastRatioWithMostContrastingPixel:YES];
}

+ (id<GTXChecking>)checkForSufficientTextViewContrastRatioWithMostContrastingPixel:(BOOL)useMostContrastingPixel {
NSString *checkName = useMostContrastingPixel ? kGTXCheckNameTextViewMinimumContrastRatioWithMostContrastingPixel : kGTXCheckNameTextViewMinimumContrastRatio;
id<GTXChecking> check =
[GTXCheckBlock GTXCheckWithName:kGTXCheckNameTextViewMinimumContrastRatio
[GTXCheckBlock GTXCheckWithName:checkName
block:^BOOL(id element, GTXErrorRefType errorOrNil) {
if (![element isKindOfClass:[UITextView class]]) {
return YES;
} else if ([[(UITextView *)element text] length] == 0) {
return YES;
}
CGFloat ratio = [GTXImageAndColorUtils contrastRatioOfUILabel:element];
CGFloat ratio = [GTXImageAndColorUtils contrastRatioOfUITextView:element
useMostContrastingPixel:useMostContrastingPixel];
BOOL hasSufficientContrast =
(ratio >= kGTXMinContrastRatioForAccessibleText - kGTXContrastRatioAccuracy);
if (!hasSufficientContrast) {
Expand All @@ -357,7 +379,7 @@ @implementation GTXChecksCollection
(float)kGTXMinContrastRatioForAccessibleText, (float)ratio];
[NSError gtx_logOrSetGTXCheckFailedError:errorOrNil
element:element
name:kGTXCheckNameTextViewMinimumContrastRatio
name:checkName
description:description];
}
return hasSufficientContrast;
Expand Down
6 changes: 4 additions & 2 deletions Classes/GTXImageAndColorUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ extern const CGFloat kGTXContrastRatioAccuracy;
* of which the label must already be in a window before this method can be used.
*
* @param label The label whose contrast ratio is to be computed.
* @param useMostContrastingPixel Determine if this check is using the most contrasting pixel
*
* @return The contrast ratio (proportional to 1.0) of the label.
*/
+ (CGFloat)contrastRatioOfUILabel:(UILabel *)label;
+ (CGFloat)contrastRatioOfUILabel:(UILabel *)label useMostContrastingPixel:(BOOL)useMostContrastingPixel;

/**
* Computes contrast ratio of the given text view to its background.
Expand All @@ -68,9 +69,10 @@ extern const CGFloat kGTXContrastRatioAccuracy;
* of which the text view must already be in a window before this method can be used.
*
* @param view The text view whose contrast ratio is to be computed.
* @param useMostContrastingPixel Determine if this check is using the most contrasting pixel
*
* @return The contrast ratio (proportional to 1.0) of the text view.
*/
+ (CGFloat)contrastRatioOfUITextView:(UITextView *)view;
+ (CGFloat)contrastRatioOfUITextView:(UITextView *)view useMostContrastingPixel:(BOOL)useMostContrastingPixel;

@end
55 changes: 48 additions & 7 deletions Classes/GTXImageAndColorUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ + (CGFloat)contrastRatioWithLuminaceOfFirstColor:(CGFloat)color1Luminance
return (brighterColorLuminance + 0.05f) / (darkerColorLuminance + 0.05f);
}

+ (CGFloat)contrastRatioOfUILabel:(UILabel *)label {

+ (CGFloat)contrastRatioOfUILabel:(UILabel *)label useMostContrastingPixel:(BOOL)useMostContrastingPixel {
NSAssert(label.window, @"Label %@ must be part of view hierarchy to use this method, see API"
@" docs for more info.", label);

Expand All @@ -68,10 +69,12 @@ + (CGFloat)contrastRatioOfUILabel:(UILabel *)label {
UIImage *after = [self gtx_takeSnapshot:label];
label.textColor = prevColor;

return [self gtx_contrastRatioWithTextElementImage:before textElementColorShiftedImage:after];
return [self gtx_contrastRatioWithTextElementImage:before
textElementColorShiftedImage:after
useMostContrastingPixel:useMostContrastingPixel];
}

+ (CGFloat)contrastRatioOfUITextView:(UITextView *)view {
+ (CGFloat)contrastRatioOfUITextView:(UITextView *)view useMostContrastingPixel:(BOOL)useMostContrastingPixel {
NSAssert(view.window, @"View %@ must be part of view hierarchy to use this method, see API"
@" docs for more info.", view);

Expand All @@ -84,7 +87,9 @@ + (CGFloat)contrastRatioOfUITextView:(UITextView *)view {
UIImage *after = [self gtx_takeSnapshot:view];
view.textColor = prevColor;

return [self gtx_contrastRatioWithTextElementImage:before textElementColorShiftedImage:after];
return [self gtx_contrastRatioWithTextElementImage:before
textElementColorShiftedImage:after
useMostContrastingPixel:useMostContrastingPixel];
}

#pragma mark - Utils
Expand Down Expand Up @@ -117,14 +122,18 @@ + (UIImage *)gtx_takeSnapshot:(UIView *)element {
*
* @param original The original image of the text element.
* @param colorShifted Image of the text element with color of the text shifted (changed).
* @param useMostContrastingPixel Determine if this check is using the most contrasting pixel
*
* @return The contrast ratio (proportional to 1.0) of the label.
*/
+ (CGFloat)gtx_contrastRatioWithTextElementImage:(UIImage *)original
textElementColorShiftedImage:(UIImage *)colorShifted {
textElementColorShiftedImage:(UIImage *)colorShifted
useMostContrastingPixel:(BOOL)useMostContrastingPixel {
// Luminace of image is computed using Reinhard’s method:
// Luminace of image = Geometric Mean of luminance of individual pixels.
CGFloat textLogAverage = 0;
CGFloat textLogDark = CGFLOAT_MAX;
CGFloat textLogLite = CGFLOAT_MIN;
NSInteger textPixelCount = 0;
CGFloat backgroundLogAverage = 0;
NSInteger backgroundPixelCount = 0;
Expand Down Expand Up @@ -163,6 +172,14 @@ + (CGFloat)gtx_contrastRatioWithTextElementImage:(UIImage *)original
// This pixel has changed from before: it is part of the text.
textLogAverage += logLuminance;
textPixelCount += 1;
if (useMostContrastingPixel) {
if (textLogDark > logLuminance) {
textLogDark = logLuminance;
}
if (textLogLite < logLuminance) {
textLogLite = logLuminance;
}
}
} else {
// This pixel has *not* changed from before: it is part of the text background.
backgroundLogAverage += logLuminance;
Expand All @@ -173,18 +190,42 @@ + (CGFloat)gtx_contrastRatioWithTextElementImage:(UIImage *)original

// Compute the geometric mean and scale the result back.
CGFloat textLuminance = 1.0f;
CGFloat textLuminanceDark = 1.0f;
CGFloat textLuminanceLite = 1.0f;
if (textPixelCount != 0) {
textLuminance =
(CGFloat)(exp(textLogAverage / textPixelCount) - luminanceOffset) / luminanceScale;
if (useMostContrastingPixel) {
textLuminanceDark =
(CGFloat)(exp(textLogDark) - luminanceOffset) / luminanceScale;
textLuminanceLite =
(CGFloat)(exp(textLogLite) - luminanceOffset) / luminanceScale;
}
}
CGFloat backgroundLuminance = 1.0;
if (backgroundPixelCount != 0) {
backgroundLuminance =
(CGFloat)(exp(backgroundLogAverage / backgroundPixelCount) - luminanceOffset) /
luminanceScale;
}
return [self contrastRatioWithLuminaceOfFirstColor:textLuminance
andLuminanceOfSecondColor:backgroundLuminance];
CGFloat averageLuminanceRatio = [self contrastRatioWithLuminaceOfFirstColor:textLuminance
andLuminanceOfSecondColor:backgroundLuminance];
if (!useMostContrastingPixel) {
return averageLuminanceRatio;
}

CGFloat darkLuminanceRatio = [self contrastRatioWithLuminaceOfFirstColor:textLuminanceDark
andLuminanceOfSecondColor:backgroundLuminance];
CGFloat liteLuminanceRatio = [self contrastRatioWithLuminaceOfFirstColor:textLuminanceLite
andLuminanceOfSecondColor:backgroundLuminance];
return MAX(MAX(averageLuminanceRatio, darkLuminanceRatio), liteLuminanceRatio);
}

+ (CGFloat)gtx_contrastRatioWithTextElementImage:(UIImage *)original
textElementColorShiftedImage:(UIImage *)colorShifted {
return [self gtx_contrastRatioWithTextElementImage:original
textElementColorShiftedImage:colorShifted
useMostContrastingPixel:NO];
}

/**
Expand Down