1/* 2 * Copyright 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.graphics; 18 19import android.graphics.Bitmap; 20import android.graphics.Color; 21import android.graphics.Rect; 22import android.os.AsyncTask; 23import android.support.annotation.ColorInt; 24import android.support.annotation.NonNull; 25import android.support.annotation.Nullable; 26import android.support.v4.graphics.ColorUtils; 27import android.support.v4.util.ArrayMap; 28import android.util.Log; 29import android.util.SparseBooleanArray; 30import android.util.TimingLogger; 31 32import java.util.ArrayList; 33import java.util.Arrays; 34import java.util.Collections; 35import java.util.List; 36import java.util.Map; 37 38/** 39 * A helper class to extract prominent colors from an image. 40 * <p> 41 * A number of colors with different profiles are extracted from the image: 42 * <ul> 43 * <li>Vibrant</li> 44 * <li>Vibrant Dark</li> 45 * <li>Vibrant Light</li> 46 * <li>Muted</li> 47 * <li>Muted Dark</li> 48 * <li>Muted Light</li> 49 * </ul> 50 * These can be retrieved from the appropriate getter method. 51 * 52 * <p> 53 * Instances are created with a {@link Builder} which supports several options to tweak the 54 * generated Palette. See that class' documentation for more information. 55 * <p> 56 * Generation should always be completed on a background thread, ideally the one in 57 * which you load your image on. {@link Builder} supports both synchronous and asynchronous 58 * generation: 59 * 60 * <pre> 61 * // Synchronous 62 * Palette p = Palette.from(bitmap).generate(); 63 * 64 * // Asynchronous 65 * Palette.from(bitmap).generate(new PaletteAsyncListener() { 66 * public void onGenerated(Palette p) { 67 * // Use generated instance 68 * } 69 * }); 70 * </pre> 71 */ 72public final class Palette { 73 74 /** 75 * Listener to be used with {@link #generateAsync(Bitmap, PaletteAsyncListener)} or 76 * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)} 77 */ 78 public interface PaletteAsyncListener { 79 80 /** 81 * Called when the {@link Palette} has been generated. 82 */ 83 void onGenerated(Palette palette); 84 } 85 86 static final int DEFAULT_RESIZE_BITMAP_AREA = 112 * 112; 87 static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16; 88 89 static final float MIN_CONTRAST_TITLE_TEXT = 3.0f; 90 static final float MIN_CONTRAST_BODY_TEXT = 4.5f; 91 92 static final String LOG_TAG = "Palette"; 93 static final boolean LOG_TIMINGS = false; 94 95 /** 96 * Start generating a {@link Palette} with the returned {@link Builder} instance. 97 */ 98 public static Builder from(Bitmap bitmap) { 99 return new Builder(bitmap); 100 } 101 102 /** 103 * Generate a {@link Palette} from the pre-generated list of {@link Palette.Swatch} swatches. 104 * This is useful for testing, or if you want to resurrect a {@link Palette} instance from a 105 * list of swatches. Will return null if the {@code swatches} is null. 106 */ 107 public static Palette from(List<Swatch> swatches) { 108 return new Builder(swatches).generate(); 109 } 110 111 /** 112 * @deprecated Use {@link Builder} to generate the Palette. 113 */ 114 @Deprecated 115 public static Palette generate(Bitmap bitmap) { 116 return from(bitmap).generate(); 117 } 118 119 /** 120 * @deprecated Use {@link Builder} to generate the Palette. 121 */ 122 @Deprecated 123 public static Palette generate(Bitmap bitmap, int numColors) { 124 return from(bitmap).maximumColorCount(numColors).generate(); 125 } 126 127 /** 128 * @deprecated Use {@link Builder} to generate the Palette. 129 */ 130 @Deprecated 131 public static AsyncTask<Bitmap, Void, Palette> generateAsync( 132 Bitmap bitmap, PaletteAsyncListener listener) { 133 return from(bitmap).generate(listener); 134 } 135 136 /** 137 * @deprecated Use {@link Builder} to generate the Palette. 138 */ 139 @Deprecated 140 public static AsyncTask<Bitmap, Void, Palette> generateAsync( 141 final Bitmap bitmap, final int numColors, final PaletteAsyncListener listener) { 142 return from(bitmap).maximumColorCount(numColors).generate(listener); 143 } 144 145 private final List<Swatch> mSwatches; 146 private final List<Target> mTargets; 147 148 private final Map<Target, Swatch> mSelectedSwatches; 149 private final SparseBooleanArray mUsedColors; 150 151 private final Swatch mDominantSwatch; 152 153 Palette(List<Swatch> swatches, List<Target> targets) { 154 mSwatches = swatches; 155 mTargets = targets; 156 157 mUsedColors = new SparseBooleanArray(); 158 mSelectedSwatches = new ArrayMap<>(); 159 160 mDominantSwatch = findDominantSwatch(); 161 } 162 163 /** 164 * Returns all of the swatches which make up the palette. 165 */ 166 @NonNull 167 public List<Swatch> getSwatches() { 168 return Collections.unmodifiableList(mSwatches); 169 } 170 171 /** 172 * Returns the targets used to generate this palette. 173 */ 174 @NonNull 175 public List<Target> getTargets() { 176 return Collections.unmodifiableList(mTargets); 177 } 178 179 /** 180 * Returns the most vibrant swatch in the palette. Might be null. 181 * 182 * @see Target#VIBRANT 183 */ 184 @Nullable 185 public Swatch getVibrantSwatch() { 186 return getSwatchForTarget(Target.VIBRANT); 187 } 188 189 /** 190 * Returns a light and vibrant swatch from the palette. Might be null. 191 * 192 * @see Target#LIGHT_VIBRANT 193 */ 194 @Nullable 195 public Swatch getLightVibrantSwatch() { 196 return getSwatchForTarget(Target.LIGHT_VIBRANT); 197 } 198 199 /** 200 * Returns a dark and vibrant swatch from the palette. Might be null. 201 * 202 * @see Target#DARK_VIBRANT 203 */ 204 @Nullable 205 public Swatch getDarkVibrantSwatch() { 206 return getSwatchForTarget(Target.DARK_VIBRANT); 207 } 208 209 /** 210 * Returns a muted swatch from the palette. Might be null. 211 * 212 * @see Target#MUTED 213 */ 214 @Nullable 215 public Swatch getMutedSwatch() { 216 return getSwatchForTarget(Target.MUTED); 217 } 218 219 /** 220 * Returns a muted and light swatch from the palette. Might be null. 221 * 222 * @see Target#LIGHT_MUTED 223 */ 224 @Nullable 225 public Swatch getLightMutedSwatch() { 226 return getSwatchForTarget(Target.LIGHT_MUTED); 227 } 228 229 /** 230 * Returns a muted and dark swatch from the palette. Might be null. 231 * 232 * @see Target#DARK_MUTED 233 */ 234 @Nullable 235 public Swatch getDarkMutedSwatch() { 236 return getSwatchForTarget(Target.DARK_MUTED); 237 } 238 239 /** 240 * Returns the most vibrant color in the palette as an RGB packed int. 241 * 242 * @param defaultColor value to return if the swatch isn't available 243 * @see #getVibrantSwatch() 244 */ 245 @ColorInt 246 public int getVibrantColor(@ColorInt final int defaultColor) { 247 return getColorForTarget(Target.VIBRANT, defaultColor); 248 } 249 250 /** 251 * Returns a light and vibrant color from the palette as an RGB packed int. 252 * 253 * @param defaultColor value to return if the swatch isn't available 254 * @see #getLightVibrantSwatch() 255 */ 256 @ColorInt 257 public int getLightVibrantColor(@ColorInt final int defaultColor) { 258 return getColorForTarget(Target.LIGHT_VIBRANT, defaultColor); 259 } 260 261 /** 262 * Returns a dark and vibrant color from the palette as an RGB packed int. 263 * 264 * @param defaultColor value to return if the swatch isn't available 265 * @see #getDarkVibrantSwatch() 266 */ 267 @ColorInt 268 public int getDarkVibrantColor(@ColorInt final int defaultColor) { 269 return getColorForTarget(Target.DARK_VIBRANT, defaultColor); 270 } 271 272 /** 273 * Returns a muted color from the palette as an RGB packed int. 274 * 275 * @param defaultColor value to return if the swatch isn't available 276 * @see #getMutedSwatch() 277 */ 278 @ColorInt 279 public int getMutedColor(@ColorInt final int defaultColor) { 280 return getColorForTarget(Target.MUTED, defaultColor); 281 } 282 283 /** 284 * Returns a muted and light color from the palette as an RGB packed int. 285 * 286 * @param defaultColor value to return if the swatch isn't available 287 * @see #getLightMutedSwatch() 288 */ 289 @ColorInt 290 public int getLightMutedColor(@ColorInt final int defaultColor) { 291 return getColorForTarget(Target.LIGHT_MUTED, defaultColor); 292 } 293 294 /** 295 * Returns a muted and dark color from the palette as an RGB packed int. 296 * 297 * @param defaultColor value to return if the swatch isn't available 298 * @see #getDarkMutedSwatch() 299 */ 300 @ColorInt 301 public int getDarkMutedColor(@ColorInt final int defaultColor) { 302 return getColorForTarget(Target.DARK_MUTED, defaultColor); 303 } 304 305 /** 306 * Returns the selected swatch for the given target from the palette, or {@code null} if one 307 * could not be found. 308 */ 309 @Nullable 310 public Swatch getSwatchForTarget(@NonNull final Target target) { 311 return mSelectedSwatches.get(target); 312 } 313 314 /** 315 * Returns the selected color for the given target from the palette as an RGB packed int. 316 * 317 * @param defaultColor value to return if the swatch isn't available 318 */ 319 @ColorInt 320 public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) { 321 Swatch swatch = getSwatchForTarget(target); 322 return swatch != null ? swatch.getRgb() : defaultColor; 323 } 324 325 /** 326 * Returns the dominant swatch from the palette. 327 * 328 * <p>The dominant swatch is defined as the swatch with the greatest population (frequency) 329 * within the palette.</p> 330 */ 331 @Nullable 332 public Swatch getDominantSwatch() { 333 return mDominantSwatch; 334 } 335 336 /** 337 * Returns the color of the dominant swatch from the palette, as an RGB packed int. 338 * 339 * @param defaultColor value to return if the swatch isn't available 340 * @see #getDominantSwatch() 341 */ 342 @ColorInt 343 public int getDominantColor(@ColorInt int defaultColor) { 344 return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor; 345 } 346 347 void generate() { 348 // We need to make sure that the scored targets are generated first. This is so that 349 // inherited targets have something to inherit from 350 for (int i = 0, count = mTargets.size(); i < count; i++) { 351 final Target target = mTargets.get(i); 352 target.normalizeWeights(); 353 mSelectedSwatches.put(target, generateScoredTarget(target)); 354 } 355 // We now clear out the used colors 356 mUsedColors.clear(); 357 } 358 359 private Swatch generateScoredTarget(final Target target) { 360 final Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target); 361 if (maxScoreSwatch != null && target.isExclusive()) { 362 // If we have a swatch, and the target is exclusive, add the color to the used list 363 mUsedColors.append(maxScoreSwatch.getRgb(), true); 364 } 365 return maxScoreSwatch; 366 } 367 368 private Swatch getMaxScoredSwatchForTarget(final Target target) { 369 float maxScore = 0; 370 Swatch maxScoreSwatch = null; 371 for (int i = 0, count = mSwatches.size(); i < count; i++) { 372 final Swatch swatch = mSwatches.get(i); 373 if (shouldBeScoredForTarget(swatch, target)) { 374 final float score = generateScore(swatch, target); 375 if (maxScoreSwatch == null || score > maxScore) { 376 maxScoreSwatch = swatch; 377 maxScore = score; 378 } 379 } 380 } 381 return maxScoreSwatch; 382 } 383 384 private boolean shouldBeScoredForTarget(final Swatch swatch, final Target target) { 385 // Check whether the HSL values are within the correct ranges, and this color hasn't 386 // been used yet. 387 final float hsl[] = swatch.getHsl(); 388 return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation() 389 && hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness() 390 && !mUsedColors.get(swatch.getRgb()); 391 } 392 393 private float generateScore(Swatch swatch, Target target) { 394 final float[] hsl = swatch.getHsl(); 395 396 float saturationScore = 0; 397 float luminanceScore = 0; 398 float populationScore = 0; 399 400 final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1; 401 402 if (target.getSaturationWeight() > 0) { 403 saturationScore = target.getSaturationWeight() 404 * (1f - Math.abs(hsl[1] - target.getTargetSaturation())); 405 } 406 if (target.getLightnessWeight() > 0) { 407 luminanceScore = target.getLightnessWeight() 408 * (1f - Math.abs(hsl[2] - target.getTargetLightness())); 409 } 410 if (target.getPopulationWeight() > 0) { 411 populationScore = target.getPopulationWeight() 412 * (swatch.getPopulation() / (float) maxPopulation); 413 } 414 415 return saturationScore + luminanceScore + populationScore; 416 } 417 418 private Swatch findDominantSwatch() { 419 int maxPop = Integer.MIN_VALUE; 420 Swatch maxSwatch = null; 421 for (int i = 0, count = mSwatches.size(); i < count; i++) { 422 Swatch swatch = mSwatches.get(i); 423 if (swatch.getPopulation() > maxPop) { 424 maxSwatch = swatch; 425 maxPop = swatch.getPopulation(); 426 } 427 } 428 return maxSwatch; 429 } 430 431 private static float[] copyHslValues(Swatch color) { 432 final float[] newHsl = new float[3]; 433 System.arraycopy(color.getHsl(), 0, newHsl, 0, 3); 434 return newHsl; 435 } 436 437 /** 438 * Represents a color swatch generated from an image's palette. The RGB color can be retrieved 439 * by calling {@link #getRgb()}. 440 */ 441 public static final class Swatch { 442 private final int mRed, mGreen, mBlue; 443 private final int mRgb; 444 private final int mPopulation; 445 446 private boolean mGeneratedTextColors; 447 private int mTitleTextColor; 448 private int mBodyTextColor; 449 450 private float[] mHsl; 451 452 public Swatch(@ColorInt int color, int population) { 453 mRed = Color.red(color); 454 mGreen = Color.green(color); 455 mBlue = Color.blue(color); 456 mRgb = color; 457 mPopulation = population; 458 } 459 460 Swatch(int red, int green, int blue, int population) { 461 mRed = red; 462 mGreen = green; 463 mBlue = blue; 464 mRgb = Color.rgb(red, green, blue); 465 mPopulation = population; 466 } 467 468 Swatch(float[] hsl, int population) { 469 this(ColorUtils.HSLToColor(hsl), population); 470 mHsl = hsl; 471 } 472 473 /** 474 * @return this swatch's RGB color value 475 */ 476 @ColorInt 477 public int getRgb() { 478 return mRgb; 479 } 480 481 /** 482 * Return this swatch's HSL values. 483 * hsv[0] is Hue [0 .. 360) 484 * hsv[1] is Saturation [0...1] 485 * hsv[2] is Lightness [0...1] 486 */ 487 public float[] getHsl() { 488 if (mHsl == null) { 489 mHsl = new float[3]; 490 } 491 ColorUtils.RGBToHSL(mRed, mGreen, mBlue, mHsl); 492 return mHsl; 493 } 494 495 /** 496 * @return the number of pixels represented by this swatch 497 */ 498 public int getPopulation() { 499 return mPopulation; 500 } 501 502 /** 503 * Returns an appropriate color to use for any 'title' text which is displayed over this 504 * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast. 505 */ 506 @ColorInt 507 public int getTitleTextColor() { 508 ensureTextColorsGenerated(); 509 return mTitleTextColor; 510 } 511 512 /** 513 * Returns an appropriate color to use for any 'body' text which is displayed over this 514 * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast. 515 */ 516 @ColorInt 517 public int getBodyTextColor() { 518 ensureTextColorsGenerated(); 519 return mBodyTextColor; 520 } 521 522 private void ensureTextColorsGenerated() { 523 if (!mGeneratedTextColors) { 524 // First check white, as most colors will be dark 525 final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha( 526 Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT); 527 final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha( 528 Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT); 529 530 if (lightBodyAlpha != -1 && lightTitleAlpha != -1) { 531 // If we found valid light values, use them and return 532 mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha); 533 mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha); 534 mGeneratedTextColors = true; 535 return; 536 } 537 538 final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha( 539 Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT); 540 final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha( 541 Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT); 542 543 if (darkBodyAlpha != -1 && darkTitleAlpha != -1) { 544 // If we found valid dark values, use them and return 545 mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); 546 mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); 547 mGeneratedTextColors = true; 548 return; 549 } 550 551 // If we reach here then we can not find title and body values which use the same 552 // lightness, we need to use mismatched values 553 mBodyTextColor = lightBodyAlpha != -1 554 ? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha) 555 : ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha); 556 mTitleTextColor = lightTitleAlpha != -1 557 ? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha) 558 : ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha); 559 mGeneratedTextColors = true; 560 } 561 } 562 563 @Override 564 public String toString() { 565 return new StringBuilder(getClass().getSimpleName()) 566 .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']') 567 .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']') 568 .append(" [Population: ").append(mPopulation).append(']') 569 .append(" [Title Text: #").append(Integer.toHexString(getTitleTextColor())) 570 .append(']') 571 .append(" [Body Text: #").append(Integer.toHexString(getBodyTextColor())) 572 .append(']').toString(); 573 } 574 575 @Override 576 public boolean equals(Object o) { 577 if (this == o) { 578 return true; 579 } 580 if (o == null || getClass() != o.getClass()) { 581 return false; 582 } 583 584 Swatch swatch = (Swatch) o; 585 return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb; 586 } 587 588 @Override 589 public int hashCode() { 590 return 31 * mRgb + mPopulation; 591 } 592 } 593 594 /** 595 * Builder class for generating {@link Palette} instances. 596 */ 597 public static final class Builder { 598 private final List<Swatch> mSwatches; 599 private final Bitmap mBitmap; 600 601 private final List<Target> mTargets = new ArrayList<>(); 602 603 private int mMaxColors = DEFAULT_CALCULATE_NUMBER_COLORS; 604 private int mResizeArea = DEFAULT_RESIZE_BITMAP_AREA; 605 private int mResizeMaxDimension = -1; 606 607 private final List<Filter> mFilters = new ArrayList<>(); 608 private Rect mRegion; 609 610 /** 611 * Construct a new {@link Builder} using a source {@link Bitmap} 612 */ 613 public Builder(Bitmap bitmap) { 614 if (bitmap == null || bitmap.isRecycled()) { 615 throw new IllegalArgumentException("Bitmap is not valid"); 616 } 617 mFilters.add(DEFAULT_FILTER); 618 mBitmap = bitmap; 619 mSwatches = null; 620 621 // Add the default targets 622 mTargets.add(Target.LIGHT_VIBRANT); 623 mTargets.add(Target.VIBRANT); 624 mTargets.add(Target.DARK_VIBRANT); 625 mTargets.add(Target.LIGHT_MUTED); 626 mTargets.add(Target.MUTED); 627 mTargets.add(Target.DARK_MUTED); 628 } 629 630 /** 631 * Construct a new {@link Builder} using a list of {@link Swatch} instances. 632 * Typically only used for testing. 633 */ 634 public Builder(List<Swatch> swatches) { 635 if (swatches == null || swatches.isEmpty()) { 636 throw new IllegalArgumentException("List of Swatches is not valid"); 637 } 638 mFilters.add(DEFAULT_FILTER); 639 mSwatches = swatches; 640 mBitmap = null; 641 } 642 643 /** 644 * Set the maximum number of colors to use in the quantization step when using a 645 * {@link android.graphics.Bitmap} as the source. 646 * <p> 647 * Good values for depend on the source image type. For landscapes, good values are in 648 * the range 10-16. For images which are largely made up of people's faces then this 649 * value should be increased to ~24. 650 */ 651 @NonNull 652 public Builder maximumColorCount(int colors) { 653 mMaxColors = colors; 654 return this; 655 } 656 657 /** 658 * Set the resize value when using a {@link android.graphics.Bitmap} as the source. 659 * If the bitmap's largest dimension is greater than the value specified, then the bitmap 660 * will be resized so that its largest dimension matches {@code maxDimension}. If the 661 * bitmap is smaller or equal, the original is used as-is. 662 * 663 * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle 664 * abnormal aspect ratios more gracefully. 665 * 666 * @param maxDimension the number of pixels that the max dimension should be scaled down to, 667 * or any value <= 0 to disable resizing. 668 */ 669 @NonNull 670 @Deprecated 671 public Builder resizeBitmapSize(final int maxDimension) { 672 mResizeMaxDimension = maxDimension; 673 mResizeArea = -1; 674 return this; 675 } 676 677 /** 678 * Set the resize value when using a {@link android.graphics.Bitmap} as the source. 679 * If the bitmap's area is greater than the value specified, then the bitmap 680 * will be resized so that its area matches {@code area}. If the 681 * bitmap is smaller or equal, the original is used as-is. 682 * <p> 683 * This value has a large effect on the processing time. The larger the resized image is, 684 * the greater time it will take to generate the palette. The smaller the image is, the 685 * more detail is lost in the resulting image and thus less precision for color selection. 686 * 687 * @param area the number of pixels that the intermediary scaled down Bitmap should cover, 688 * or any value <= 0 to disable resizing. 689 */ 690 @NonNull 691 public Builder resizeBitmapArea(final int area) { 692 mResizeArea = area; 693 mResizeMaxDimension = -1; 694 return this; 695 } 696 697 /** 698 * Clear all added filters. This includes any default filters added automatically by 699 * {@link Palette}. 700 */ 701 @NonNull 702 public Builder clearFilters() { 703 mFilters.clear(); 704 return this; 705 } 706 707 /** 708 * Add a filter to be able to have fine grained control over which colors are 709 * allowed in the resulting palette. 710 * 711 * @param filter filter to add. 712 */ 713 @NonNull 714 public Builder addFilter(Filter filter) { 715 if (filter != null) { 716 mFilters.add(filter); 717 } 718 return this; 719 } 720 721 /** 722 * Set a region of the bitmap to be used exclusively when calculating the palette. 723 * <p>This only works when the original input is a {@link Bitmap}.</p> 724 * 725 * @param left The left side of the rectangle used for the region. 726 * @param top The top of the rectangle used for the region. 727 * @param right The right side of the rectangle used for the region. 728 * @param bottom The bottom of the rectangle used for the region. 729 */ 730 @NonNull 731 public Builder setRegion(int left, int top, int right, int bottom) { 732 if (mBitmap != null) { 733 if (mRegion == null) mRegion = new Rect(); 734 // Set the Rect to be initially the whole Bitmap 735 mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); 736 // Now just get the intersection with the region 737 if (!mRegion.intersect(left, top, right, bottom)) { 738 throw new IllegalArgumentException("The given region must intersect with " 739 + "the Bitmap's dimensions."); 740 } 741 } 742 return this; 743 } 744 745 /** 746 * Clear any previously region set via {@link #setRegion(int, int, int, int)}. 747 */ 748 @NonNull 749 public Builder clearRegion() { 750 mRegion = null; 751 return this; 752 } 753 754 /** 755 * Add a target profile to be generated in the palette. 756 * 757 * <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p> 758 */ 759 @NonNull 760 public Builder addTarget(@NonNull final Target target) { 761 if (!mTargets.contains(target)) { 762 mTargets.add(target); 763 } 764 return this; 765 } 766 767 /** 768 * Clear all added targets. This includes any default targets added automatically by 769 * {@link Palette}. 770 */ 771 @NonNull 772 public Builder clearTargets() { 773 if (mTargets != null) { 774 mTargets.clear(); 775 } 776 return this; 777 } 778 779 /** 780 * Generate and return the {@link Palette} synchronously. 781 */ 782 @NonNull 783 public Palette generate() { 784 final TimingLogger logger = LOG_TIMINGS 785 ? new TimingLogger(LOG_TAG, "Generation") 786 : null; 787 788 List<Swatch> swatches; 789 790 if (mBitmap != null) { 791 // We have a Bitmap so we need to use quantization to reduce the number of colors 792 793 // First we'll scale down the bitmap if needed 794 final Bitmap bitmap = scaleBitmapDown(mBitmap); 795 796 if (logger != null) { 797 logger.addSplit("Processed Bitmap"); 798 } 799 800 final Rect region = mRegion; 801 if (bitmap != mBitmap && region != null) { 802 // If we have a scaled bitmap and a selected region, we need to scale down the 803 // region to match the new scale 804 final double scale = bitmap.getWidth() / (double) mBitmap.getWidth(); 805 region.left = (int) Math.floor(region.left * scale); 806 region.top = (int) Math.floor(region.top * scale); 807 region.right = Math.min((int) Math.ceil(region.right * scale), 808 bitmap.getWidth()); 809 region.bottom = Math.min((int) Math.ceil(region.bottom * scale), 810 bitmap.getHeight()); 811 } 812 813 // Now generate a quantizer from the Bitmap 814 final ColorCutQuantizer quantizer = new ColorCutQuantizer( 815 getPixelsFromBitmap(bitmap), 816 mMaxColors, 817 mFilters.isEmpty() ? null : mFilters.toArray(new Filter[mFilters.size()])); 818 819 // If created a new bitmap, recycle it 820 if (bitmap != mBitmap) { 821 bitmap.recycle(); 822 } 823 824 swatches = quantizer.getQuantizedColors(); 825 826 if (logger != null) { 827 logger.addSplit("Color quantization completed"); 828 } 829 } else { 830 // Else we're using the provided swatches 831 swatches = mSwatches; 832 } 833 834 // Now create a Palette instance 835 final Palette p = new Palette(swatches, mTargets); 836 // And make it generate itself 837 p.generate(); 838 839 if (logger != null) { 840 logger.addSplit("Created Palette"); 841 logger.dumpToLog(); 842 } 843 844 return p; 845 } 846 847 /** 848 * Generate the {@link Palette} asynchronously. The provided listener's 849 * {@link PaletteAsyncListener#onGenerated} method will be called with the palette when 850 * generated. 851 */ 852 @NonNull 853 public AsyncTask<Bitmap, Void, Palette> generate(final PaletteAsyncListener listener) { 854 if (listener == null) { 855 throw new IllegalArgumentException("listener can not be null"); 856 } 857 858 return new AsyncTask<Bitmap, Void, Palette>() { 859 @Override 860 protected Palette doInBackground(Bitmap... params) { 861 try { 862 return generate(); 863 } catch (Exception e) { 864 Log.e(LOG_TAG, "Exception thrown during async generate", e); 865 return null; 866 } 867 } 868 869 @Override 870 protected void onPostExecute(Palette colorExtractor) { 871 listener.onGenerated(colorExtractor); 872 } 873 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap); 874 } 875 876 private int[] getPixelsFromBitmap(Bitmap bitmap) { 877 final int bitmapWidth = bitmap.getWidth(); 878 final int bitmapHeight = bitmap.getHeight(); 879 final int[] pixels = new int[bitmapWidth * bitmapHeight]; 880 bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight); 881 882 if (mRegion == null) { 883 // If we don't have a region, return all of the pixels 884 return pixels; 885 } else { 886 // If we do have a region, lets create a subset array containing only the region's 887 // pixels 888 final int regionWidth = mRegion.width(); 889 final int regionHeight = mRegion.height(); 890 // pixels contains all of the pixels, so we need to iterate through each row and 891 // copy the regions pixels into a new smaller array 892 final int[] subsetPixels = new int[regionWidth * regionHeight]; 893 for (int row = 0; row < regionHeight; row++) { 894 System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left, 895 subsetPixels, row * regionWidth, regionWidth); 896 } 897 return subsetPixels; 898 } 899 } 900 901 /** 902 * Scale the bitmap down as needed. 903 */ 904 private Bitmap scaleBitmapDown(final Bitmap bitmap) { 905 double scaleRatio = -1; 906 907 if (mResizeArea > 0) { 908 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight(); 909 if (bitmapArea > mResizeArea) { 910 scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea); 911 } 912 } else if (mResizeMaxDimension > 0) { 913 final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight()); 914 if (maxDimension > mResizeMaxDimension) { 915 scaleRatio = mResizeMaxDimension / (double) maxDimension; 916 } 917 } 918 919 if (scaleRatio <= 0) { 920 // Scaling has been disabled or not needed so just return the Bitmap 921 return bitmap; 922 } 923 924 return Bitmap.createScaledBitmap(bitmap, 925 (int) Math.ceil(bitmap.getWidth() * scaleRatio), 926 (int) Math.ceil(bitmap.getHeight() * scaleRatio), 927 false); 928 } 929 } 930 931 /** 932 * A Filter provides a mechanism for exercising fine-grained control over which colors 933 * are valid within a resulting {@link Palette}. 934 */ 935 public interface Filter { 936 /** 937 * Hook to allow clients to be able filter colors from resulting palette. 938 * 939 * @param rgb the color in RGB888. 940 * @param hsl HSL representation of the color. 941 * 942 * @return true if the color is allowed, false if not. 943 * 944 * @see Builder#addFilter(Filter) 945 */ 946 boolean isAllowed(int rgb, float[] hsl); 947 } 948 949 /** 950 * The default filter. 951 */ 952 static final Filter DEFAULT_FILTER = new Filter() { 953 private static final float BLACK_MAX_LIGHTNESS = 0.05f; 954 private static final float WHITE_MIN_LIGHTNESS = 0.95f; 955 956 @Override 957 public boolean isAllowed(int rgb, float[] hsl) { 958 return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl); 959 } 960 961 /** 962 * @return true if the color represents a color which is close to black. 963 */ 964 private boolean isBlack(float[] hslColor) { 965 return hslColor[2] <= BLACK_MAX_LIGHTNESS; 966 } 967 968 /** 969 * @return true if the color represents a color which is close to white. 970 */ 971 private boolean isWhite(float[] hslColor) { 972 return hslColor[2] >= WHITE_MIN_LIGHTNESS; 973 } 974 975 /** 976 * @return true if the color lies close to the red side of the I line. 977 */ 978 private boolean isNearRedILine(float[] hslColor) { 979 return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f; 980 } 981 }; 982} 983