NotificationColorUtil.java revision 389edcd7c56b886abe2df23eba38cb8aa40082f0
1/*
2 * Copyright (C) 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 com.android.internal.util;
18
19import android.annotation.ColorInt;
20import android.annotation.FloatRange;
21import android.annotation.IntRange;
22import android.annotation.NonNull;
23import android.app.Notification;
24import android.content.Context;
25import android.content.res.ColorStateList;
26import android.content.res.Resources;
27import android.graphics.Bitmap;
28import android.graphics.Color;
29import android.graphics.drawable.AnimationDrawable;
30import android.graphics.drawable.BitmapDrawable;
31import android.graphics.drawable.Drawable;
32import android.graphics.drawable.Icon;
33import android.graphics.drawable.VectorDrawable;
34import android.text.SpannableStringBuilder;
35import android.text.Spanned;
36import android.text.style.CharacterStyle;
37import android.text.style.ForegroundColorSpan;
38import android.text.style.TextAppearanceSpan;
39import android.util.Log;
40import android.util.Pair;
41
42import java.util.Arrays;
43import java.util.WeakHashMap;
44
45/**
46 * Helper class to process legacy (Holo) notifications to make them look like material notifications.
47 *
48 * @hide
49 */
50public class NotificationColorUtil {
51
52    private static final String TAG = "NotificationColorUtil";
53    private static final boolean DEBUG = false;
54
55    private static final Object sLock = new Object();
56    private static NotificationColorUtil sInstance;
57
58    private final ImageUtils mImageUtils = new ImageUtils();
59    private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
60            new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
61
62    private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
63
64    public static NotificationColorUtil getInstance(Context context) {
65        synchronized (sLock) {
66            if (sInstance == null) {
67                sInstance = new NotificationColorUtil(context);
68            }
69            return sInstance;
70        }
71    }
72
73    private NotificationColorUtil(Context context) {
74        mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
75                com.android.internal.R.dimen.notification_large_icon_width);
76    }
77
78    /**
79     * Checks whether a Bitmap is a small grayscale icon.
80     * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
81     *
82     * @param bitmap The bitmap to test.
83     * @return True if the bitmap is grayscale; false if it is color or too large to examine.
84     */
85    public boolean isGrayscaleIcon(Bitmap bitmap) {
86        // quick test: reject large bitmaps
87        if (bitmap.getWidth() > mGrayscaleIconMaxSize
88                || bitmap.getHeight() > mGrayscaleIconMaxSize) {
89            return false;
90        }
91
92        synchronized (sLock) {
93            Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
94            if (cached != null) {
95                if (cached.second == bitmap.getGenerationId()) {
96                    return cached.first;
97                }
98            }
99        }
100        boolean result;
101        int generationId;
102        synchronized (mImageUtils) {
103            result = mImageUtils.isGrayscale(bitmap);
104
105            // generationId and the check whether the Bitmap is grayscale can't be read atomically
106            // here. However, since the thread is in the process of posting the notification, we can
107            // assume that it doesn't modify the bitmap while we are checking the pixels.
108            generationId = bitmap.getGenerationId();
109        }
110        synchronized (sLock) {
111            mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
112        }
113        return result;
114    }
115
116    /**
117     * Checks whether a Drawable is a small grayscale icon.
118     * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
119     *
120     * @param d The drawable to test.
121     * @return True if the bitmap is grayscale; false if it is color or too large to examine.
122     */
123    public boolean isGrayscaleIcon(Drawable d) {
124        if (d == null) {
125            return false;
126        } else if (d instanceof BitmapDrawable) {
127            BitmapDrawable bd = (BitmapDrawable) d;
128            return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
129        } else if (d instanceof AnimationDrawable) {
130            AnimationDrawable ad = (AnimationDrawable) d;
131            int count = ad.getNumberOfFrames();
132            return count > 0 && isGrayscaleIcon(ad.getFrame(0));
133        } else if (d instanceof VectorDrawable) {
134            // We just assume you're doing the right thing if using vectors
135            return true;
136        } else {
137            return false;
138        }
139    }
140
141    public boolean isGrayscaleIcon(Context context, Icon icon) {
142        if (icon == null) {
143            return false;
144        }
145        switch (icon.getType()) {
146            case Icon.TYPE_BITMAP:
147                return isGrayscaleIcon(icon.getBitmap());
148            case Icon.TYPE_RESOURCE:
149                return isGrayscaleIcon(context, icon.getResId());
150            default:
151                return false;
152        }
153    }
154
155    /**
156     * Checks whether a drawable with a resoure id is a small grayscale icon.
157     * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
158     *
159     * @param context The context to load the drawable from.
160     * @return True if the bitmap is grayscale; false if it is color or too large to examine.
161     */
162    public boolean isGrayscaleIcon(Context context, int drawableResId) {
163        if (drawableResId != 0) {
164            try {
165                return isGrayscaleIcon(context.getDrawable(drawableResId));
166            } catch (Resources.NotFoundException ex) {
167                Log.e(TAG, "Drawable not found: " + drawableResId);
168                return false;
169            }
170        } else {
171            return false;
172        }
173    }
174
175    /**
176     * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
177     * the text.
178     *
179     * @param charSequence The text to process.
180     * @return The color inverted text.
181     */
182    public CharSequence invertCharSequenceColors(CharSequence charSequence) {
183        if (charSequence instanceof Spanned) {
184            Spanned ss = (Spanned) charSequence;
185            Object[] spans = ss.getSpans(0, ss.length(), Object.class);
186            SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
187            for (Object span : spans) {
188                Object resultSpan = span;
189                if (resultSpan instanceof CharacterStyle) {
190                    resultSpan = ((CharacterStyle) span).getUnderlying();
191                }
192                if (resultSpan instanceof TextAppearanceSpan) {
193                    TextAppearanceSpan processedSpan = processTextAppearanceSpan(
194                            (TextAppearanceSpan) span);
195                    if (processedSpan != resultSpan) {
196                        resultSpan = processedSpan;
197                    } else {
198                        // we need to still take the orgininal for wrapped spans
199                        resultSpan = span;
200                    }
201                } else if (resultSpan instanceof ForegroundColorSpan) {
202                    ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan;
203                    int foregroundColor = originalSpan.getForegroundColor();
204                    resultSpan = new ForegroundColorSpan(processColor(foregroundColor));
205                } else {
206                    resultSpan = span;
207                }
208                builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
209                        ss.getSpanFlags(span));
210            }
211            return builder;
212        }
213        return charSequence;
214    }
215
216    private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
217        ColorStateList colorStateList = span.getTextColor();
218        if (colorStateList != null) {
219            int[] colors = colorStateList.getColors();
220            boolean changed = false;
221            for (int i = 0; i < colors.length; i++) {
222                if (ImageUtils.isGrayscale(colors[i])) {
223
224                    // Allocate a new array so we don't change the colors in the old color state
225                    // list.
226                    if (!changed) {
227                        colors = Arrays.copyOf(colors, colors.length);
228                    }
229                    colors[i] = processColor(colors[i]);
230                    changed = true;
231                }
232            }
233            if (changed) {
234                return new TextAppearanceSpan(
235                        span.getFamily(), span.getTextStyle(), span.getTextSize(),
236                        new ColorStateList(colorStateList.getStates(), colors),
237                        span.getLinkTextColor());
238            }
239        }
240        return span;
241    }
242
243    private int processColor(int color) {
244        return Color.argb(Color.alpha(color),
245                255 - Color.red(color),
246                255 - Color.green(color),
247                255 - Color.blue(color));
248    }
249
250    /**
251     * Finds a suitable color such that there's enough contrast.
252     *
253     * @param color the color to start searching from.
254     * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
255     * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
256     * @param minRatio the minimum contrast ratio required.
257     * @return a color with the same hue as {@param color}, potentially darkened to meet the
258     *          contrast ratio.
259     */
260    public static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
261        int fg = findFg ? color : other;
262        int bg = findFg ? other : color;
263        if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
264            return color;
265        }
266
267        double[] lab = new double[3];
268        ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
269
270        double low = 0, high = lab[0];
271        final double a = lab[1], b = lab[2];
272        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
273            final double l = (low + high) / 2;
274            if (findFg) {
275                fg = ColorUtilsFromCompat.LABToColor(l, a, b);
276            } else {
277                bg = ColorUtilsFromCompat.LABToColor(l, a, b);
278            }
279            if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
280                low = l;
281            } else {
282                high = l;
283            }
284        }
285        return ColorUtilsFromCompat.LABToColor(low, a, b);
286    }
287
288    /**
289     * Finds a suitable alpha such that there's enough contrast.
290     *
291     * @param color the color to start searching from.
292     * @param backgroundColor the color to ensure contrast against.
293     * @param minRatio the minimum contrast ratio required.
294     * @return the same color as {@param color} with potentially modified alpha to meet contrast
295     */
296    public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) {
297        int fg = color;
298        int bg = backgroundColor;
299        if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
300            return color;
301        }
302        int startAlpha = Color.alpha(color);
303        int r = Color.red(color);
304        int g = Color.green(color);
305        int b = Color.blue(color);
306
307        int low = startAlpha, high = 255;
308        for (int i = 0; i < 15 && high - low > 0; i++) {
309            final int alpha = (low + high) / 2;
310            fg = Color.argb(alpha, r, g, b);
311            if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
312                high = alpha;
313            } else {
314                low = alpha;
315            }
316        }
317        return Color.argb(high, r, g, b);
318    }
319
320    /**
321     * Finds a suitable color such that there's enough contrast.
322     *
323     * @param color the color to start searching from.
324     * @param other the color to ensure contrast against. Assumed to be darker than {@param color}
325     * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
326     * @param minRatio the minimum contrast ratio required.
327     * @return a color with the same hue as {@param color}, potentially darkened to meet the
328     *          contrast ratio.
329     */
330    public static int findContrastColorAgainstDark(int color, int other, boolean findFg,
331            double minRatio) {
332        int fg = findFg ? color : other;
333        int bg = findFg ? other : color;
334        if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
335            return color;
336        }
337
338        float[] hsl = new float[3];
339        ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl);
340
341        float low = hsl[2], high = 1;
342        for (int i = 0; i < 15 && high - low > 0.00001; i++) {
343            final float l = (low + high) / 2;
344            hsl[2] = l;
345            if (findFg) {
346                fg = ColorUtilsFromCompat.HSLToColor(hsl);
347            } else {
348                bg = ColorUtilsFromCompat.HSLToColor(hsl);
349            }
350            if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
351                high = l;
352            } else {
353                low = l;
354            }
355        }
356        return findFg ? fg : bg;
357    }
358
359    public static int ensureTextContrastOnBlack(int color) {
360        return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12);
361    }
362
363    /**
364     * Finds a text color with sufficient contrast over bg that has the same hue as the original
365     * color, assuming it is for large text.
366     */
367    public static int ensureLargeTextContrast(int color, int bg) {
368        return findContrastColor(color, bg, true, 3);
369    }
370
371    /**
372     * Finds a text color with sufficient contrast over bg that has the same hue as the original
373     * color.
374     */
375    private static int ensureTextContrast(int color, int bg) {
376        return findContrastColor(color, bg, true, 4.5);
377    }
378
379    /** Finds a background color for a text view with given text color and hint text color, that
380     * has the same hue as the original color.
381     */
382    public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
383        color = findContrastColor(color, hintColor, false, 3.0);
384        return findContrastColor(color, textColor, false, 4.5);
385    }
386
387    private static String contrastChange(int colorOld, int colorNew, int bg) {
388        return String.format("from %.2f:1 to %.2f:1",
389                ColorUtilsFromCompat.calculateContrast(colorOld, bg),
390                ColorUtilsFromCompat.calculateContrast(colorNew, bg));
391    }
392
393    /**
394     * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
395     */
396    public static int resolveColor(Context context, int color) {
397        if (color == Notification.COLOR_DEFAULT) {
398            return context.getColor(com.android.internal.R.color.notification_icon_default_color);
399        }
400        return color;
401    }
402
403    /**
404     * Resolves a Notification's color such that it has enough contrast to be used as the
405     * color for the Notification's action and header text.
406     *
407     * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
408     * @param backgroundColor the background color to ensure the contrast against.
409     * @return a color of the same hue with enough contrast against the backgrounds.
410     */
411    public static int resolveContrastColor(Context context, int notificationColor,
412            int backgroundColor) {
413        final int resolvedColor = resolveColor(context, notificationColor);
414
415        final int actionBg = context.getColor(
416                com.android.internal.R.color.notification_action_list);
417
418        int color = resolvedColor;
419        color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg);
420        color = NotificationColorUtil.ensureTextContrast(color, backgroundColor);
421
422        if (color != resolvedColor) {
423            if (DEBUG){
424                Log.w(TAG, String.format(
425                        "Enhanced contrast of notification for %s %s (over action)"
426                                + " and %s (over background) by changing #%s to %s",
427                        context.getPackageName(),
428                        NotificationColorUtil.contrastChange(resolvedColor, color, actionBg),
429                        NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor),
430                        Integer.toHexString(resolvedColor), Integer.toHexString(color)));
431            }
432        }
433        return color;
434    }
435
436    /**
437     * Change a color by a specified value
438     * @param baseColor the base color to lighten
439     * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
440     *               increase in the LAB color space. A negative value will darken the color and
441     *               a positive will lighten it.
442     * @return the changed color
443     */
444    public static int changeColorLightness(int baseColor, int amount) {
445        final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
446        ColorUtilsFromCompat.colorToLAB(baseColor, result);
447        result[0] = Math.max(Math.min(100, result[0] + amount), 0);
448        return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
449    }
450
451    public static int resolveAmbientColor(Context context, int notificationColor) {
452        final int resolvedColor = resolveColor(context, notificationColor);
453
454        int color = resolvedColor;
455        color = NotificationColorUtil.ensureTextContrastOnBlack(color);
456
457        if (color != resolvedColor) {
458            if (DEBUG){
459                Log.w(TAG, String.format(
460                        "Ambient contrast of notification for %s is %s (over black)"
461                                + " by changing #%s to #%s",
462                        context.getPackageName(),
463                        NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK),
464                        Integer.toHexString(resolvedColor), Integer.toHexString(color)));
465            }
466        }
467        return color;
468    }
469
470    public static int resolvePrimaryColor(Context context, int backgroundColor) {
471        boolean useDark = shouldUseDark(backgroundColor);
472        if (useDark) {
473            return context.getColor(
474                    com.android.internal.R.color.notification_primary_text_color_light);
475        } else {
476            return context.getColor(
477                    com.android.internal.R.color.notification_primary_text_color_dark);
478        }
479    }
480
481    public static int resolveSecondaryColor(Context context, int backgroundColor) {
482        boolean useDark = shouldUseDark(backgroundColor);
483        if (useDark) {
484            return context.getColor(
485                    com.android.internal.R.color.notification_secondary_text_color_light);
486        } else {
487            return context.getColor(
488                    com.android.internal.R.color.notification_secondary_text_color_dark);
489        }
490    }
491
492    public static int resolveActionBarColor(Context context, int backgroundColor) {
493        if (backgroundColor == Notification.COLOR_DEFAULT) {
494            return context.getColor(com.android.internal.R.color.notification_action_list);
495        }
496        return getShiftedColor(backgroundColor, 7);
497    }
498
499    /**
500     * Get a color that stays in the same tint, but darkens or lightens it by a certain
501     * amount.
502     * This also looks at the lightness of the provided color and shifts it appropriately.
503     *
504     * @param color the base color to use
505     * @param amount the amount from 1 to 100 how much to modify the color
506     * @return the now color that was modified
507     */
508    public static int getShiftedColor(int color, int amount) {
509        final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
510        ColorUtilsFromCompat.colorToLAB(color, result);
511        if (result[0] >= 4) {
512            result[0] = Math.max(0, result[0] - amount);
513        } else {
514            result[0] = Math.min(100, result[0] + amount);
515        }
516        return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
517    }
518
519    private static boolean shouldUseDark(int backgroundColor) {
520        boolean useDark = backgroundColor == Notification.COLOR_DEFAULT;
521        if (!useDark) {
522            useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5;
523        }
524        return useDark;
525    }
526
527    public static double calculateLuminance(int backgroundColor) {
528        return ColorUtilsFromCompat.calculateLuminance(backgroundColor);
529    }
530
531
532    public static double calculateContrast(int foregroundColor, int backgroundColor) {
533        return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor);
534    }
535
536    public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) {
537        return NotificationColorUtil.calculateContrast(backgroundColor, foregroundColor) >= 4.5;
538    }
539
540    /**
541     * Composite two potentially translucent colors over each other and returns the result.
542     */
543    public static int compositeColors(int foreground, int background) {
544        return ColorUtilsFromCompat.compositeColors(foreground, background);
545    }
546
547    public static boolean isColorLight(int backgroundColor) {
548        return calculateLuminance(backgroundColor) > 0.5f;
549    }
550
551    /**
552     * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
553     */
554    private static class ColorUtilsFromCompat {
555        private static final double XYZ_WHITE_REFERENCE_X = 95.047;
556        private static final double XYZ_WHITE_REFERENCE_Y = 100;
557        private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
558        private static final double XYZ_EPSILON = 0.008856;
559        private static final double XYZ_KAPPA = 903.3;
560
561        private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
562        private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
563
564        private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
565
566        private ColorUtilsFromCompat() {}
567
568        /**
569         * Composite two potentially translucent colors over each other and returns the result.
570         */
571        public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
572            int bgAlpha = Color.alpha(background);
573            int fgAlpha = Color.alpha(foreground);
574            int a = compositeAlpha(fgAlpha, bgAlpha);
575
576            int r = compositeComponent(Color.red(foreground), fgAlpha,
577                    Color.red(background), bgAlpha, a);
578            int g = compositeComponent(Color.green(foreground), fgAlpha,
579                    Color.green(background), bgAlpha, a);
580            int b = compositeComponent(Color.blue(foreground), fgAlpha,
581                    Color.blue(background), bgAlpha, a);
582
583            return Color.argb(a, r, g, b);
584        }
585
586        private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
587            return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
588        }
589
590        private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
591            if (a == 0) return 0;
592            return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
593        }
594
595        /**
596         * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
597         * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
598         */
599        @FloatRange(from = 0.0, to = 1.0)
600        public static double calculateLuminance(@ColorInt int color) {
601            final double[] result = getTempDouble3Array();
602            colorToXYZ(color, result);
603            // Luminance is the Y component
604            return result[1] / 100;
605        }
606
607        /**
608         * Returns the contrast ratio between {@code foreground} and {@code background}.
609         * {@code background} must be opaque.
610         * <p>
611         * Formula defined
612         * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
613         */
614        public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
615            if (Color.alpha(background) != 255) {
616                throw new IllegalArgumentException("background can not be translucent: #"
617                        + Integer.toHexString(background));
618            }
619            if (Color.alpha(foreground) < 255) {
620                // If the foreground is translucent, composite the foreground over the background
621                foreground = compositeColors(foreground, background);
622            }
623
624            final double luminance1 = calculateLuminance(foreground) + 0.05;
625            final double luminance2 = calculateLuminance(background) + 0.05;
626
627            // Now return the lighter luminance divided by the darker luminance
628            return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
629        }
630
631        /**
632         * Convert the ARGB color to its CIE Lab representative components.
633         *
634         * @param color  the ARGB color to convert. The alpha component is ignored
635         * @param outLab 3-element array which holds the resulting LAB components
636         */
637        public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
638            RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
639        }
640
641        /**
642         * Convert RGB components to its CIE Lab representative components.
643         *
644         * <ul>
645         * <li>outLab[0] is L [0 ...100)</li>
646         * <li>outLab[1] is a [-128...127)</li>
647         * <li>outLab[2] is b [-128...127)</li>
648         * </ul>
649         *
650         * @param r      red component value [0..255]
651         * @param g      green component value [0..255]
652         * @param b      blue component value [0..255]
653         * @param outLab 3-element array which holds the resulting LAB components
654         */
655        public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
656                @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
657                @NonNull double[] outLab) {
658            // First we convert RGB to XYZ
659            RGBToXYZ(r, g, b, outLab);
660            // outLab now contains XYZ
661            XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
662            // outLab now contains LAB representation
663        }
664
665        /**
666         * Convert the ARGB color to it's CIE XYZ representative components.
667         *
668         * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
669         * 2° Standard Observer (1931).</p>
670         *
671         * <ul>
672         * <li>outXyz[0] is X [0 ...95.047)</li>
673         * <li>outXyz[1] is Y [0...100)</li>
674         * <li>outXyz[2] is Z [0...108.883)</li>
675         * </ul>
676         *
677         * @param color  the ARGB color to convert. The alpha component is ignored
678         * @param outXyz 3-element array which holds the resulting LAB components
679         */
680        public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
681            RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
682        }
683
684        /**
685         * Convert RGB components to it's CIE XYZ representative components.
686         *
687         * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
688         * 2° Standard Observer (1931).</p>
689         *
690         * <ul>
691         * <li>outXyz[0] is X [0 ...95.047)</li>
692         * <li>outXyz[1] is Y [0...100)</li>
693         * <li>outXyz[2] is Z [0...108.883)</li>
694         * </ul>
695         *
696         * @param r      red component value [0..255]
697         * @param g      green component value [0..255]
698         * @param b      blue component value [0..255]
699         * @param outXyz 3-element array which holds the resulting XYZ components
700         */
701        public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
702                @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
703                @NonNull double[] outXyz) {
704            if (outXyz.length != 3) {
705                throw new IllegalArgumentException("outXyz must have a length of 3.");
706            }
707
708            double sr = r / 255.0;
709            sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
710            double sg = g / 255.0;
711            sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
712            double sb = b / 255.0;
713            sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
714
715            outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
716            outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
717            outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
718        }
719
720        /**
721         * Converts a color from CIE XYZ to CIE Lab representation.
722         *
723         * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
724         * 2° Standard Observer (1931).</p>
725         *
726         * <ul>
727         * <li>outLab[0] is L [0 ...100)</li>
728         * <li>outLab[1] is a [-128...127)</li>
729         * <li>outLab[2] is b [-128...127)</li>
730         * </ul>
731         *
732         * @param x      X component value [0...95.047)
733         * @param y      Y component value [0...100)
734         * @param z      Z component value [0...108.883)
735         * @param outLab 3-element array which holds the resulting Lab components
736         */
737        public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
738                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
739                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
740                @NonNull double[] outLab) {
741            if (outLab.length != 3) {
742                throw new IllegalArgumentException("outLab must have a length of 3.");
743            }
744            x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
745            y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
746            z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
747            outLab[0] = Math.max(0, 116 * y - 16);
748            outLab[1] = 500 * (x - y);
749            outLab[2] = 200 * (y - z);
750        }
751
752        /**
753         * Converts a color from CIE Lab to CIE XYZ representation.
754         *
755         * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
756         * 2° Standard Observer (1931).</p>
757         *
758         * <ul>
759         * <li>outXyz[0] is X [0 ...95.047)</li>
760         * <li>outXyz[1] is Y [0...100)</li>
761         * <li>outXyz[2] is Z [0...108.883)</li>
762         * </ul>
763         *
764         * @param l      L component value [0...100)
765         * @param a      A component value [-128...127)
766         * @param b      B component value [-128...127)
767         * @param outXyz 3-element array which holds the resulting XYZ components
768         */
769        public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
770                @FloatRange(from = -128, to = 127) final double a,
771                @FloatRange(from = -128, to = 127) final double b,
772                @NonNull double[] outXyz) {
773            final double fy = (l + 16) / 116;
774            final double fx = a / 500 + fy;
775            final double fz = fy - b / 200;
776
777            double tmp = Math.pow(fx, 3);
778            final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
779            final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
780
781            tmp = Math.pow(fz, 3);
782            final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
783
784            outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
785            outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
786            outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
787        }
788
789        /**
790         * Converts a color from CIE XYZ to its RGB representation.
791         *
792         * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
793         * 2° Standard Observer (1931).</p>
794         *
795         * @param x X component value [0...95.047)
796         * @param y Y component value [0...100)
797         * @param z Z component value [0...108.883)
798         * @return int containing the RGB representation
799         */
800        @ColorInt
801        public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
802                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
803                @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
804            double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
805            double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
806            double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
807
808            r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
809            g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
810            b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
811
812            return Color.rgb(
813                    constrain((int) Math.round(r * 255), 0, 255),
814                    constrain((int) Math.round(g * 255), 0, 255),
815                    constrain((int) Math.round(b * 255), 0, 255));
816        }
817
818        /**
819         * Converts a color from CIE Lab to its RGB representation.
820         *
821         * @param l L component value [0...100]
822         * @param a A component value [-128...127]
823         * @param b B component value [-128...127]
824         * @return int containing the RGB representation
825         */
826        @ColorInt
827        public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
828                @FloatRange(from = -128, to = 127) final double a,
829                @FloatRange(from = -128, to = 127) final double b) {
830            final double[] result = getTempDouble3Array();
831            LABToXYZ(l, a, b, result);
832            return XYZToColor(result[0], result[1], result[2]);
833        }
834
835        private static int constrain(int amount, int low, int high) {
836            return amount < low ? low : (amount > high ? high : amount);
837        }
838
839        private static float constrain(float amount, float low, float high) {
840            return amount < low ? low : (amount > high ? high : amount);
841        }
842
843        private static double pivotXyzComponent(double component) {
844            return component > XYZ_EPSILON
845                    ? Math.pow(component, 1 / 3.0)
846                    : (XYZ_KAPPA * component + 16) / 116;
847        }
848
849        public static double[] getTempDouble3Array() {
850            double[] result = TEMP_ARRAY.get();
851            if (result == null) {
852                result = new double[3];
853                TEMP_ARRAY.set(result);
854            }
855            return result;
856        }
857
858        /**
859         * Convert HSL (hue-saturation-lightness) components to a RGB color.
860         * <ul>
861         * <li>hsl[0] is Hue [0 .. 360)</li>
862         * <li>hsl[1] is Saturation [0...1]</li>
863         * <li>hsl[2] is Lightness [0...1]</li>
864         * </ul>
865         * If hsv values are out of range, they are pinned.
866         *
867         * @param hsl 3-element array which holds the input HSL components
868         * @return the resulting RGB color
869         */
870        @ColorInt
871        public static int HSLToColor(@NonNull float[] hsl) {
872            final float h = hsl[0];
873            final float s = hsl[1];
874            final float l = hsl[2];
875
876            final float c = (1f - Math.abs(2 * l - 1f)) * s;
877            final float m = l - 0.5f * c;
878            final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
879
880            final int hueSegment = (int) h / 60;
881
882            int r = 0, g = 0, b = 0;
883
884            switch (hueSegment) {
885                case 0:
886                    r = Math.round(255 * (c + m));
887                    g = Math.round(255 * (x + m));
888                    b = Math.round(255 * m);
889                    break;
890                case 1:
891                    r = Math.round(255 * (x + m));
892                    g = Math.round(255 * (c + m));
893                    b = Math.round(255 * m);
894                    break;
895                case 2:
896                    r = Math.round(255 * m);
897                    g = Math.round(255 * (c + m));
898                    b = Math.round(255 * (x + m));
899                    break;
900                case 3:
901                    r = Math.round(255 * m);
902                    g = Math.round(255 * (x + m));
903                    b = Math.round(255 * (c + m));
904                    break;
905                case 4:
906                    r = Math.round(255 * (x + m));
907                    g = Math.round(255 * m);
908                    b = Math.round(255 * (c + m));
909                    break;
910                case 5:
911                case 6:
912                    r = Math.round(255 * (c + m));
913                    g = Math.round(255 * m);
914                    b = Math.round(255 * (x + m));
915                    break;
916            }
917
918            r = constrain(r, 0, 255);
919            g = constrain(g, 0, 255);
920            b = constrain(b, 0, 255);
921
922            return Color.rgb(r, g, b);
923        }
924
925        /**
926         * Convert the ARGB color to its HSL (hue-saturation-lightness) components.
927         * <ul>
928         * <li>outHsl[0] is Hue [0 .. 360)</li>
929         * <li>outHsl[1] is Saturation [0...1]</li>
930         * <li>outHsl[2] is Lightness [0...1]</li>
931         * </ul>
932         *
933         * @param color  the ARGB color to convert. The alpha component is ignored
934         * @param outHsl 3-element array which holds the resulting HSL components
935         */
936        public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) {
937            RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl);
938        }
939
940        /**
941         * Convert RGB components to HSL (hue-saturation-lightness).
942         * <ul>
943         * <li>outHsl[0] is Hue [0 .. 360)</li>
944         * <li>outHsl[1] is Saturation [0...1]</li>
945         * <li>outHsl[2] is Lightness [0...1]</li>
946         * </ul>
947         *
948         * @param r      red component value [0..255]
949         * @param g      green component value [0..255]
950         * @param b      blue component value [0..255]
951         * @param outHsl 3-element array which holds the resulting HSL components
952         */
953        public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r,
954                @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
955                @NonNull float[] outHsl) {
956            final float rf = r / 255f;
957            final float gf = g / 255f;
958            final float bf = b / 255f;
959
960            final float max = Math.max(rf, Math.max(gf, bf));
961            final float min = Math.min(rf, Math.min(gf, bf));
962            final float deltaMaxMin = max - min;
963
964            float h, s;
965            float l = (max + min) / 2f;
966
967            if (max == min) {
968                // Monochromatic
969                h = s = 0f;
970            } else {
971                if (max == rf) {
972                    h = ((gf - bf) / deltaMaxMin) % 6f;
973                } else if (max == gf) {
974                    h = ((bf - rf) / deltaMaxMin) + 2f;
975                } else {
976                    h = ((rf - gf) / deltaMaxMin) + 4f;
977                }
978
979                s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
980            }
981
982            h = (h * 60f) % 360f;
983            if (h < 0) {
984                h += 360f;
985            }
986
987            outHsl[0] = constrain(h, 0f, 360f);
988            outHsl[1] = constrain(s, 0f, 1f);
989            outHsl[2] = constrain(l, 0f, 1f);
990        }
991
992    }
993}
994