• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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