NotificationColorUtil.java revision 487374fd6d1c61b49734ce1857824646be83b587
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.TextAppearanceSpan; 37import android.util.Log; 38import android.util.Pair; 39 40import java.util.Arrays; 41import java.util.WeakHashMap; 42 43/** 44 * Helper class to process legacy (Holo) notifications to make them look like material notifications. 45 * 46 * @hide 47 */ 48public 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 suitable color such that there's enough contrast. 272 * 273 * @param color the color to start searching from. 274 * @param other the color to ensure contrast against. Assumed to be darker than {@param color} 275 * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. 276 * @param minRatio the minimum contrast ratio required. 277 * @return a color with the same hue as {@param color}, potentially darkened to meet the 278 * contrast ratio. 279 */ 280 public static int findContrastColorAgainstDark(int color, int other, boolean findFg, 281 double minRatio) { 282 int fg = findFg ? color : other; 283 int bg = findFg ? other : color; 284 if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { 285 return color; 286 } 287 288 double[] lab = new double[3]; 289 ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab); 290 291 double low = lab[0], high = 100; 292 final double a = lab[1], b = lab[2]; 293 for (int i = 0; i < 15 && high - low > 0.00001; i++) { 294 final double l = (low + high) / 2; 295 if (findFg) { 296 fg = ColorUtilsFromCompat.LABToColor(l, a, b); 297 } else { 298 bg = ColorUtilsFromCompat.LABToColor(l, a, b); 299 } 300 if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { 301 high = l; 302 } else { 303 low = l; 304 } 305 } 306 return ColorUtilsFromCompat.LABToColor(high, a, b); 307 } 308 309 public static int ensureTextContrastOnBlack(int color) { 310 return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12); 311 } 312 313 /** 314 * Finds a text color with sufficient contrast over bg that has the same hue as the original 315 * color, assuming it is for large text. 316 */ 317 public static int ensureLargeTextContrast(int color, int bg) { 318 return findContrastColor(color, bg, true, 3); 319 } 320 321 /** 322 * Finds a text color with sufficient contrast over bg that has the same hue as the original 323 * color. 324 */ 325 private static int ensureTextContrast(int color, int bg) { 326 return findContrastColor(color, bg, true, 4.5); 327 } 328 329 /** Finds a background color for a text view with given text color and hint text color, that 330 * has the same hue as the original color. 331 */ 332 public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) { 333 color = findContrastColor(color, hintColor, false, 3.0); 334 return findContrastColor(color, textColor, false, 4.5); 335 } 336 337 private static String contrastChange(int colorOld, int colorNew, int bg) { 338 return String.format("from %.2f:1 to %.2f:1", 339 ColorUtilsFromCompat.calculateContrast(colorOld, bg), 340 ColorUtilsFromCompat.calculateContrast(colorNew, bg)); 341 } 342 343 /** 344 * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} 345 */ 346 public static int resolveColor(Context context, int color) { 347 if (color == Notification.COLOR_DEFAULT) { 348 return context.getColor(com.android.internal.R.color.notification_icon_default_color); 349 } 350 return color; 351 } 352 353 /** 354 * Resolves a Notification's color such that it has enough contrast to be used as the 355 * color for the Notification's action and header text. 356 * 357 * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT} 358 * @return a color of the same hue with enough contrast against the backgrounds. 359 */ 360 public static int resolveContrastColor(Context context, int notificationColor) { 361 final int resolvedColor = resolveColor(context, notificationColor); 362 363 final int actionBg = context.getColor( 364 com.android.internal.R.color.notification_action_list); 365 final int notiBg = context.getColor( 366 com.android.internal.R.color.notification_material_background_color); 367 368 int color = resolvedColor; 369 color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg); 370 color = NotificationColorUtil.ensureTextContrast(color, notiBg); 371 372 if (color != resolvedColor) { 373 if (DEBUG){ 374 Log.w(TAG, String.format( 375 "Enhanced contrast of notification for %s %s (over action)" 376 + " and %s (over background) by changing #%s to %s", 377 context.getPackageName(), 378 NotificationColorUtil.contrastChange(resolvedColor, color, actionBg), 379 NotificationColorUtil.contrastChange(resolvedColor, color, notiBg), 380 Integer.toHexString(resolvedColor), Integer.toHexString(color))); 381 } 382 } 383 return color; 384 } 385 386 /** 387 * Lighten a color by a specified value 388 * @param baseColor the base color to lighten 389 * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L 390 * increase in the LAB color space. 391 * @return the lightened color 392 */ 393 public static int lightenColor(int baseColor, int amount) { 394 final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); 395 ColorUtilsFromCompat.colorToLAB(baseColor, result); 396 result[0] = Math.min(100, result[0] + amount); 397 return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); 398 } 399 400 public static int resolveAmbientColor(Context context, int notificationColor) { 401 final int resolvedColor = resolveColor(context, notificationColor); 402 403 int color = resolvedColor; 404 color = NotificationColorUtil.ensureTextContrastOnBlack(color); 405 406 if (color != resolvedColor) { 407 if (DEBUG){ 408 Log.w(TAG, String.format( 409 "Ambient contrast of notification for %s is %s (over black)" 410 + " by changing #%s to #%s", 411 context.getPackageName(), 412 NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK), 413 Integer.toHexString(resolvedColor), Integer.toHexString(color))); 414 } 415 } 416 return color; 417 } 418 419 /** 420 * Framework copy of functions needed from android.support.v4.graphics.ColorUtils. 421 */ 422 private static class ColorUtilsFromCompat { 423 private static final double XYZ_WHITE_REFERENCE_X = 95.047; 424 private static final double XYZ_WHITE_REFERENCE_Y = 100; 425 private static final double XYZ_WHITE_REFERENCE_Z = 108.883; 426 private static final double XYZ_EPSILON = 0.008856; 427 private static final double XYZ_KAPPA = 903.3; 428 429 private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; 430 private static final int MIN_ALPHA_SEARCH_PRECISION = 1; 431 432 private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>(); 433 434 private ColorUtilsFromCompat() {} 435 436 /** 437 * Composite two potentially translucent colors over each other and returns the result. 438 */ 439 public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { 440 int bgAlpha = Color.alpha(background); 441 int fgAlpha = Color.alpha(foreground); 442 int a = compositeAlpha(fgAlpha, bgAlpha); 443 444 int r = compositeComponent(Color.red(foreground), fgAlpha, 445 Color.red(background), bgAlpha, a); 446 int g = compositeComponent(Color.green(foreground), fgAlpha, 447 Color.green(background), bgAlpha, a); 448 int b = compositeComponent(Color.blue(foreground), fgAlpha, 449 Color.blue(background), bgAlpha, a); 450 451 return Color.argb(a, r, g, b); 452 } 453 454 private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { 455 return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); 456 } 457 458 private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { 459 if (a == 0) return 0; 460 return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); 461 } 462 463 /** 464 * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. 465 * <p>Defined as the Y component in the XYZ representation of {@code color}.</p> 466 */ 467 @FloatRange(from = 0.0, to = 1.0) 468 public static double calculateLuminance(@ColorInt int color) { 469 final double[] result = getTempDouble3Array(); 470 colorToXYZ(color, result); 471 // Luminance is the Y component 472 return result[1] / 100; 473 } 474 475 /** 476 * Returns the contrast ratio between {@code foreground} and {@code background}. 477 * {@code background} must be opaque. 478 * <p> 479 * Formula defined 480 * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef">here</a>. 481 */ 482 public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) { 483 if (Color.alpha(background) != 255) { 484 throw new IllegalArgumentException("background can not be translucent: #" 485 + Integer.toHexString(background)); 486 } 487 if (Color.alpha(foreground) < 255) { 488 // If the foreground is translucent, composite the foreground over the background 489 foreground = compositeColors(foreground, background); 490 } 491 492 final double luminance1 = calculateLuminance(foreground) + 0.05; 493 final double luminance2 = calculateLuminance(background) + 0.05; 494 495 // Now return the lighter luminance divided by the darker luminance 496 return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); 497 } 498 499 /** 500 * Convert the ARGB color to its CIE Lab representative components. 501 * 502 * @param color the ARGB color to convert. The alpha component is ignored 503 * @param outLab 3-element array which holds the resulting LAB components 504 */ 505 public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) { 506 RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab); 507 } 508 509 /** 510 * Convert RGB components to its CIE Lab representative components. 511 * 512 * <ul> 513 * <li>outLab[0] is L [0 ...100)</li> 514 * <li>outLab[1] is a [-128...127)</li> 515 * <li>outLab[2] is b [-128...127)</li> 516 * </ul> 517 * 518 * @param r red component value [0..255] 519 * @param g green component value [0..255] 520 * @param b blue component value [0..255] 521 * @param outLab 3-element array which holds the resulting LAB components 522 */ 523 public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r, 524 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 525 @NonNull double[] outLab) { 526 // First we convert RGB to XYZ 527 RGBToXYZ(r, g, b, outLab); 528 // outLab now contains XYZ 529 XYZToLAB(outLab[0], outLab[1], outLab[2], outLab); 530 // outLab now contains LAB representation 531 } 532 533 /** 534 * Convert the ARGB color to it's CIE XYZ representative components. 535 * 536 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 537 * 2° Standard Observer (1931).</p> 538 * 539 * <ul> 540 * <li>outXyz[0] is X [0 ...95.047)</li> 541 * <li>outXyz[1] is Y [0...100)</li> 542 * <li>outXyz[2] is Z [0...108.883)</li> 543 * </ul> 544 * 545 * @param color the ARGB color to convert. The alpha component is ignored 546 * @param outXyz 3-element array which holds the resulting LAB components 547 */ 548 public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { 549 RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); 550 } 551 552 /** 553 * Convert RGB components to it's CIE XYZ representative components. 554 * 555 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 556 * 2° Standard Observer (1931).</p> 557 * 558 * <ul> 559 * <li>outXyz[0] is X [0 ...95.047)</li> 560 * <li>outXyz[1] is Y [0...100)</li> 561 * <li>outXyz[2] is Z [0...108.883)</li> 562 * </ul> 563 * 564 * @param r red component value [0..255] 565 * @param g green component value [0..255] 566 * @param b blue component value [0..255] 567 * @param outXyz 3-element array which holds the resulting XYZ components 568 */ 569 public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r, 570 @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 571 @NonNull double[] outXyz) { 572 if (outXyz.length != 3) { 573 throw new IllegalArgumentException("outXyz must have a length of 3."); 574 } 575 576 double sr = r / 255.0; 577 sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); 578 double sg = g / 255.0; 579 sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); 580 double sb = b / 255.0; 581 sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); 582 583 outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805); 584 outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722); 585 outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505); 586 } 587 588 /** 589 * Converts a color from CIE XYZ to CIE Lab representation. 590 * 591 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 592 * 2° Standard Observer (1931).</p> 593 * 594 * <ul> 595 * <li>outLab[0] is L [0 ...100)</li> 596 * <li>outLab[1] is a [-128...127)</li> 597 * <li>outLab[2] is b [-128...127)</li> 598 * </ul> 599 * 600 * @param x X component value [0...95.047) 601 * @param y Y component value [0...100) 602 * @param z Z component value [0...108.883) 603 * @param outLab 3-element array which holds the resulting Lab components 604 */ 605 public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 606 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 607 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z, 608 @NonNull double[] outLab) { 609 if (outLab.length != 3) { 610 throw new IllegalArgumentException("outLab must have a length of 3."); 611 } 612 x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X); 613 y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y); 614 z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z); 615 outLab[0] = Math.max(0, 116 * y - 16); 616 outLab[1] = 500 * (x - y); 617 outLab[2] = 200 * (y - z); 618 } 619 620 /** 621 * Converts a color from CIE Lab to CIE XYZ representation. 622 * 623 * <p>The resulting XYZ representation will use the D65 illuminant and the CIE 624 * 2° Standard Observer (1931).</p> 625 * 626 * <ul> 627 * <li>outXyz[0] is X [0 ...95.047)</li> 628 * <li>outXyz[1] is Y [0...100)</li> 629 * <li>outXyz[2] is Z [0...108.883)</li> 630 * </ul> 631 * 632 * @param l L component value [0...100) 633 * @param a A component value [-128...127) 634 * @param b B component value [-128...127) 635 * @param outXyz 3-element array which holds the resulting XYZ components 636 */ 637 public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l, 638 @FloatRange(from = -128, to = 127) final double a, 639 @FloatRange(from = -128, to = 127) final double b, 640 @NonNull double[] outXyz) { 641 final double fy = (l + 16) / 116; 642 final double fx = a / 500 + fy; 643 final double fz = fy - b / 200; 644 645 double tmp = Math.pow(fx, 3); 646 final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA; 647 final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA; 648 649 tmp = Math.pow(fz, 3); 650 final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA; 651 652 outXyz[0] = xr * XYZ_WHITE_REFERENCE_X; 653 outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y; 654 outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z; 655 } 656 657 /** 658 * Converts a color from CIE XYZ to its RGB representation. 659 * 660 * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE 661 * 2° Standard Observer (1931).</p> 662 * 663 * @param x X component value [0...95.047) 664 * @param y Y component value [0...100) 665 * @param z Z component value [0...108.883) 666 * @return int containing the RGB representation 667 */ 668 @ColorInt 669 public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 670 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 671 @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) { 672 double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100; 673 double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100; 674 double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100; 675 676 r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; 677 g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; 678 b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; 679 680 return Color.rgb( 681 constrain((int) Math.round(r * 255), 0, 255), 682 constrain((int) Math.round(g * 255), 0, 255), 683 constrain((int) Math.round(b * 255), 0, 255)); 684 } 685 686 /** 687 * Converts a color from CIE Lab to its RGB representation. 688 * 689 * @param l L component value [0...100] 690 * @param a A component value [-128...127] 691 * @param b B component value [-128...127] 692 * @return int containing the RGB representation 693 */ 694 @ColorInt 695 public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l, 696 @FloatRange(from = -128, to = 127) final double a, 697 @FloatRange(from = -128, to = 127) final double b) { 698 final double[] result = getTempDouble3Array(); 699 LABToXYZ(l, a, b, result); 700 return XYZToColor(result[0], result[1], result[2]); 701 } 702 703 private static int constrain(int amount, int low, int high) { 704 return amount < low ? low : (amount > high ? high : amount); 705 } 706 707 private static double pivotXyzComponent(double component) { 708 return component > XYZ_EPSILON 709 ? Math.pow(component, 1 / 3.0) 710 : (XYZ_KAPPA * component + 16) / 116; 711 } 712 713 public static double[] getTempDouble3Array() { 714 double[] result = TEMP_ARRAY.get(); 715 if (result == null) { 716 result = new double[3]; 717 TEMP_ARRAY.set(result); 718 } 719 return result; 720 } 721 722 } 723} 724