1 /* 2 * Copyright (C) 2014 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.android.apps.common.testing.accessibility.framework.utils.contrast; 18 19 import static java.lang.Math.max; 20 import static java.lang.Math.min; 21 22 import com.google.common.collect.Range; 23 24 /** Utilities for dealing with colors and evaluation of their relative contrast. */ 25 public final class ContrastUtils { 26 27 /** 28 * The minimum text size considered large for contrast checking purposes, as taken from the WCAG 29 * standards at http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html 30 */ 31 public static final int WCAG_LARGE_TEXT_MIN_SIZE = 18; 32 33 /** 34 * The minimum text size for bold text to be considered large for contrast checking purposes, as 35 * taken from the WCAG standards at 36 * http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html 37 */ 38 public static final int WCAG_LARGE_BOLD_TEXT_MIN_SIZE = 14; 39 40 /** The color value used to censor secure windows from screen capture */ 41 public static final int COLOR_SECURE_WINDOW_CENSOR = Color.BLACK; 42 43 public static final double CONTRAST_RATIO_WCAG_NORMAL_TEXT = 4.5; 44 45 public static final double CONTRAST_RATIO_WCAG_LARGE_TEXT = 3.0; 46 47 private static final int COLOR_MASK = 0xFF; 48 ContrastUtils()49 private ContrastUtils() { 50 // Not instantiable 51 } 52 53 /** 54 * Calculates the luminance value of a given {@code int} representation of {@link Color}. 55 * 56 * <p>Derived from formula at http://gmazzocato.altervista.org/colorwheel/algo.php 57 * 58 * @param color The {@link Color} to evaluate 59 * @return the luminance value of the given color 60 */ calculateLuminance(int color)61 public static double calculateLuminance(int color) { 62 double r = linearColor(Color.red(color)); 63 double g = linearColor(Color.green(color)); 64 double b = linearColor(Color.blue(color)); 65 return 0.2126d * r + 0.7152d * g + 0.0722d * b; 66 } 67 linearColor(int component)68 private static double linearColor(int component) { 69 double sRGB = component / 255.0d; 70 if (sRGB <= 0.03928d) { 71 return sRGB / 12.92d; 72 } else { 73 return Math.pow(((sRGB + 0.055d) / 1.055d), 2.4d); 74 } 75 } 76 77 /** 78 * Calculates the Delta E of CIE-94 perceived color difference of two color ints. 79 * 80 * <p>Derived from formula at https://www.easyrgb.com/en/math.php and 81 * https://en.wikipedia.org/wiki/Color_difference 82 * 83 * @return the perceived color diffrence Delta E. 84 */ colorDifference(int color1, int color2)85 public static double colorDifference(int color1, int color2) { 86 double[] lab1 = rgb2lab(color1); 87 double[] lab2 = rgb2lab(color2); 88 89 double deltaL = lab1[0] - lab2[0]; 90 double deltaA = lab1[1] - lab2[1]; 91 double deltaB = lab1[2] - lab2[2]; 92 double c1 = Math.hypot(lab1[1], lab1[2]); 93 double c2 = Math.hypot(lab2[1], lab2[2]); 94 double deltaC = c1 - c2; 95 double deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC; 96 deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH); 97 double sc = 1.0 + 0.045d * c1; 98 double sh = 1.0 + 0.015d * c1; 99 double deltaLKlsl = deltaL / 1.0d; 100 double deltaCkcsc = deltaC / sc; 101 double deltaHkhsh = deltaH / sh; 102 return Math.sqrt(deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh); 103 } 104 105 /** 106 * Convert linear RGB to CIE-L*ab color space. 107 * 108 * <p>Derived from formula at https://www.easyrgb.com/en/math.php 109 * 110 * @param color The int representation of an sRGB color. 111 * @return the three values for L*, a* and b* 112 */ 113 public static double[] rgb2lab(int color) { 114 double r = linearColor(Color.red(color)); 115 double g = linearColor(Color.green(color)); 116 double b = linearColor(Color.blue(color)); 117 double x = (r * 0.4124d + g * 0.3576d + b * 0.1805d) / 0.95047d; 118 double y = (r * 0.2126d + g * 0.7152d + b * 0.0722d) / 1.00000d; 119 double z = (r * 0.0193d + g * 0.1192d + b * 0.9505d) / 1.08883d; 120 x = (x > 0.008856d) ? Math.cbrt(x) : (7.787d * x) + 16.0d / 116; 121 y = (y > 0.008856d) ? Math.cbrt(y) : (7.787d * y) + 16.0d / 116; 122 z = (z > 0.008856d) ? Math.cbrt(z) : (7.787d * z) + 16.0d / 116; 123 return new double[] {(116 * y) - 16, 500 * (x - y), 200 * (y - z)}; 124 } 125 126 /** 127 * Calculates the contrast ratio of two color ints. 128 */ calculateContrastRatio(int color1, int color2)129 public static double calculateContrastRatio(int color1, int color2) { 130 return calculateContrastRatio(calculateLuminance(color1), calculateLuminance(color2)); 131 } 132 133 /** 134 * Calculates the contrast ratio of two order-independent luminance values. 135 * 136 * <p>Derived from formula at http://gmazzocato.altervista.org/colorwheel/algo.php 137 * 138 * @param lum1 The first luminance value 139 * @param lum2 The second luminance value 140 * @return The contrast ratio of the luminance values 141 * @throws IllegalArgumentException if luminance values are < 0 142 */ calculateContrastRatio(double lum1, double lum2)143 public static double calculateContrastRatio(double lum1, double lum2) { 144 if ((lum1 < 0.0d) || (lum2 < 0.0d)) { 145 throw new IllegalArgumentException("Luminance values may not be negative."); 146 } 147 148 return (max(lum1, lum2) + 0.05d) / (min(lum1, lum2) + 0.05d); 149 } 150 151 /** 152 * Calculates contrast ratio between foreground color and background color when the background is 153 * overlaid on an opaque backdrop. 154 * 155 * @param foregroundColor The foreground color (can be opaque or non-opaque) 156 * @param backgroundColor The background color (can be opaque or non-opaque) 157 * @param backdrop The opaque backdrop 158 * @return The contrast ratio of foreground with background overlaid on backdrop 159 */ calculateContrastOnBackdrop( int foregroundColor, int backgroundColor, int backdrop)160 private static double calculateContrastOnBackdrop( 161 int foregroundColor, int backgroundColor, int backdrop) { 162 int backgroundOnBackdrop = compositeColors(backgroundColor, backdrop); 163 int compositeforegroundColorOnBackgroundOnBackdrop = 164 compositeColors(foregroundColor, backgroundOnBackdrop); 165 return calculateContrastRatio( 166 compositeforegroundColorOnBackgroundOnBackdrop, backgroundOnBackdrop); 167 } 168 169 /** 170 * Calculates maximum and minimum value of contrast ratio when background color is not opaque. 171 * Approach: https://lists.w3.org/Archives/Public/w3c-wai-ig/2012OctDec/0011.html#replies 172 * 173 * @param foregroundColor The foreground color (can be opaque or non-opaque) 174 * @param backgroundColor The background color (can be opaque or non-opaque) 175 * @return The range [minimum, maximum] of contrast ratio 176 */ calculateContrastRatioRange( int foregroundColor, int backgroundColor)177 public static Range<Double> calculateContrastRatioRange( 178 int foregroundColor, int backgroundColor) { 179 double contrastOnBlackBackdrop = 180 calculateContrastOnBackdrop(foregroundColor, backgroundColor, Color.BLACK); 181 double contrastOnWhiteBackdrop = 182 calculateContrastOnBackdrop(foregroundColor, backgroundColor, Color.WHITE); 183 184 double backgroundOnBlackLuminance = 185 calculateLuminance(compositeColors(backgroundColor, Color.BLACK)); 186 double backgroundOnWhiteLuminance = 187 calculateLuminance(compositeColors(backgroundColor, Color.WHITE)); 188 double foregroundColorLuminance = calculateLuminance(foregroundColor); 189 190 double minimumContrastRatio = 1.0; 191 if (foregroundColorLuminance < backgroundOnBlackLuminance) { 192 minimumContrastRatio = contrastOnBlackBackdrop; 193 } else if (backgroundOnWhiteLuminance < foregroundColorLuminance) { 194 minimumContrastRatio = contrastOnWhiteBackdrop; 195 } 196 return Range.closed( 197 minimumContrastRatio, max(contrastOnBlackBackdrop, contrastOnWhiteBackdrop)); 198 } 199 200 /** 201 * Converts an {@code int} representation of a {@link Color} to a hex string in the form of <code> 202 * #<i>rrggbb</i></code> if opaque or <code>#<i>aarrggbb</i></code> if not. 203 * 204 * @param color The {@link Color} value to convert 205 * @return The hex string representation of the color 206 */ colorToHexString(int color)207 public static String colorToHexString(int color) { 208 if (Color.alpha(color) == 255) { 209 return String.format("#%06X", (0xFFFFFF & color)); 210 } 211 return String.format("#%08X", color); 212 } 213 214 /** 215 * Overlays a translucent color over an opaque color. 216 * 217 * @param color The translucent {@link Color} 218 * @param colorToOverlayOn The opaque {@link Color} 219 * @return The alpha blended {@link Color} 220 */ compositeColors(int color, int colorToOverlayOn)221 public static int compositeColors(int color, int colorToOverlayOn) { 222 int alpha = Color.alpha(color); 223 int r = compositeComponents(Color.red(color), Color.red(colorToOverlayOn), alpha); 224 int g = compositeComponents(Color.green(color), Color.green(colorToOverlayOn), alpha); 225 int b = compositeComponents(Color.blue(color), Color.blue(colorToOverlayOn), alpha); 226 return Color.argb(COLOR_MASK, r, g, b); 227 } 228 229 /** 230 * Overlays a translucent color component over an opaque color component. 231 * 232 * @param component The translucent color component 233 * @param componentToOverlayOn The opaque color component 234 * @return The alpha blended color component 235 */ compositeComponents(int component, int componentToOverlayOn, int alpha)236 private static int compositeComponents(int component, int componentToOverlayOn, int alpha) { 237 return (component * alpha + componentToOverlayOn * (COLOR_MASK - alpha)) / COLOR_MASK; 238 } 239 } 240