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