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