Home | History | Annotate | Download | only in util
      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 
     17 package com.android.internal.util;
     18 
     19 import android.annotation.ColorInt;
     20 import android.annotation.FloatRange;
     21 import android.annotation.IntRange;
     22 import android.annotation.NonNull;
     23 import android.app.Notification;
     24 import android.content.Context;
     25 import android.content.res.ColorStateList;
     26 import android.content.res.Resources;
     27 import android.graphics.Bitmap;
     28 import android.graphics.Color;
     29 import android.graphics.drawable.AnimationDrawable;
     30 import android.graphics.drawable.BitmapDrawable;
     31 import android.graphics.drawable.Drawable;
     32 import android.graphics.drawable.Icon;
     33 import android.graphics.drawable.VectorDrawable;
     34 import android.text.SpannableStringBuilder;
     35 import android.text.Spanned;
     36 import android.text.style.TextAppearanceSpan;
     37 import android.util.Log;
     38 import android.util.Pair;
     39 
     40 import java.util.Arrays;
     41 import java.util.WeakHashMap;
     42 
     43 /**
     44  * Helper class to process legacy (Holo) notifications to make them look like material notifications.
     45  *
     46  * @hide
     47  */
     48 public class NotificationColorUtil {
     49 
     50     private static final String TAG = "NotificationColorUtil";
     51     private static final boolean DEBUG = false;
     52 
     53     private static final Object sLock = new Object();
     54     private static NotificationColorUtil sInstance;
     55 
     56     private final ImageUtils mImageUtils = new ImageUtils();
     57     private final WeakHashMap<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
     58             new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
     59 
     60     private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp)
     61 
     62     public static NotificationColorUtil getInstance(Context context) {
     63         synchronized (sLock) {
     64             if (sInstance == null) {
     65                 sInstance = new NotificationColorUtil(context);
     66             }
     67             return sInstance;
     68         }
     69     }
     70 
     71     private NotificationColorUtil(Context context) {
     72         mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize(
     73                 com.android.internal.R.dimen.notification_large_icon_width);
     74     }
     75 
     76     /**
     77      * Checks whether a Bitmap is a small grayscale icon.
     78      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
     79      *
     80      * @param bitmap The bitmap to test.
     81      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
     82      */
     83     public boolean isGrayscaleIcon(Bitmap bitmap) {
     84         // quick test: reject large bitmaps
     85         if (bitmap.getWidth() > mGrayscaleIconMaxSize
     86                 || bitmap.getHeight() > mGrayscaleIconMaxSize) {
     87             return false;
     88         }
     89 
     90         synchronized (sLock) {
     91             Pair<Boolean, Integer> cached = mGrayscaleBitmapCache.get(bitmap);
     92             if (cached != null) {
     93                 if (cached.second == bitmap.getGenerationId()) {
     94                     return cached.first;
     95                 }
     96             }
     97         }
     98         boolean result;
     99         int generationId;
    100         synchronized (mImageUtils) {
    101             result = mImageUtils.isGrayscale(bitmap);
    102 
    103             // generationId and the check whether the Bitmap is grayscale can't be read atomically
    104             // here. However, since the thread is in the process of posting the notification, we can
    105             // assume that it doesn't modify the bitmap while we are checking the pixels.
    106             generationId = bitmap.getGenerationId();
    107         }
    108         synchronized (sLock) {
    109             mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId));
    110         }
    111         return result;
    112     }
    113 
    114     /**
    115      * Checks whether a Drawable is a small grayscale icon.
    116      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
    117      *
    118      * @param d The drawable to test.
    119      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
    120      */
    121     public boolean isGrayscaleIcon(Drawable d) {
    122         if (d == null) {
    123             return false;
    124         } else if (d instanceof BitmapDrawable) {
    125             BitmapDrawable bd = (BitmapDrawable) d;
    126             return bd.getBitmap() != null && isGrayscaleIcon(bd.getBitmap());
    127         } else if (d instanceof AnimationDrawable) {
    128             AnimationDrawable ad = (AnimationDrawable) d;
    129             int count = ad.getNumberOfFrames();
    130             return count > 0 && isGrayscaleIcon(ad.getFrame(0));
    131         } else if (d instanceof VectorDrawable) {
    132             // We just assume you're doing the right thing if using vectors
    133             return true;
    134         } else {
    135             return false;
    136         }
    137     }
    138 
    139     public boolean isGrayscaleIcon(Context context, Icon icon) {
    140         if (icon == null) {
    141             return false;
    142         }
    143         switch (icon.getType()) {
    144             case Icon.TYPE_BITMAP:
    145                 return isGrayscaleIcon(icon.getBitmap());
    146             case Icon.TYPE_RESOURCE:
    147                 return isGrayscaleIcon(context, icon.getResId());
    148             default:
    149                 return false;
    150         }
    151     }
    152 
    153     /**
    154      * Checks whether a drawable with a resoure id is a small grayscale icon.
    155      * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp".
    156      *
    157      * @param context The context to load the drawable from.
    158      * @return True if the bitmap is grayscale; false if it is color or too large to examine.
    159      */
    160     public boolean isGrayscaleIcon(Context context, int drawableResId) {
    161         if (drawableResId != 0) {
    162             try {
    163                 return isGrayscaleIcon(context.getDrawable(drawableResId));
    164             } catch (Resources.NotFoundException ex) {
    165                 Log.e(TAG, "Drawable not found: " + drawableResId);
    166                 return false;
    167             }
    168         } else {
    169             return false;
    170         }
    171     }
    172 
    173     /**
    174      * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on
    175      * the text.
    176      *
    177      * @param charSequence The text to process.
    178      * @return The color inverted text.
    179      */
    180     public CharSequence invertCharSequenceColors(CharSequence charSequence) {
    181         if (charSequence instanceof Spanned) {
    182             Spanned ss = (Spanned) charSequence;
    183             Object[] spans = ss.getSpans(0, ss.length(), Object.class);
    184             SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString());
    185             for (Object span : spans) {
    186                 Object resultSpan = span;
    187                 if (span instanceof TextAppearanceSpan) {
    188                     resultSpan = processTextAppearanceSpan((TextAppearanceSpan) span);
    189                 }
    190                 builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span),
    191                         ss.getSpanFlags(span));
    192             }
    193             return builder;
    194         }
    195         return charSequence;
    196     }
    197 
    198     private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) {
    199         ColorStateList colorStateList = span.getTextColor();
    200         if (colorStateList != null) {
    201             int[] colors = colorStateList.getColors();
    202             boolean changed = false;
    203             for (int i = 0; i < colors.length; i++) {
    204                 if (ImageUtils.isGrayscale(colors[i])) {
    205 
    206                     // Allocate a new array so we don't change the colors in the old color state
    207                     // list.
    208                     if (!changed) {
    209                         colors = Arrays.copyOf(colors, colors.length);
    210                     }
    211                     colors[i] = processColor(colors[i]);
    212                     changed = true;
    213                 }
    214             }
    215             if (changed) {
    216                 return new TextAppearanceSpan(
    217                         span.getFamily(), span.getTextStyle(), span.getTextSize(),
    218                         new ColorStateList(colorStateList.getStates(), colors),
    219                         span.getLinkTextColor());
    220             }
    221         }
    222         return span;
    223     }
    224 
    225     private int processColor(int color) {
    226         return Color.argb(Color.alpha(color),
    227                 255 - Color.red(color),
    228                 255 - Color.green(color),
    229                 255 - Color.blue(color));
    230     }
    231 
    232     /**
    233      * Finds a suitable color such that there's enough contrast.
    234      *
    235      * @param color the color to start searching from.
    236      * @param other the color to ensure contrast against. Assumed to be lighter than {@param color}
    237      * @param findFg if true, we assume {@param color} is a foreground, otherwise a background.
    238      * @param minRatio the minimum contrast ratio required.
    239      * @return a color with the same hue as {@param color}, potentially darkened to meet the
    240      *          contrast ratio.
    241      */
    242     private static int findContrastColor(int color, int other, boolean findFg, double minRatio) {
    243         int fg = findFg ? color : other;
    244         int bg = findFg ? other : color;
    245         if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) {
    246             return color;
    247         }
    248 
    249         double[] lab = new double[3];
    250         ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab);
    251 
    252         double low = 0, high = lab[0];
    253         final double a = lab[1], b = lab[2];
    254         for (int i = 0; i < 15 && high - low > 0.00001; i++) {
    255             final double l = (low + high) / 2;
    256             if (findFg) {
    257                 fg = ColorUtilsFromCompat.LABToColor(l, a, b);
    258             } else {
    259                 bg = ColorUtilsFromCompat.LABToColor(l, a, b);
    260             }
    261             if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) {
    262                 low = l;
    263             } else {
    264                 high = l;
    265             }
    266         }
    267         return ColorUtilsFromCompat.LABToColor(low, a, b);
    268     }
    269 
    270     /**
    271      * Finds a text color with sufficient contrast over bg that has the same hue as the original
    272      * color, assuming it is for large text.
    273      */
    274     public static int ensureLargeTextContrast(int color, int bg) {
    275         return findContrastColor(color, bg, true, 3);
    276     }
    277 
    278     /**
    279      * Finds a text color with sufficient contrast over bg that has the same hue as the original
    280      * color.
    281      */
    282     private static int ensureTextContrast(int color, int bg) {
    283         return findContrastColor(color, bg, true, 4.5);
    284     }
    285 
    286     /** Finds a background color for a text view with given text color and hint text color, that
    287      * has the same hue as the original color.
    288      */
    289     public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) {
    290         color = findContrastColor(color, hintColor, false, 3.0);
    291         return findContrastColor(color, textColor, false, 4.5);
    292     }
    293 
    294     private static String contrastChange(int colorOld, int colorNew, int bg) {
    295         return String.format("from %.2f:1 to %.2f:1",
    296                 ColorUtilsFromCompat.calculateContrast(colorOld, bg),
    297                 ColorUtilsFromCompat.calculateContrast(colorNew, bg));
    298     }
    299 
    300     /**
    301      * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT}
    302      */
    303     public static int resolveColor(Context context, int color) {
    304         if (color == Notification.COLOR_DEFAULT) {
    305             return context.getColor(com.android.internal.R.color.notification_icon_default_color);
    306         }
    307         return color;
    308     }
    309 
    310     /**
    311      * Resolves a Notification's color such that it has enough contrast to be used as the
    312      * color for the Notification's action and header text.
    313      *
    314      * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT}
    315      * @return a color of the same hue with enough contrast against the backgrounds.
    316      */
    317     public static int resolveContrastColor(Context context, int notificationColor) {
    318         final int resolvedColor = resolveColor(context, notificationColor);
    319 
    320         final int actionBg = context.getColor(
    321                 com.android.internal.R.color.notification_action_list);
    322         final int notiBg = context.getColor(
    323                 com.android.internal.R.color.notification_material_background_color);
    324 
    325         int color = resolvedColor;
    326         color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg);
    327         color = NotificationColorUtil.ensureTextContrast(color, notiBg);
    328 
    329         if (color != resolvedColor) {
    330             if (DEBUG){
    331                 Log.w(TAG, String.format(
    332                         "Enhanced contrast of notification for %s %s (over action)"
    333                                 + " and %s (over background) by changing #%s to %s",
    334                         context.getPackageName(),
    335                         NotificationColorUtil.contrastChange(resolvedColor, color, actionBg),
    336                         NotificationColorUtil.contrastChange(resolvedColor, color, notiBg),
    337                         Integer.toHexString(resolvedColor), Integer.toHexString(color)));
    338             }
    339         }
    340         return color;
    341     }
    342 
    343     /**
    344      * Lighten a color by a specified value
    345      * @param baseColor the base color to lighten
    346      * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L
    347      *               increase in the LAB color space.
    348      * @return the lightened color
    349      */
    350     public static int lightenColor(int baseColor, int amount) {
    351         final double[] result = ColorUtilsFromCompat.getTempDouble3Array();
    352         ColorUtilsFromCompat.colorToLAB(baseColor, result);
    353         result[0] = Math.min(100, result[0] + amount);
    354         return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]);
    355     }
    356 
    357     /**
    358      * Framework copy of functions needed from android.support.v4.graphics.ColorUtils.
    359      */
    360     private static class ColorUtilsFromCompat {
    361         private static final double XYZ_WHITE_REFERENCE_X = 95.047;
    362         private static final double XYZ_WHITE_REFERENCE_Y = 100;
    363         private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
    364         private static final double XYZ_EPSILON = 0.008856;
    365         private static final double XYZ_KAPPA = 903.3;
    366 
    367         private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
    368         private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
    369 
    370         private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
    371 
    372         private ColorUtilsFromCompat() {}
    373 
    374         /**
    375          * Composite two potentially translucent colors over each other and returns the result.
    376          */
    377         public static int compositeColors(@ColorInt int foreground, @ColorInt int background) {
    378             int bgAlpha = Color.alpha(background);
    379             int fgAlpha = Color.alpha(foreground);
    380             int a = compositeAlpha(fgAlpha, bgAlpha);
    381 
    382             int r = compositeComponent(Color.red(foreground), fgAlpha,
    383                     Color.red(background), bgAlpha, a);
    384             int g = compositeComponent(Color.green(foreground), fgAlpha,
    385                     Color.green(background), bgAlpha, a);
    386             int b = compositeComponent(Color.blue(foreground), fgAlpha,
    387                     Color.blue(background), bgAlpha, a);
    388 
    389             return Color.argb(a, r, g, b);
    390         }
    391 
    392         private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
    393             return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
    394         }
    395 
    396         private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
    397             if (a == 0) return 0;
    398             return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
    399         }
    400 
    401         /**
    402          * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
    403          * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
    404          */
    405         @FloatRange(from = 0.0, to = 1.0)
    406         public static double calculateLuminance(@ColorInt int color) {
    407             final double[] result = getTempDouble3Array();
    408             colorToXYZ(color, result);
    409             // Luminance is the Y component
    410             return result[1] / 100;
    411         }
    412 
    413         /**
    414          * Returns the contrast ratio between {@code foreground} and {@code background}.
    415          * {@code background} must be opaque.
    416          * <p>
    417          * Formula defined
    418          * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>.
    419          */
    420         public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) {
    421             if (Color.alpha(background) != 255) {
    422                 throw new IllegalArgumentException("background can not be translucent: #"
    423                         + Integer.toHexString(background));
    424             }
    425             if (Color.alpha(foreground) < 255) {
    426                 // If the foreground is translucent, composite the foreground over the background
    427                 foreground = compositeColors(foreground, background);
    428             }
    429 
    430             final double luminance1 = calculateLuminance(foreground) + 0.05;
    431             final double luminance2 = calculateLuminance(background) + 0.05;
    432 
    433             // Now return the lighter luminance divided by the darker luminance
    434             return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
    435         }
    436 
    437         /**
    438          * Convert the ARGB color to its CIE Lab representative components.
    439          *
    440          * @param color  the ARGB color to convert. The alpha component is ignored
    441          * @param outLab 3-element array which holds the resulting LAB components
    442          */
    443         public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) {
    444             RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab);
    445         }
    446 
    447         /**
    448          * Convert RGB components to its CIE Lab representative components.
    449          *
    450          * <ul>
    451          * <li>outLab[0] is L [0 ...100)</li>
    452          * <li>outLab[1] is a [-128...127)</li>
    453          * <li>outLab[2] is b [-128...127)</li>
    454          * </ul>
    455          *
    456          * @param r      red component value [0..255]
    457          * @param g      green component value [0..255]
    458          * @param b      blue component value [0..255]
    459          * @param outLab 3-element array which holds the resulting LAB components
    460          */
    461         public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
    462                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
    463                 @NonNull double[] outLab) {
    464             // First we convert RGB to XYZ
    465             RGBToXYZ(r, g, b, outLab);
    466             // outLab now contains XYZ
    467             XYZToLAB(outLab[0], outLab[1], outLab[2], outLab);
    468             // outLab now contains LAB representation
    469         }
    470 
    471         /**
    472          * Convert the ARGB color to it's CIE XYZ representative components.
    473          *
    474          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
    475          * 2 Standard Observer (1931).</p>
    476          *
    477          * <ul>
    478          * <li>outXyz[0] is X [0 ...95.047)</li>
    479          * <li>outXyz[1] is Y [0...100)</li>
    480          * <li>outXyz[2] is Z [0...108.883)</li>
    481          * </ul>
    482          *
    483          * @param color  the ARGB color to convert. The alpha component is ignored
    484          * @param outXyz 3-element array which holds the resulting LAB components
    485          */
    486         public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) {
    487             RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz);
    488         }
    489 
    490         /**
    491          * Convert RGB components to it's CIE XYZ representative components.
    492          *
    493          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
    494          * 2 Standard Observer (1931).</p>
    495          *
    496          * <ul>
    497          * <li>outXyz[0] is X [0 ...95.047)</li>
    498          * <li>outXyz[1] is Y [0...100)</li>
    499          * <li>outXyz[2] is Z [0...108.883)</li>
    500          * </ul>
    501          *
    502          * @param r      red component value [0..255]
    503          * @param g      green component value [0..255]
    504          * @param b      blue component value [0..255]
    505          * @param outXyz 3-element array which holds the resulting XYZ components
    506          */
    507         public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
    508                 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
    509                 @NonNull double[] outXyz) {
    510             if (outXyz.length != 3) {
    511                 throw new IllegalArgumentException("outXyz must have a length of 3.");
    512             }
    513 
    514             double sr = r / 255.0;
    515             sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
    516             double sg = g / 255.0;
    517             sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
    518             double sb = b / 255.0;
    519             sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
    520 
    521             outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
    522             outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
    523             outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
    524         }
    525 
    526         /**
    527          * Converts a color from CIE XYZ to CIE Lab representation.
    528          *
    529          * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
    530          * 2 Standard Observer (1931).</p>
    531          *
    532          * <ul>
    533          * <li>outLab[0] is L [0 ...100)</li>
    534          * <li>outLab[1] is a [-128...127)</li>
    535          * <li>outLab[2] is b [-128...127)</li>
    536          * </ul>
    537          *
    538          * @param x      X component value [0...95.047)
    539          * @param y      Y component value [0...100)
    540          * @param z      Z component value [0...108.883)
    541          * @param outLab 3-element array which holds the resulting Lab components
    542          */
    543         public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
    544                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
    545                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
    546                 @NonNull double[] outLab) {
    547             if (outLab.length != 3) {
    548                 throw new IllegalArgumentException("outLab must have a length of 3.");
    549             }
    550             x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
    551             y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
    552             z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
    553             outLab[0] = Math.max(0, 116 * y - 16);
    554             outLab[1] = 500 * (x - y);
    555             outLab[2] = 200 * (y - z);
    556         }
    557 
    558         /**
    559          * Converts a color from CIE Lab to CIE XYZ representation.
    560          *
    561          * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
    562          * 2 Standard Observer (1931).</p>
    563          *
    564          * <ul>
    565          * <li>outXyz[0] is X [0 ...95.047)</li>
    566          * <li>outXyz[1] is Y [0...100)</li>
    567          * <li>outXyz[2] is Z [0...108.883)</li>
    568          * </ul>
    569          *
    570          * @param l      L component value [0...100)
    571          * @param a      A component value [-128...127)
    572          * @param b      B component value [-128...127)
    573          * @param outXyz 3-element array which holds the resulting XYZ components
    574          */
    575         public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
    576                 @FloatRange(from = -128, to = 127) final double a,
    577                 @FloatRange(from = -128, to = 127) final double b,
    578                 @NonNull double[] outXyz) {
    579             final double fy = (l + 16) / 116;
    580             final double fx = a / 500 + fy;
    581             final double fz = fy - b / 200;
    582 
    583             double tmp = Math.pow(fx, 3);
    584             final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
    585             final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
    586 
    587             tmp = Math.pow(fz, 3);
    588             final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
    589 
    590             outXyz[0] = xr * XYZ_WHITE_REFERENCE_X;
    591             outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y;
    592             outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z;
    593         }
    594 
    595         /**
    596          * Converts a color from CIE XYZ to its RGB representation.
    597          *
    598          * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
    599          * 2 Standard Observer (1931).</p>
    600          *
    601          * @param x X component value [0...95.047)
    602          * @param y Y component value [0...100)
    603          * @param z Z component value [0...108.883)
    604          * @return int containing the RGB representation
    605          */
    606         @ColorInt
    607         public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
    608                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
    609                 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
    610             double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
    611             double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
    612             double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
    613 
    614             r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
    615             g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
    616             b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
    617 
    618             return Color.rgb(
    619                     constrain((int) Math.round(r * 255), 0, 255),
    620                     constrain((int) Math.round(g * 255), 0, 255),
    621                     constrain((int) Math.round(b * 255), 0, 255));
    622         }
    623 
    624         /**
    625          * Converts a color from CIE Lab to its RGB representation.
    626          *
    627          * @param l L component value [0...100]
    628          * @param a A component value [-128...127]
    629          * @param b B component value [-128...127]
    630          * @return int containing the RGB representation
    631          */
    632         @ColorInt
    633         public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
    634                 @FloatRange(from = -128, to = 127) final double a,
    635                 @FloatRange(from = -128, to = 127) final double b) {
    636             final double[] result = getTempDouble3Array();
    637             LABToXYZ(l, a, b, result);
    638             return XYZToColor(result[0], result[1], result[2]);
    639         }
    640 
    641         private static int constrain(int amount, int low, int high) {
    642             return amount < low ? low : (amount > high ? high : amount);
    643         }
    644 
    645         private static double pivotXyzComponent(double component) {
    646             return component > XYZ_EPSILON
    647                     ? Math.pow(component, 1 / 3.0)
    648                     : (XYZ_KAPPA * component + 16) / 116;
    649         }
    650 
    651         public static double[] getTempDouble3Array() {
    652             double[] result = TEMP_ARRAY.get();
    653             if (result == null) {
    654                 result = new double[3];
    655                 TEMP_ARRAY.set(result);
    656             }
    657             return result;
    658         }
    659 
    660     }
    661 }
    662