1/*
2 * Copyright 2014 The Android Open Source Project
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
17package android.support.v7.graphics;
18
19import android.graphics.Color;
20
21final class ColorUtils {
22
23    private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
24    private static final int MIN_ALPHA_SEARCH_PRECISION = 10;
25
26    private ColorUtils() {}
27
28    /**
29     * Composite two potentially translucent colors over each other and returns the result.
30     */
31    private static int compositeColors(int fg, int bg) {
32        final float alpha1 = Color.alpha(fg) / 255f;
33        final float alpha2 = Color.alpha(bg) / 255f;
34
35        float a = (alpha1 + alpha2) * (1f - alpha1);
36        float r = (Color.red(fg) * alpha1) + (Color.red(bg) * alpha2 * (1f - alpha1));
37        float g = (Color.green(fg) * alpha1) + (Color.green(bg) * alpha2 * (1f - alpha1));
38        float b = (Color.blue(fg) * alpha1) + (Color.blue(bg) * alpha2 * (1f - alpha1));
39
40        return Color.argb((int) a, (int) r, (int) g, (int) b);
41    }
42
43    /**
44     * Returns the luminance of a color.
45     *
46     * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
47     */
48    private static double calculateLuminance(int color) {
49        double red = Color.red(color) / 255d;
50        red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4);
51
52        double green = Color.green(color) / 255d;
53        green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4);
54
55        double blue = Color.blue(color) / 255d;
56        blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4);
57
58        return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
59    }
60
61    /**
62     * Returns the contrast ratio between two colors.
63     *
64     * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
65     */
66    private static double calculateContrast(int foreground, int background) {
67        if (Color.alpha(background) != 255) {
68            throw new IllegalArgumentException("background can not be translucent");
69        }
70        if (Color.alpha(foreground) < 255) {
71            // If the foreground is translucent, composite the foreground over the background
72            foreground = compositeColors(foreground, background);
73        }
74
75        final double luminance1 = calculateLuminance(foreground) + 0.05;
76        final double luminance2 = calculateLuminance(background) + 0.05;
77
78        // Now return the lighter luminance divided by the darker luminance
79        return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
80    }
81
82    /**
83     * Finds the minimum alpha value which can be applied to {@code foreground} so that is has a
84     * contrast value of at least {@code minContrastRatio} when compared to background.
85     *
86     * @return the alpha value in the range 0-255.
87     */
88    private static int findMinimumAlpha(int foreground, int background, double minContrastRatio) {
89        if (Color.alpha(background) != 255) {
90            throw new IllegalArgumentException("background can not be translucent");
91        }
92
93        // First lets check that a fully opaque foreground has sufficient contrast
94        int testForeground = modifyAlpha(foreground, 255);
95        double testRatio = calculateContrast(testForeground, background);
96        if (testRatio < minContrastRatio) {
97            // Fully opaque foreground does not have sufficient contrast, return error
98            return -1;
99        }
100
101        // Binary search to find a value with the minimum value which provides sufficient contrast
102        int numIterations = 0;
103        int minAlpha = 0;
104        int maxAlpha = 255;
105
106        while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS &&
107                (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) {
108            final int testAlpha = (minAlpha + maxAlpha) / 2;
109
110            testForeground = modifyAlpha(foreground, testAlpha);
111            testRatio = calculateContrast(testForeground, background);
112
113            if (testRatio < minContrastRatio) {
114                minAlpha = testAlpha;
115            } else {
116                maxAlpha = testAlpha;
117            }
118
119            numIterations++;
120        }
121
122        // Conservatively return the max of the range of possible alphas, which is known to pass.
123        return maxAlpha;
124    }
125
126    static int getTextColorForBackground(int backgroundColor, int textColor, float minContrastRatio) {
127        final int minAlpha = ColorUtils
128                .findMinimumAlpha(textColor, backgroundColor, minContrastRatio);
129
130        if (minAlpha >= 0) {
131            return ColorUtils.modifyAlpha(textColor, minAlpha);
132        }
133
134        // Didn't find an opacity which provided enough contrast
135        return -1;
136    }
137
138    static void RGBtoHSL(int r, int g, int b, float[] hsl) {
139        final float rf = r / 255f;
140        final float gf = g / 255f;
141        final float bf = b / 255f;
142
143        final float max = Math.max(rf, Math.max(gf, bf));
144        final float min = Math.min(rf, Math.min(gf, bf));
145        final float deltaMaxMin = max - min;
146
147        float h, s;
148        float l = (max + min) / 2f;
149
150        if (max == min) {
151            // Monochromatic
152            h = s = 0f;
153        } else {
154            if (max == rf) {
155                h = ((gf - bf) / deltaMaxMin) % 6f;
156            } else if (max == gf) {
157                h = ((bf - rf) / deltaMaxMin) + 2f;
158            } else {
159                h = ((rf - gf) / deltaMaxMin) + 4f;
160            }
161
162            s =  deltaMaxMin / (1f - Math.abs(2f * l - 1f));
163        }
164
165        hsl[0] = (h * 60f) % 360f;
166        hsl[1] = s;
167        hsl[2] = l;
168    }
169
170    static int HSLtoRGB (float[] hsl) {
171        final float h = hsl[0];
172        final float s = hsl[1];
173        final float l = hsl[2];
174
175        final float c = (1f - Math.abs(2 * l - 1f)) * s;
176        final float m = l - 0.5f * c;
177        final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
178
179        final int hueSegment = (int) h / 60;
180
181        int r = 0, g = 0, b = 0;
182
183        switch (hueSegment) {
184            case 0:
185                r = Math.round(255 * (c + m));
186                g = Math.round(255 * (x + m));
187                b = Math.round(255 * m);
188                break;
189            case 1:
190                r = Math.round(255 * (x + m));
191                g = Math.round(255 * (c + m));
192                b = Math.round(255 * m);
193                break;
194            case 2:
195                r = Math.round(255 * m);
196                g = Math.round(255 * (c + m));
197                b = Math.round(255 * (x + m));
198                break;
199            case 3:
200                r = Math.round(255 * m);
201                g = Math.round(255 * (x + m));
202                b = Math.round(255 * (c + m));
203                break;
204            case 4:
205                r = Math.round(255 * (x + m));
206                g = Math.round(255 * m);
207                b = Math.round(255 * (c + m));
208                break;
209            case 5:
210            case 6:
211                r = Math.round(255 * (c + m));
212                g = Math.round(255 * m);
213                b = Math.round(255 * (x + m));
214                break;
215        }
216
217        r = Math.max(0, Math.min(255, r));
218        g = Math.max(0, Math.min(255, g));
219        b = Math.max(0, Math.min(255, b));
220
221        return Color.rgb(r, g, b);
222    }
223
224    /**
225     * Set the alpha component of {@code color} to be {@code alpha}.
226     */
227    static int modifyAlpha(int color, int alpha) {
228        return (color & 0x00ffffff) | (alpha << 24);
229    }
230
231}
232