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