diff --git a/Classes/GTXChecksCollection.h b/Classes/GTXChecksCollection.h index 3a40141..d957da5 100644 --- a/Classes/GTXChecksCollection.h +++ b/Classes/GTXChecksCollection.h @@ -91,9 +91,21 @@ typedef NS_ENUM(NSUInteger, GTXVersion) { */ + (id)checkForSufficientContrastRatio; +/** + * @return a check that verifies that contrast of all UILabel elements using the most contrasting + * pixel is at least 3.0. + */ ++ (id)checkForSufficientContrastRatioWithMostContrastingPixel; + /** * @return a check that verifies that contrast of all UITextView elements is at least 3.0. */ + (id)checkForSufficientTextViewContrastRatio; +/** + * @return a check that verifies that contrast of all UITextView elements using the most contrasting + * pixel is at least 3.0. + */ ++ (id)checkForSufficientTextViewContrastRatioWithMostContrastingPixel; + @end diff --git a/Classes/GTXChecksCollection.m b/Classes/GTXChecksCollection.m index 1d82e88..d0c4158 100644 --- a/Classes/GTXChecksCollection.m +++ b/Classes/GTXChecksCollection.m @@ -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 @@ -310,15 +312,25 @@ @implementation GTXChecksCollection } + (id)checkForSufficientContrastRatio { + return [self checkForSufficientContrastRatioWithMostContrastingPixel:NO]; +} + ++ (id)checkForSufficientContrastRatioWithMostContrastingPixel { + return [self checkForSufficientContrastRatioWithMostContrastingPixel:YES]; +} + ++ (id)checkForSufficientContrastRatioWithMostContrastingPixel:(BOOL)useMostContrastingPixel { + NSString *checkName = useMostContrastingPixel ? kGTXCheckNameLabelMinimumContrastRatioWithMostContrastingPixel : kGTXCheckNameLabelMinimumContrastRatio; id 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) { @@ -329,7 +341,7 @@ @implementation GTXChecksCollection (float)kGTXMinContrastRatioForAccessibleText, (float)ratio]; [NSError gtx_logOrSetGTXCheckFailedError:errorOrNil element:element - name:kGTXCheckNameLabelMinimumContrastRatio + name:checkName description:description]; } return hasSufficientContrast; @@ -338,15 +350,25 @@ @implementation GTXChecksCollection } + (id)checkForSufficientTextViewContrastRatio { + return [self checkForSufficientTextViewContrastRatioWithMostContrastingPixel:NO]; +} + ++ (id)checkForSufficientTextViewContrastRatioWithMostContrastingPixel { + return [self checkForSufficientTextViewContrastRatioWithMostContrastingPixel:YES]; +} + ++ (id)checkForSufficientTextViewContrastRatioWithMostContrastingPixel:(BOOL)useMostContrastingPixel { + NSString *checkName = useMostContrastingPixel ? kGTXCheckNameTextViewMinimumContrastRatioWithMostContrastingPixel : kGTXCheckNameTextViewMinimumContrastRatio; id 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) { @@ -357,7 +379,7 @@ @implementation GTXChecksCollection (float)kGTXMinContrastRatioForAccessibleText, (float)ratio]; [NSError gtx_logOrSetGTXCheckFailedError:errorOrNil element:element - name:kGTXCheckNameTextViewMinimumContrastRatio + name:checkName description:description]; } return hasSufficientContrast; diff --git a/Classes/GTXImageAndColorUtils.h b/Classes/GTXImageAndColorUtils.h index cefd3b4..ce8323a 100644 --- a/Classes/GTXImageAndColorUtils.h +++ b/Classes/GTXImageAndColorUtils.h @@ -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. @@ -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 diff --git a/Classes/GTXImageAndColorUtils.m b/Classes/GTXImageAndColorUtils.m index 96856bf..be5a964 100644 --- a/Classes/GTXImageAndColorUtils.m +++ b/Classes/GTXImageAndColorUtils.m @@ -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); @@ -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); @@ -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 @@ -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; @@ -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; @@ -173,9 +190,17 @@ + (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) { @@ -183,8 +208,24 @@ + (CGFloat)gtx_contrastRatioWithTextElementImage:(UIImage *)original (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]; } /**