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