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.os.AsyncTask;
22import android.support.v4.os.AsyncTaskCompat;
23
24import java.util.Arrays;
25import java.util.Collections;
26import java.util.List;
27
28/**
29 * A helper class to extract prominent colors from an image.
30 * <p>
31 * A number of colors with different profiles are extracted from the image:
32 * <ul>
33 *     <li>Vibrant</li>
34 *     <li>Vibrant Dark</li>
35 *     <li>Vibrant Light</li>
36 *     <li>Muted</li>
37 *     <li>Muted Dark</li>
38 *     <li>Muted Light</li>
39 * </ul>
40 * These can be retrieved from the appropriate getter method.
41 *
42 * <p>
43 * Instances can be created with the synchronous factory methods {@link #generate(Bitmap)} and
44 * {@link #generate(Bitmap, int)}.
45 * <p>
46 * These should be called on a background thread, ideally the one in
47 * which you load your images on. Sometimes that is not possible, so asynchronous factory methods
48 * have also been provided: {@link #generateAsync(Bitmap, PaletteAsyncListener)} and
49 * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}. These can be used as so:
50 *
51 * <pre>
52 * Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() {
53 *     public void onGenerated(Palette palette) {
54 *         // Do something with colors...
55 *     }
56 * });
57 * </pre>
58 */
59public final class Palette {
60
61    /**
62     * Listener to be used with {@link #generateAsync(Bitmap, PaletteAsyncListener)} or
63     * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}
64     */
65    public interface PaletteAsyncListener {
66
67        /**
68         * Called when the {@link Palette} has been generated.
69         */
70        void onGenerated(Palette palette);
71    }
72
73    private static final int CALCULATE_BITMAP_MIN_DIMENSION = 100;
74    private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
75
76    private static final float TARGET_DARK_LUMA = 0.26f;
77    private static final float MAX_DARK_LUMA = 0.45f;
78
79    private static final float MIN_LIGHT_LUMA = 0.55f;
80    private static final float TARGET_LIGHT_LUMA = 0.74f;
81
82    private static final float MIN_NORMAL_LUMA = 0.3f;
83    private static final float TARGET_NORMAL_LUMA = 0.5f;
84    private static final float MAX_NORMAL_LUMA = 0.7f;
85
86    private static final float TARGET_MUTED_SATURATION = 0.3f;
87    private static final float MAX_MUTED_SATURATION = 0.4f;
88
89    private static final float TARGET_VIBRANT_SATURATION = 1f;
90    private static final float MIN_VIBRANT_SATURATION = 0.35f;
91
92    private static final float WEIGHT_SATURATION = 3f;
93    private static final float WEIGHT_LUMA = 6f;
94    private static final float WEIGHT_POPULATION = 1f;
95
96    private static final float MIN_CONTRAST_TITLE_TEXT = 3.0f;
97    private static final float MIN_CONTRAST_BODY_TEXT = 4.5f;
98
99    private final List<Swatch> mSwatches;
100    private final int mHighestPopulation;
101
102    private Swatch mVibrantSwatch;
103    private Swatch mMutedSwatch;
104
105    private Swatch mDarkVibrantSwatch;
106    private Swatch mDarkMutedSwatch;
107
108    private Swatch mLightVibrantSwatch;
109    private Swatch mLightMutedColor;
110
111    /**
112     * Generate a {@link Palette} from a {@link Bitmap} using the default number of colors.
113     */
114    public static Palette generate(Bitmap bitmap) {
115        return generate(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS);
116    }
117
118    /**
119     * Generate a {@link Palette} from a {@link Bitmap} using the specified {@code numColors}.
120     * Good values for {@code numColors} depend on the source image type.
121     * For landscapes, a good values are in the range 12-16. For images which are largely made up
122     * of people's faces then this value should be increased to 24-32.
123     *
124     * @param numColors The maximum number of colors in the generated palette. Increasing this
125     *                  number will increase the time needed to compute the values.
126     */
127    public static Palette generate(Bitmap bitmap, int numColors) {
128        checkBitmapParam(bitmap);
129        checkNumberColorsParam(numColors);
130
131        // First we'll scale down the bitmap so it's shortest dimension is 100px
132        final Bitmap scaledBitmap = scaleBitmapDown(bitmap);
133
134        // Now generate a quantizer from the Bitmap
135        ColorCutQuantizer quantizer = ColorCutQuantizer.fromBitmap(scaledBitmap, numColors);
136
137        // If created a new bitmap, recycle it
138        if (scaledBitmap != bitmap) {
139            scaledBitmap.recycle();
140        }
141
142        // Now return a ColorExtractor instance
143        return new Palette(quantizer.getQuantizedColors());
144    }
145
146    /**
147     * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)}
148     * will be called with the created instance. The resulting {@link Palette} is the same as
149     * what would be created by calling {@link #generate(Bitmap)}.
150     *
151     * @param listener Listener to be invoked when the {@link Palette} has been generated.
152     *
153     * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance.
154     */
155    public static AsyncTask<Bitmap, Void, Palette> generateAsync(
156            Bitmap bitmap, PaletteAsyncListener listener) {
157        return generateAsync(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS, listener);
158    }
159
160    /**
161     * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)}
162     * will be called with the created instance. The resulting {@link Palette} is the same as what
163     * would be created by calling {@link #generate(Bitmap, int)}.
164     *
165     * @param listener Listener to be invoked when the {@link Palette} has been generated.
166     *
167     * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance.
168     */
169    public static AsyncTask<Bitmap, Void, Palette> generateAsync(
170            final Bitmap bitmap, final int numColors, final PaletteAsyncListener listener) {
171        checkBitmapParam(bitmap);
172        checkNumberColorsParam(numColors);
173        checkAsyncListenerParam(listener);
174
175        return AsyncTaskCompat.executeParallel(
176                new AsyncTask<Bitmap, Void, Palette>() {
177                    @Override
178                    protected Palette doInBackground(Bitmap... params) {
179                        return generate(params[0], numColors);
180                    }
181
182                    @Override
183                    protected void onPostExecute(Palette colorExtractor) {
184                        listener.onGenerated(colorExtractor);
185                    }
186                }, bitmap);
187    }
188
189    private Palette(List<Swatch> swatches) {
190        mSwatches = swatches;
191        mHighestPopulation = findMaxPopulation();
192
193        mVibrantSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
194                TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
195
196        mLightVibrantSwatch = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
197                TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
198
199        mDarkVibrantSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
200                TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
201
202        mMutedSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
203                TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
204
205        mLightMutedColor = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
206                TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
207
208        mDarkMutedSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
209                TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
210
211        // Now try and generate any missing colors
212        generateEmptySwatches();
213    }
214
215    /**
216     * Returns all of the swatches which make up the palette.
217     */
218    public List<Swatch> getSwatches() {
219        return Collections.unmodifiableList(mSwatches);
220    }
221
222    /**
223     * Returns the most vibrant swatch in the palette. Might be null.
224     */
225    public Swatch getVibrantSwatch() {
226        return mVibrantSwatch;
227    }
228
229    /**
230     * Returns a light and vibrant swatch from the palette. Might be null.
231     */
232    public Swatch getLightVibrantSwatch() {
233        return mLightVibrantSwatch;
234    }
235
236    /**
237     * Returns a dark and vibrant swatch from the palette. Might be null.
238     */
239    public Swatch getDarkVibrantSwatch() {
240        return mDarkVibrantSwatch;
241    }
242
243    /**
244     * Returns a muted swatch from the palette. Might be null.
245     */
246    public Swatch getMutedSwatch() {
247        return mMutedSwatch;
248    }
249
250    /**
251     * Returns a muted and light swatch from the palette. Might be null.
252     */
253    public Swatch getLightMutedSwatch() {
254        return mLightMutedColor;
255    }
256
257    /**
258     * Returns a muted and dark swatch from the palette. Might be null.
259     */
260    public Swatch getDarkMutedSwatch() {
261        return mDarkMutedSwatch;
262    }
263
264    /**
265     * Returns the most vibrant color in the palette as an RGB packed int.
266     *
267     * @param defaultColor value to return if the swatch isn't available
268     */
269    public int getVibrantColor(int defaultColor) {
270        return mVibrantSwatch != null ? mVibrantSwatch.getRgb() : defaultColor;
271    }
272
273    /**
274     * Returns a light and vibrant color from the palette as an RGB packed int.
275     *
276     * @param defaultColor value to return if the swatch isn't available
277     */
278    public int getLightVibrantColor(int defaultColor) {
279        return mLightVibrantSwatch != null ? mLightVibrantSwatch.getRgb() : defaultColor;
280    }
281
282    /**
283     * Returns a dark and vibrant color from the palette as an RGB packed int.
284     *
285     * @param defaultColor value to return if the swatch isn't available
286     */
287    public int getDarkVibrantColor(int defaultColor) {
288        return mDarkVibrantSwatch != null ? mDarkVibrantSwatch.getRgb() : defaultColor;
289    }
290
291    /**
292     * Returns a muted color from the palette as an RGB packed int.
293     *
294     * @param defaultColor value to return if the swatch isn't available
295     */
296    public int getMutedColor(int defaultColor) {
297        return mMutedSwatch != null ? mMutedSwatch.getRgb() : defaultColor;
298    }
299
300    /**
301     * Returns a muted and light color from the palette as an RGB packed int.
302     *
303     * @param defaultColor value to return if the swatch isn't available
304     */
305    public int getLightMutedColor(int defaultColor) {
306        return mLightMutedColor != null ? mLightMutedColor.getRgb() : defaultColor;
307    }
308
309    /**
310     * Returns a muted and dark color from the palette as an RGB packed int.
311     *
312     * @param defaultColor value to return if the swatch isn't available
313     */
314    public int getDarkMutedColor(int defaultColor) {
315        return mDarkMutedSwatch != null ? mDarkMutedSwatch.getRgb() : defaultColor;
316    }
317
318    /**
319     * @return true if we have already selected {@code swatch}
320     */
321    private boolean isAlreadySelected(Swatch swatch) {
322        return mVibrantSwatch == swatch || mDarkVibrantSwatch == swatch ||
323                mLightVibrantSwatch == swatch || mMutedSwatch == swatch ||
324                mDarkMutedSwatch == swatch || mLightMutedColor == swatch;
325    }
326
327    private Swatch findColor(float targetLuma, float minLuma, float maxLuma,
328                             float targetSaturation, float minSaturation, float maxSaturation) {
329        Swatch max = null;
330        float maxValue = 0f;
331
332        for (Swatch swatch : mSwatches) {
333            final float sat = swatch.getHsl()[1];
334            final float luma = swatch.getHsl()[2];
335
336            if (sat >= minSaturation && sat <= maxSaturation &&
337                    luma >= minLuma && luma <= maxLuma &&
338                    !isAlreadySelected(swatch)) {
339                float thisValue = createComparisonValue(sat, targetSaturation, luma, targetLuma,
340                        swatch.getPopulation(), mHighestPopulation);
341                if (max == null || thisValue > maxValue) {
342                    max = swatch;
343                    maxValue = thisValue;
344                }
345            }
346        }
347
348        return max;
349    }
350
351    /**
352     * Try and generate any missing swatches from the swatches we did find.
353     */
354    private void generateEmptySwatches() {
355        if (mVibrantSwatch == null) {
356            // If we do not have a vibrant color...
357            if (mDarkVibrantSwatch != null) {
358                // ...but we do have a dark vibrant, generate the value by modifying the luma
359                final float[] newHsl = copyHslValues(mDarkVibrantSwatch);
360                newHsl[2] = TARGET_NORMAL_LUMA;
361                mVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0);
362            }
363        }
364
365        if (mDarkVibrantSwatch == null) {
366            // If we do not have a dark vibrant color...
367            if (mVibrantSwatch != null) {
368                // ...but we do have a vibrant, generate the value by modifying the luma
369                final float[] newHsl = copyHslValues(mVibrantSwatch);
370                newHsl[2] = TARGET_DARK_LUMA;
371                mDarkVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0);
372            }
373        }
374    }
375
376    /**
377     * Find the {@link Swatch} with the highest population value and return the population.
378     */
379    private int findMaxPopulation() {
380        int population = 0;
381        for (Swatch swatch : mSwatches) {
382            population = Math.max(population, swatch.getPopulation());
383        }
384        return population;
385    }
386
387    @Override
388    public boolean equals(Object o) {
389        if (this == o) {
390            return true;
391        }
392        if (o == null || getClass() != o.getClass()) {
393            return false;
394        }
395
396        Palette palette = (Palette) o;
397
398        if (mSwatches != null ? !mSwatches.equals(palette.mSwatches) : palette.mSwatches != null) {
399            return false;
400        }
401        if (mDarkMutedSwatch != null ? !mDarkMutedSwatch.equals(palette.mDarkMutedSwatch)
402                : palette.mDarkMutedSwatch != null) {
403            return false;
404        }
405        if (mDarkVibrantSwatch != null ? !mDarkVibrantSwatch.equals(palette.mDarkVibrantSwatch)
406                : palette.mDarkVibrantSwatch != null) {
407            return false;
408        }
409        if (mLightMutedColor != null ? !mLightMutedColor.equals(palette.mLightMutedColor)
410                : palette.mLightMutedColor != null) {
411            return false;
412        }
413        if (mLightVibrantSwatch != null ? !mLightVibrantSwatch.equals(palette.mLightVibrantSwatch)
414                : palette.mLightVibrantSwatch != null) {
415            return false;
416        }
417        if (mMutedSwatch != null ? !mMutedSwatch.equals(palette.mMutedSwatch)
418                : palette.mMutedSwatch != null) {
419            return false;
420        }
421        if (mVibrantSwatch != null ? !mVibrantSwatch.equals(palette.mVibrantSwatch)
422                : palette.mVibrantSwatch != null) {
423            return false;
424        }
425
426        return true;
427    }
428
429    @Override
430    public int hashCode() {
431        int result = mSwatches != null ? mSwatches.hashCode() : 0;
432        result = 31 * result + (mVibrantSwatch != null ? mVibrantSwatch.hashCode() : 0);
433        result = 31 * result + (mMutedSwatch != null ? mMutedSwatch.hashCode() : 0);
434        result = 31 * result + (mDarkVibrantSwatch != null ? mDarkVibrantSwatch.hashCode() : 0);
435        result = 31 * result + (mDarkMutedSwatch != null ? mDarkMutedSwatch.hashCode() : 0);
436        result = 31 * result + (mLightVibrantSwatch != null ? mLightVibrantSwatch.hashCode() : 0);
437        result = 31 * result + (mLightMutedColor != null ? mLightMutedColor.hashCode() : 0);
438        return result;
439    }
440
441    /**
442     * Scale the bitmap down so that it's smallest dimension is
443     * {@value #CALCULATE_BITMAP_MIN_DIMENSION}px. If {@code bitmap} is smaller than this, than it
444     * is returned.
445     */
446    private static Bitmap scaleBitmapDown(Bitmap bitmap) {
447        final int minDimension = Math.min(bitmap.getWidth(), bitmap.getHeight());
448
449        if (minDimension <= CALCULATE_BITMAP_MIN_DIMENSION) {
450            // If the bitmap is small enough already, just return it
451            return bitmap;
452        }
453
454        final float scaleRatio = CALCULATE_BITMAP_MIN_DIMENSION / (float) minDimension;
455        return Bitmap.createScaledBitmap(bitmap,
456                Math.round(bitmap.getWidth() * scaleRatio),
457                Math.round(bitmap.getHeight() * scaleRatio),
458                false);
459    }
460
461    private static float createComparisonValue(float saturation, float targetSaturation,
462            float luma, float targetLuma,
463            int population, int highestPopulation) {
464        return weightedMean(
465                invertDiff(saturation, targetSaturation), WEIGHT_SATURATION,
466                invertDiff(luma, targetLuma), WEIGHT_LUMA,
467                population / (float) highestPopulation, WEIGHT_POPULATION
468        );
469    }
470
471    /**
472     * Copy a {@link Swatch}'s HSL values into a new float[].
473     */
474    private static float[] copyHslValues(Swatch color) {
475        final float[] newHsl = new float[3];
476        System.arraycopy(color.getHsl(), 0, newHsl, 0, 3);
477        return newHsl;
478    }
479
480    /**
481     * Returns a value in the range 0-1. 1 is returned when {@code value} equals the
482     * {@code targetValue} and then decreases as the absolute difference between {@code value} and
483     * {@code targetValue} increases.
484     *
485     * @param value the item's value
486     * @param targetValue the value which we desire
487     */
488    private static float invertDiff(float value, float targetValue) {
489        return 1f - Math.abs(value - targetValue);
490    }
491
492    private static float weightedMean(float... values) {
493        float sum = 0f;
494        float sumWeight = 0f;
495
496        for (int i = 0; i < values.length; i += 2) {
497            float value = values[i];
498            float weight = values[i + 1];
499
500            sum += (value * weight);
501            sumWeight += weight;
502        }
503
504        return sum / sumWeight;
505    }
506
507    private static void checkBitmapParam(Bitmap bitmap) {
508        if (bitmap == null) {
509            throw new IllegalArgumentException("bitmap can not be null");
510        }
511        if (bitmap.isRecycled()) {
512            throw new IllegalArgumentException("bitmap can not be recycled");
513        }
514    }
515
516    private static void checkNumberColorsParam(int numColors) {
517        if (numColors < 1) {
518            throw new IllegalArgumentException("numColors must be 1 of greater");
519        }
520    }
521
522    private static void checkAsyncListenerParam(PaletteAsyncListener listener) {
523        if (listener == null) {
524            throw new IllegalArgumentException("listener can not be null");
525        }
526    }
527
528    /**
529     * Represents a color swatch generated from an image's palette. The RGB color can be retrieved
530     * by calling {@link #getRgb()}.
531     */
532    public static final class Swatch {
533        private final int mRed, mGreen, mBlue;
534        private final int mRgb;
535        private final int mPopulation;
536
537        private boolean mGeneratedTextColors;
538        private int mTitleTextColor;
539        private int mBodyTextColor;
540
541        private float[] mHsl;
542
543        Swatch(int rgbColor, int population) {
544            mRed = Color.red(rgbColor);
545            mGreen = Color.green(rgbColor);
546            mBlue = Color.blue(rgbColor);
547            mRgb = rgbColor;
548            mPopulation = population;
549        }
550
551        Swatch(int red, int green, int blue, int population) {
552            mRed = red;
553            mGreen = green;
554            mBlue = blue;
555            mRgb = Color.rgb(red, green, blue);
556            mPopulation = population;
557        }
558
559        /**
560         * @return this swatch's RGB color value
561         */
562        public int getRgb() {
563            return mRgb;
564        }
565
566        /**
567         * Return this swatch's HSL values.
568         *     hsv[0] is Hue [0 .. 360)
569         *     hsv[1] is Saturation [0...1]
570         *     hsv[2] is Lightness [0...1]
571         */
572        public float[] getHsl() {
573            if (mHsl == null) {
574                // Lazily generate HSL values from RGB
575                mHsl = new float[3];
576                ColorUtils.RGBtoHSL(mRed, mGreen, mBlue, mHsl);
577            }
578            return mHsl;
579        }
580
581        /**
582         * @return the number of pixels represented by this swatch
583         */
584        public int getPopulation() {
585            return mPopulation;
586        }
587
588        /**
589         * Returns an appropriate color to use for any 'title' text which is displayed over this
590         * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast.
591         */
592        public int getTitleTextColor() {
593            ensureTextColorsGenerated();
594            return mTitleTextColor;
595        }
596
597        /**
598         * Returns an appropriate color to use for any 'body' text which is displayed over this
599         * {@link Swatch}'s color. This color is guaranteed to have sufficient contrast.
600         */
601        public int getBodyTextColor() {
602            ensureTextColorsGenerated();
603            return mBodyTextColor;
604        }
605
606        private void ensureTextColorsGenerated() {
607            if (!mGeneratedTextColors) {
608                mTitleTextColor = ColorUtils.getTextColorForBackground(mRgb,
609                        MIN_CONTRAST_TITLE_TEXT);
610                mBodyTextColor = ColorUtils.getTextColorForBackground(mRgb,
611                        MIN_CONTRAST_BODY_TEXT);
612                mGeneratedTextColors = true;
613            }
614        }
615
616        @Override
617        public String toString() {
618            return new StringBuilder(getClass().getSimpleName())
619                    .append(" [RGB: #").append(Integer.toHexString(getRgb())).append(']')
620                    .append(" [HSL: ").append(Arrays.toString(getHsl())).append(']')
621                    .append(" [Population: ").append(mPopulation).append(']')
622                    .append(" [Title Text: #").append(Integer.toHexString(mTitleTextColor)).append(']')
623                    .append(" [Body Text: #").append(Integer.toHexString(mBodyTextColor)).append(']')
624                    .toString();
625        }
626
627        @Override
628        public boolean equals(Object o) {
629            if (this == o) {
630                return true;
631            }
632            if (o == null || getClass() != o.getClass()) {
633                return false;
634            }
635
636            Swatch swatch = (Swatch) o;
637            return mPopulation == swatch.mPopulation && mRgb == swatch.mRgb;
638        }
639
640        @Override
641        public int hashCode() {
642            return 31 * mRgb + mPopulation;
643        }
644    }
645
646}
647