• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.internal.graphics.palette;
18 
19 import android.annotation.ColorInt;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.graphics.Bitmap;
23 import android.graphics.Color;
24 import android.graphics.Rect;
25 import android.os.AsyncTask;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 import android.util.SparseBooleanArray;
29 import android.util.TimingLogger;
30 
31 import com.android.internal.graphics.ColorUtils;
32 
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.List;
37 import 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  */
77 public 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          */
onGenerated(Palette palette)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      */
from(Bitmap bitmap)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      */
from(List<Palette.Swatch> swatches)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
generate(Bitmap bitmap)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
generate(Bitmap bitmap, int numColors)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
generateAsync( Bitmap bitmap, Palette.PaletteAsyncListener listener)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
generateAsync( final Bitmap bitmap, final int numColors, final Palette.PaletteAsyncListener listener)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 
Palette(List<Palette.Swatch> swatches, List<Target> targets)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
getSwatches()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
getTargets()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
getVibrantSwatch()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
getLightVibrantSwatch()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
getDarkVibrantSwatch()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
getMutedSwatch()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
getLightMutedSwatch()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
getDarkMutedSwatch()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
getVibrantColor(@olorInt final int defaultColor)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
getLightVibrantColor(@olorInt final int defaultColor)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
getDarkVibrantColor(@olorInt final int defaultColor)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
getMutedColor(@olorInt final int defaultColor)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
getLightMutedColor(@olorInt final int defaultColor)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
getDarkMutedColor(@olorInt final int defaultColor)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
getSwatchForTarget(@onNull final Target target)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
getColorForTarget(@onNull final Target target, @ColorInt final int defaultColor)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
getDominantSwatch()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
getDominantColor(@olorInt int defaultColor)348     public int getDominantColor(@ColorInt int defaultColor) {
349         return mDominantSwatch != null ? mDominantSwatch.getRgb() : defaultColor;
350     }
351 
generate()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 
generateScoredTarget(final Target target)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 
getMaxScoredSwatchForTarget(final Target target)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 
shouldBeScoredForTarget(final Palette.Swatch swatch, final Target target)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 
generateScore(Palette.Swatch swatch, Target target)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 
findDominantSwatch()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 
copyHslValues(Palette.Swatch color)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 
Swatch(@olorInt int color, int population)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 
Swatch(int red, int green, int blue, int population)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 
Swatch(float[] hsl, int population)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
getRgb()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          */
getHsl()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          */
getPopulation()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
getTitleTextColor()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
getBodyTextColor()522         public int getBodyTextColor() {
523             ensureTextColorsGenerated();
524             return mBodyTextColor;
525         }
526 
ensureTextColorsGenerated()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
toString()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
equals(Object o)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
hashCode()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         private Quantizer mQuantizer;
617 
618         /**
619          * Construct a new {@link Palette.Builder} using a source {@link Bitmap}
620          */
Builder(Bitmap bitmap)621         public Builder(Bitmap bitmap) {
622             if (bitmap == null || bitmap.isRecycled()) {
623                 throw new IllegalArgumentException("Bitmap is not valid");
624             }
625             mFilters.add(DEFAULT_FILTER);
626             mBitmap = bitmap;
627             mSwatches = null;
628 
629             // Add the default targets
630             mTargets.add(Target.LIGHT_VIBRANT);
631             mTargets.add(Target.VIBRANT);
632             mTargets.add(Target.DARK_VIBRANT);
633             mTargets.add(Target.LIGHT_MUTED);
634             mTargets.add(Target.MUTED);
635             mTargets.add(Target.DARK_MUTED);
636         }
637 
638         /**
639          * Construct a new {@link Palette.Builder} using a list of {@link Palette.Swatch} instances.
640          * Typically only used for testing.
641          */
Builder(List<Palette.Swatch> swatches)642         public Builder(List<Palette.Swatch> swatches) {
643             if (swatches == null || swatches.isEmpty()) {
644                 throw new IllegalArgumentException("List of Swatches is not valid");
645             }
646             mFilters.add(DEFAULT_FILTER);
647             mSwatches = swatches;
648             mBitmap = null;
649         }
650 
651         /**
652          * Set the maximum number of colors to use in the quantization step when using a
653          * {@link android.graphics.Bitmap} as the source.
654          * <p>
655          * Good values for depend on the source image type. For landscapes, good values are in
656          * the range 10-16. For images which are largely made up of people's faces then this
657          * value should be increased to ~24.
658          */
659         @NonNull
maximumColorCount(int colors)660         public Palette.Builder maximumColorCount(int colors) {
661             mMaxColors = colors;
662             return this;
663         }
664 
665         /**
666          * Set the resize value when using a {@link android.graphics.Bitmap} as the source.
667          * If the bitmap's largest dimension is greater than the value specified, then the bitmap
668          * will be resized so that its largest dimension matches {@code maxDimension}. If the
669          * bitmap is smaller or equal, the original is used as-is.
670          *
671          * @deprecated Using {@link #resizeBitmapArea(int)} is preferred since it can handle
672          * abnormal aspect ratios more gracefully.
673          *
674          * @param maxDimension the number of pixels that the max dimension should be scaled down to,
675          *                     or any value <= 0 to disable resizing.
676          */
677         @NonNull
678         @Deprecated
resizeBitmapSize(final int maxDimension)679         public Palette.Builder resizeBitmapSize(final int maxDimension) {
680             mResizeMaxDimension = maxDimension;
681             mResizeArea = -1;
682             return this;
683         }
684 
685         /**
686          * Set the resize value when using a {@link android.graphics.Bitmap} as the source.
687          * If the bitmap's area is greater than the value specified, then the bitmap
688          * will be resized so that its area matches {@code area}. If the
689          * bitmap is smaller or equal, the original is used as-is.
690          * <p>
691          * This value has a large effect on the processing time. The larger the resized image is,
692          * the greater time it will take to generate the palette. The smaller the image is, the
693          * more detail is lost in the resulting image and thus less precision for color selection.
694          *
695          * @param area the number of pixels that the intermediary scaled down Bitmap should cover,
696          *             or any value <= 0 to disable resizing.
697          */
698         @NonNull
resizeBitmapArea(final int area)699         public Palette.Builder resizeBitmapArea(final int area) {
700             mResizeArea = area;
701             mResizeMaxDimension = -1;
702             return this;
703         }
704 
705         /**
706          * Clear all added filters. This includes any default filters added automatically by
707          * {@link Palette}.
708          */
709         @NonNull
clearFilters()710         public Palette.Builder clearFilters() {
711             mFilters.clear();
712             return this;
713         }
714 
715         /**
716          * Add a filter to be able to have fine grained control over which colors are
717          * allowed in the resulting palette.
718          *
719          * @param filter filter to add.
720          */
721         @NonNull
addFilter( Palette.Filter filter)722         public Palette.Builder addFilter(
723                 Palette.Filter filter) {
724             if (filter != null) {
725                 mFilters.add(filter);
726             }
727             return this;
728         }
729 
730         /**
731          * Set a specific quantization algorithm. {@link ColorCutQuantizer} will
732          * be used if unspecified.
733          *
734          * @param quantizer Quantizer implementation.
735          */
736         @NonNull
setQuantizer(Quantizer quantizer)737         public Palette.Builder setQuantizer(Quantizer quantizer) {
738             mQuantizer = quantizer;
739             return this;
740         }
741 
742         /**
743          * Set a region of the bitmap to be used exclusively when calculating the palette.
744          * <p>This only works when the original input is a {@link Bitmap}.</p>
745          *
746          * @param left The left side of the rectangle used for the region.
747          * @param top The top of the rectangle used for the region.
748          * @param right The right side of the rectangle used for the region.
749          * @param bottom The bottom of the rectangle used for the region.
750          */
751         @NonNull
setRegion(int left, int top, int right, int bottom)752         public Palette.Builder setRegion(int left, int top, int right, int bottom) {
753             if (mBitmap != null) {
754                 if (mRegion == null) mRegion = new Rect();
755                 // Set the Rect to be initially the whole Bitmap
756                 mRegion.set(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
757                 // Now just get the intersection with the region
758                 if (!mRegion.intersect(left, top, right, bottom)) {
759                     throw new IllegalArgumentException("The given region must intersect with "
760                             + "the Bitmap's dimensions.");
761                 }
762             }
763             return this;
764         }
765 
766         /**
767          * Clear any previously region set via {@link #setRegion(int, int, int, int)}.
768          */
769         @NonNull
clearRegion()770         public Palette.Builder clearRegion() {
771             mRegion = null;
772             return this;
773         }
774 
775         /**
776          * Add a target profile to be generated in the palette.
777          *
778          * <p>You can retrieve the result via {@link Palette#getSwatchForTarget(Target)}.</p>
779          */
780         @NonNull
addTarget(@onNull final Target target)781         public Palette.Builder addTarget(@NonNull final Target target) {
782             if (!mTargets.contains(target)) {
783                 mTargets.add(target);
784             }
785             return this;
786         }
787 
788         /**
789          * Clear all added targets. This includes any default targets added automatically by
790          * {@link Palette}.
791          */
792         @NonNull
clearTargets()793         public Palette.Builder clearTargets() {
794             if (mTargets != null) {
795                 mTargets.clear();
796             }
797             return this;
798         }
799 
800         /**
801          * Generate and return the {@link Palette} synchronously.
802          */
803         @NonNull
generate()804         public Palette generate() {
805             final TimingLogger logger = LOG_TIMINGS
806                     ? new TimingLogger(LOG_TAG, "Generation")
807                     : null;
808 
809             List<Palette.Swatch> swatches;
810 
811             if (mBitmap != null) {
812                 // We have a Bitmap so we need to use quantization to reduce the number of colors
813 
814                 // First we'll scale down the bitmap if needed
815                 final Bitmap bitmap = scaleBitmapDown(mBitmap);
816 
817                 if (logger != null) {
818                     logger.addSplit("Processed Bitmap");
819                 }
820 
821                 final Rect region = mRegion;
822                 if (bitmap != mBitmap && region != null) {
823                     // If we have a scaled bitmap and a selected region, we need to scale down the
824                     // region to match the new scale
825                     final double scale = bitmap.getWidth() / (double) mBitmap.getWidth();
826                     region.left = (int) Math.floor(region.left * scale);
827                     region.top = (int) Math.floor(region.top * scale);
828                     region.right = Math.min((int) Math.ceil(region.right * scale),
829                             bitmap.getWidth());
830                     region.bottom = Math.min((int) Math.ceil(region.bottom * scale),
831                             bitmap.getHeight());
832                 }
833 
834                 // Now generate a quantizer from the Bitmap
835                 if (mQuantizer == null) {
836                     mQuantizer = new ColorCutQuantizer();
837                 }
838                 mQuantizer.quantize(getPixelsFromBitmap(bitmap),
839                             mMaxColors, mFilters.isEmpty() ? null :
840                             mFilters.toArray(new Palette.Filter[mFilters.size()]));
841 
842                 // If created a new bitmap, recycle it
843                 if (bitmap != mBitmap) {
844                     bitmap.recycle();
845                 }
846 
847                 swatches = mQuantizer.getQuantizedColors();
848 
849                 if (logger != null) {
850                     logger.addSplit("Color quantization completed");
851                 }
852             } else {
853                 // Else we're using the provided swatches
854                 swatches = mSwatches;
855             }
856 
857             // Now create a Palette instance
858             final Palette p = new Palette(swatches, mTargets);
859             // And make it generate itself
860             p.generate();
861 
862             if (logger != null) {
863                 logger.addSplit("Created Palette");
864                 logger.dumpToLog();
865             }
866 
867             return p;
868         }
869 
870         /**
871          * Generate the {@link Palette} asynchronously. The provided listener's
872          * {@link Palette.PaletteAsyncListener#onGenerated} method will be called with the palette when
873          * generated.
874          */
875         @NonNull
generate(final Palette.PaletteAsyncListener listener)876         public AsyncTask<Bitmap, Void, Palette> generate(final Palette.PaletteAsyncListener listener) {
877             if (listener == null) {
878                 throw new IllegalArgumentException("listener can not be null");
879             }
880 
881             return new AsyncTask<Bitmap, Void, Palette>() {
882                 @Override
883                 protected Palette doInBackground(Bitmap... params) {
884                     try {
885                         return generate();
886                     } catch (Exception e) {
887                         Log.e(LOG_TAG, "Exception thrown during async generate", e);
888                         return null;
889                     }
890                 }
891 
892                 @Override
893                 protected void onPostExecute(Palette colorExtractor) {
894                     listener.onGenerated(colorExtractor);
895                 }
896             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap);
897         }
898 
899         private int[] getPixelsFromBitmap(Bitmap bitmap) {
900             final int bitmapWidth = bitmap.getWidth();
901             final int bitmapHeight = bitmap.getHeight();
902             final int[] pixels = new int[bitmapWidth * bitmapHeight];
903             bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight);
904 
905             if (mRegion == null) {
906                 // If we don't have a region, return all of the pixels
907                 return pixels;
908             } else {
909                 // If we do have a region, lets create a subset array containing only the region's
910                 // pixels
911                 final int regionWidth = mRegion.width();
912                 final int regionHeight = mRegion.height();
913                 // pixels contains all of the pixels, so we need to iterate through each row and
914                 // copy the regions pixels into a new smaller array
915                 final int[] subsetPixels = new int[regionWidth * regionHeight];
916                 for (int row = 0; row < regionHeight; row++) {
917                     System.arraycopy(pixels, ((row + mRegion.top) * bitmapWidth) + mRegion.left,
918                             subsetPixels, row * regionWidth, regionWidth);
919                 }
920                 return subsetPixels;
921             }
922         }
923 
924         /**
925          * Scale the bitmap down as needed.
926          */
927         private Bitmap scaleBitmapDown(final Bitmap bitmap) {
928             double scaleRatio = -1;
929 
930             if (mResizeArea > 0) {
931                 final int bitmapArea = bitmap.getWidth() * bitmap.getHeight();
932                 if (bitmapArea > mResizeArea) {
933                     scaleRatio = Math.sqrt(mResizeArea / (double) bitmapArea);
934                 }
935             } else if (mResizeMaxDimension > 0) {
936                 final int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
937                 if (maxDimension > mResizeMaxDimension) {
938                     scaleRatio = mResizeMaxDimension / (double) maxDimension;
939                 }
940             }
941 
942             if (scaleRatio <= 0) {
943                 // Scaling has been disabled or not needed so just return the Bitmap
944                 return bitmap;
945             }
946 
947             return Bitmap.createScaledBitmap(bitmap,
948                     (int) Math.ceil(bitmap.getWidth() * scaleRatio),
949                     (int) Math.ceil(bitmap.getHeight() * scaleRatio),
950                     false);
951         }
952     }
953 
954     /**
955      * A Filter provides a mechanism for exercising fine-grained control over which colors
956      * are valid within a resulting {@link Palette}.
957      */
958     public interface Filter {
959         /**
960          * Hook to allow clients to be able filter colors from resulting palette.
961          *
962          * @param rgb the color in RGB888.
963          * @param hsl HSL representation of the color.
964          *
965          * @return true if the color is allowed, false if not.
966          *
967          * @see Palette.Builder#addFilter(Palette.Filter)
968          */
969         boolean isAllowed(int rgb, float[] hsl);
970     }
971 
972     /**
973      * The default filter.
974      */
975     static final Palette.Filter
976             DEFAULT_FILTER = new Palette.Filter() {
977         private static final float BLACK_MAX_LIGHTNESS = 0.05f;
978         private static final float WHITE_MIN_LIGHTNESS = 0.95f;
979 
980         @Override
981         public boolean isAllowed(int rgb, float[] hsl) {
982             return !isWhite(hsl) && !isBlack(hsl) && !isNearRedILine(hsl);
983         }
984 
985         /**
986          * @return true if the color represents a color which is close to black.
987          */
988         private boolean isBlack(float[] hslColor) {
989             return hslColor[2] <= BLACK_MAX_LIGHTNESS;
990         }
991 
992         /**
993          * @return true if the color represents a color which is close to white.
994          */
995         private boolean isWhite(float[] hslColor) {
996             return hslColor[2] >= WHITE_MIN_LIGHTNESS;
997         }
998 
999         /**
1000          * @return true if the color lies close to the red side of the I line.
1001          */
1002         private boolean isNearRedILine(float[] hslColor) {
1003             return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f;
1004         }
1005     };
1006 }
1007